@primitivedotdev/cli 0.32.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
+ import { A as createClient, C as saveCliCredentials, D as loadChatConversationByLocalId, E as loadActiveChatState, O as saveActiveChatState, S as resolveCliAuth, T as deleteChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as normalizeApiBaseUrl1, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createConfig, k as PrimitiveApiClient, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as chatStatePath, x as normalizeApiBaseUrl2, y as loadCliCredentials } from "../cli-config-D9nB6fOW.js";
1
2
  import { Args, Command, Errors, Flags } from "@oclif/core";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
4
  import { randomUUID } from "node:crypto";
4
- import { basename, dirname, join, resolve } from "node:path";
5
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
6
  import { hostname } from "node:os";
6
7
  import process$1 from "node:process";
7
8
  import { createInterface } from "node:readline/promises";
@@ -18,614 +19,6 @@ var __exportAll = (all, no_symbols) => {
18
19
  return target;
19
20
  };
20
21
  //#endregion
21
- //#region ../packages/api-core/src/api/core/bodySerializer.gen.ts
22
- const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
23
- //#endregion
24
- //#region ../packages/api-core/src/api/core/serverSentEvents.gen.ts
25
- function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
26
- let lastEventId;
27
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
28
- const createStream = async function* () {
29
- let retryDelay = sseDefaultRetryDelay ?? 3e3;
30
- let attempt = 0;
31
- const signal = options.signal ?? new AbortController().signal;
32
- while (true) {
33
- if (signal.aborted) break;
34
- attempt++;
35
- const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
36
- if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
37
- try {
38
- const requestInit = {
39
- redirect: "follow",
40
- ...options,
41
- body: options.serializedBody,
42
- headers,
43
- signal
44
- };
45
- let request = new Request(url, requestInit);
46
- if (onRequest) request = await onRequest(url, requestInit);
47
- const response = await (options.fetch ?? globalThis.fetch)(request);
48
- if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
49
- if (!response.body) throw new Error("No body in SSE response");
50
- const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
51
- let buffer = "";
52
- const abortHandler = () => {
53
- try {
54
- reader.cancel();
55
- } catch {}
56
- };
57
- signal.addEventListener("abort", abortHandler);
58
- try {
59
- while (true) {
60
- const { done, value } = await reader.read();
61
- if (done) break;
62
- buffer += value;
63
- buffer = buffer.replace(/\r\n?/g, "\n");
64
- const chunks = buffer.split("\n\n");
65
- buffer = chunks.pop() ?? "";
66
- for (const chunk of chunks) {
67
- const lines = chunk.split("\n");
68
- const dataLines = [];
69
- let eventName;
70
- for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
71
- else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
72
- else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
73
- else if (line.startsWith("retry:")) {
74
- const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
75
- if (!Number.isNaN(parsed)) retryDelay = parsed;
76
- }
77
- let data;
78
- let parsedJson = false;
79
- if (dataLines.length) {
80
- const rawData = dataLines.join("\n");
81
- try {
82
- data = JSON.parse(rawData);
83
- parsedJson = true;
84
- } catch {
85
- data = rawData;
86
- }
87
- }
88
- if (parsedJson) {
89
- if (responseValidator) await responseValidator(data);
90
- if (responseTransformer) data = await responseTransformer(data);
91
- }
92
- onSseEvent?.({
93
- data,
94
- event: eventName,
95
- id: lastEventId,
96
- retry: retryDelay
97
- });
98
- if (dataLines.length) yield data;
99
- }
100
- }
101
- } finally {
102
- signal.removeEventListener("abort", abortHandler);
103
- reader.releaseLock();
104
- }
105
- break;
106
- } catch (error) {
107
- onSseError?.(error);
108
- if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
109
- await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
110
- }
111
- }
112
- };
113
- return { stream: createStream() };
114
- }
115
- //#endregion
116
- //#region ../packages/api-core/src/api/core/pathSerializer.gen.ts
117
- const separatorArrayExplode = (style) => {
118
- switch (style) {
119
- case "label": return ".";
120
- case "matrix": return ";";
121
- case "simple": return ",";
122
- default: return "&";
123
- }
124
- };
125
- const separatorArrayNoExplode = (style) => {
126
- switch (style) {
127
- case "form": return ",";
128
- case "pipeDelimited": return "|";
129
- case "spaceDelimited": return "%20";
130
- default: return ",";
131
- }
132
- };
133
- const separatorObjectExplode = (style) => {
134
- switch (style) {
135
- case "label": return ".";
136
- case "matrix": return ";";
137
- case "simple": return ",";
138
- default: return "&";
139
- }
140
- };
141
- const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
142
- if (!explode) {
143
- const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
144
- switch (style) {
145
- case "label": return `.${joinedValues}`;
146
- case "matrix": return `;${name}=${joinedValues}`;
147
- case "simple": return joinedValues;
148
- default: return `${name}=${joinedValues}`;
149
- }
150
- }
151
- const separator = separatorArrayExplode(style);
152
- const joinedValues = value.map((v) => {
153
- if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
154
- return serializePrimitiveParam({
155
- allowReserved,
156
- name,
157
- value: v
158
- });
159
- }).join(separator);
160
- return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
161
- };
162
- const serializePrimitiveParam = ({ allowReserved, name, value }) => {
163
- if (value === void 0 || value === null) return "";
164
- if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
165
- return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
166
- };
167
- const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
168
- if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
169
- if (style !== "deepObject" && !explode) {
170
- let values = [];
171
- Object.entries(value).forEach(([key, v]) => {
172
- values = [
173
- ...values,
174
- key,
175
- allowReserved ? v : encodeURIComponent(v)
176
- ];
177
- });
178
- const joinedValues = values.join(",");
179
- switch (style) {
180
- case "form": return `${name}=${joinedValues}`;
181
- case "label": return `.${joinedValues}`;
182
- case "matrix": return `;${name}=${joinedValues}`;
183
- default: return joinedValues;
184
- }
185
- }
186
- const separator = separatorObjectExplode(style);
187
- const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
188
- allowReserved,
189
- name: style === "deepObject" ? `${name}[${key}]` : key,
190
- value: v
191
- })).join(separator);
192
- return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
193
- };
194
- //#endregion
195
- //#region ../packages/api-core/src/api/core/utils.gen.ts
196
- const PATH_PARAM_RE = /\{[^{}]+\}/g;
197
- const defaultPathSerializer = ({ path, url: _url }) => {
198
- let url = _url;
199
- const matches = _url.match(PATH_PARAM_RE);
200
- if (matches) for (const match of matches) {
201
- let explode = false;
202
- let name = match.substring(1, match.length - 1);
203
- let style = "simple";
204
- if (name.endsWith("*")) {
205
- explode = true;
206
- name = name.substring(0, name.length - 1);
207
- }
208
- if (name.startsWith(".")) {
209
- name = name.substring(1);
210
- style = "label";
211
- } else if (name.startsWith(";")) {
212
- name = name.substring(1);
213
- style = "matrix";
214
- }
215
- const value = path[name];
216
- if (value === void 0 || value === null) continue;
217
- if (Array.isArray(value)) {
218
- url = url.replace(match, serializeArrayParam({
219
- explode,
220
- name,
221
- style,
222
- value
223
- }));
224
- continue;
225
- }
226
- if (typeof value === "object") {
227
- url = url.replace(match, serializeObjectParam({
228
- explode,
229
- name,
230
- style,
231
- value,
232
- valueOnly: true
233
- }));
234
- continue;
235
- }
236
- if (style === "matrix") {
237
- url = url.replace(match, `;${serializePrimitiveParam({
238
- name,
239
- value
240
- })}`);
241
- continue;
242
- }
243
- const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
244
- url = url.replace(match, replaceValue);
245
- }
246
- return url;
247
- };
248
- const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
249
- const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
250
- let url = (baseUrl ?? "") + pathUrl;
251
- if (path) url = defaultPathSerializer({
252
- path,
253
- url
254
- });
255
- let search = query ? querySerializer(query) : "";
256
- if (search.startsWith("?")) search = search.substring(1);
257
- if (search) url += `?${search}`;
258
- return url;
259
- };
260
- function getValidRequestBody(options) {
261
- const hasBody = options.body !== void 0;
262
- if (hasBody && options.bodySerializer) {
263
- if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
264
- return options.body !== "" ? options.body : null;
265
- }
266
- if (hasBody) return options.body;
267
- }
268
- //#endregion
269
- //#region ../packages/api-core/src/api/core/auth.gen.ts
270
- const getAuthToken = async (auth, callback) => {
271
- const token = typeof callback === "function" ? await callback(auth) : callback;
272
- if (!token) return;
273
- if (auth.scheme === "bearer") return `Bearer ${token}`;
274
- if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
275
- return token;
276
- };
277
- //#endregion
278
- //#region ../packages/api-core/src/api/client/utils.gen.ts
279
- const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
280
- const querySerializer = (queryParams) => {
281
- const search = [];
282
- if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
283
- const value = queryParams[name];
284
- if (value === void 0 || value === null) continue;
285
- const options = parameters[name] || args;
286
- if (Array.isArray(value)) {
287
- const serializedArray = serializeArrayParam({
288
- allowReserved: options.allowReserved,
289
- explode: true,
290
- name,
291
- style: "form",
292
- value,
293
- ...options.array
294
- });
295
- if (serializedArray) search.push(serializedArray);
296
- } else if (typeof value === "object") {
297
- const serializedObject = serializeObjectParam({
298
- allowReserved: options.allowReserved,
299
- explode: true,
300
- name,
301
- style: "deepObject",
302
- value,
303
- ...options.object
304
- });
305
- if (serializedObject) search.push(serializedObject);
306
- } else {
307
- const serializedPrimitive = serializePrimitiveParam({
308
- allowReserved: options.allowReserved,
309
- name,
310
- value
311
- });
312
- if (serializedPrimitive) search.push(serializedPrimitive);
313
- }
314
- }
315
- return search.join("&");
316
- };
317
- return querySerializer;
318
- };
319
- /**
320
- * Infers parseAs value from provided Content-Type header.
321
- */
322
- const getParseAs = (contentType) => {
323
- if (!contentType) return "stream";
324
- const cleanContent = contentType.split(";")[0]?.trim();
325
- if (!cleanContent) return;
326
- if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
327
- if (cleanContent === "multipart/form-data") return "formData";
328
- if ([
329
- "application/",
330
- "audio/",
331
- "image/",
332
- "video/"
333
- ].some((type) => cleanContent.startsWith(type))) return "blob";
334
- if (cleanContent.startsWith("text/")) return "text";
335
- };
336
- const checkForExistence = (options, name) => {
337
- if (!name) return false;
338
- if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
339
- return false;
340
- };
341
- const setAuthParams = async ({ security, ...options }) => {
342
- for (const auth of security) {
343
- if (checkForExistence(options, auth.name)) continue;
344
- const token = await getAuthToken(auth, options.auth);
345
- if (!token) continue;
346
- const name = auth.name ?? "Authorization";
347
- switch (auth.in) {
348
- case "query":
349
- if (!options.query) options.query = {};
350
- options.query[name] = token;
351
- break;
352
- case "cookie":
353
- options.headers.append("Cookie", `${name}=${token}`);
354
- break;
355
- default:
356
- options.headers.set(name, token);
357
- break;
358
- }
359
- }
360
- };
361
- const buildUrl = (options) => getUrl({
362
- baseUrl: options.baseUrl,
363
- path: options.path,
364
- query: options.query,
365
- querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
366
- url: options.url
367
- });
368
- const mergeConfigs = (a, b) => {
369
- const config = {
370
- ...a,
371
- ...b
372
- };
373
- if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
374
- config.headers = mergeHeaders$1(a.headers, b.headers);
375
- return config;
376
- };
377
- const headersEntries = (headers) => {
378
- const entries = [];
379
- headers.forEach((value, key) => {
380
- entries.push([key, value]);
381
- });
382
- return entries;
383
- };
384
- const mergeHeaders$1 = (...headers) => {
385
- const mergedHeaders = new Headers();
386
- for (const header of headers) {
387
- if (!header) continue;
388
- const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
389
- for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
390
- else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
391
- else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
392
- }
393
- return mergedHeaders;
394
- };
395
- var Interceptors = class {
396
- fns = [];
397
- clear() {
398
- this.fns = [];
399
- }
400
- eject(id) {
401
- const index = this.getInterceptorIndex(id);
402
- if (this.fns[index]) this.fns[index] = null;
403
- }
404
- exists(id) {
405
- const index = this.getInterceptorIndex(id);
406
- return Boolean(this.fns[index]);
407
- }
408
- getInterceptorIndex(id) {
409
- if (typeof id === "number") return this.fns[id] ? id : -1;
410
- return this.fns.indexOf(id);
411
- }
412
- update(id, fn) {
413
- const index = this.getInterceptorIndex(id);
414
- if (this.fns[index]) {
415
- this.fns[index] = fn;
416
- return id;
417
- }
418
- return false;
419
- }
420
- use(fn) {
421
- this.fns.push(fn);
422
- return this.fns.length - 1;
423
- }
424
- };
425
- const createInterceptors = () => ({
426
- error: new Interceptors(),
427
- request: new Interceptors(),
428
- response: new Interceptors()
429
- });
430
- const defaultQuerySerializer = createQuerySerializer({
431
- allowReserved: false,
432
- array: {
433
- explode: true,
434
- style: "form"
435
- },
436
- object: {
437
- explode: true,
438
- style: "deepObject"
439
- }
440
- });
441
- const defaultHeaders = { "Content-Type": "application/json" };
442
- const createConfig = (override = {}) => ({
443
- ...jsonBodySerializer,
444
- headers: defaultHeaders,
445
- parseAs: "auto",
446
- querySerializer: defaultQuerySerializer,
447
- ...override
448
- });
449
- //#endregion
450
- //#region ../packages/api-core/src/api/client/client.gen.ts
451
- const createClient = (config = {}) => {
452
- let _config = mergeConfigs(createConfig(), config);
453
- const getConfig = () => ({ ..._config });
454
- const setConfig = (config) => {
455
- _config = mergeConfigs(_config, config);
456
- return getConfig();
457
- };
458
- const interceptors = createInterceptors();
459
- const beforeRequest = async (options) => {
460
- const opts = {
461
- ..._config,
462
- ...options,
463
- fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
464
- headers: mergeHeaders$1(_config.headers, options.headers),
465
- serializedBody: void 0
466
- };
467
- if (opts.security) await setAuthParams({
468
- ...opts,
469
- security: opts.security
470
- });
471
- if (opts.requestValidator) await opts.requestValidator(opts);
472
- if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
473
- if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
474
- const resolvedOpts = opts;
475
- return {
476
- opts: resolvedOpts,
477
- url: buildUrl(resolvedOpts)
478
- };
479
- };
480
- const request = async (options) => {
481
- const { opts, url } = await beforeRequest(options);
482
- const requestInit = {
483
- redirect: "follow",
484
- ...opts,
485
- body: getValidRequestBody(opts)
486
- };
487
- let request = new Request(url, requestInit);
488
- for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
489
- const _fetch = opts.fetch;
490
- let response;
491
- try {
492
- response = await _fetch(request);
493
- } catch (error) {
494
- let finalError = error;
495
- for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, void 0, request, opts);
496
- finalError = finalError || {};
497
- if (opts.throwOnError) throw finalError;
498
- return opts.responseStyle === "data" ? void 0 : {
499
- error: finalError,
500
- request,
501
- response: void 0
502
- };
503
- }
504
- for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
505
- const result = {
506
- request,
507
- response
508
- };
509
- if (response.ok) {
510
- const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
511
- if (response.status === 204 || response.headers.get("Content-Length") === "0") {
512
- let emptyData;
513
- switch (parseAs) {
514
- case "arrayBuffer":
515
- case "blob":
516
- case "text":
517
- emptyData = await response[parseAs]();
518
- break;
519
- case "formData":
520
- emptyData = new FormData();
521
- break;
522
- case "stream":
523
- emptyData = response.body;
524
- break;
525
- default:
526
- emptyData = {};
527
- break;
528
- }
529
- return opts.responseStyle === "data" ? emptyData : {
530
- data: emptyData,
531
- ...result
532
- };
533
- }
534
- let data;
535
- switch (parseAs) {
536
- case "arrayBuffer":
537
- case "blob":
538
- case "formData":
539
- case "text":
540
- data = await response[parseAs]();
541
- break;
542
- case "json": {
543
- const text = await response.text();
544
- data = text ? JSON.parse(text) : {};
545
- break;
546
- }
547
- case "stream": return opts.responseStyle === "data" ? response.body : {
548
- data: response.body,
549
- ...result
550
- };
551
- }
552
- if (parseAs === "json") {
553
- if (opts.responseValidator) await opts.responseValidator(data);
554
- if (opts.responseTransformer) data = await opts.responseTransformer(data);
555
- }
556
- return opts.responseStyle === "data" ? data : {
557
- data,
558
- ...result
559
- };
560
- }
561
- const textError = await response.text();
562
- let jsonError;
563
- try {
564
- jsonError = JSON.parse(textError);
565
- } catch {}
566
- const error = jsonError ?? textError;
567
- let finalError = error;
568
- for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, response, request, opts);
569
- finalError = finalError || {};
570
- if (opts.throwOnError) throw finalError;
571
- return opts.responseStyle === "data" ? void 0 : {
572
- error: finalError,
573
- ...result
574
- };
575
- };
576
- const makeMethodFn = (method) => (options) => request({
577
- ...options,
578
- method
579
- });
580
- const makeSseFn = (method) => async (options) => {
581
- const { opts, url } = await beforeRequest(options);
582
- return createSseClient({
583
- ...opts,
584
- body: opts.body,
585
- headers: opts.headers,
586
- method,
587
- onRequest: async (url, init) => {
588
- let request = new Request(url, init);
589
- for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
590
- return request;
591
- },
592
- serializedBody: getValidRequestBody(opts),
593
- url
594
- });
595
- };
596
- const _buildUrl = (options) => buildUrl({
597
- ..._config,
598
- ...options
599
- });
600
- return {
601
- buildUrl: _buildUrl,
602
- connect: makeMethodFn("CONNECT"),
603
- delete: makeMethodFn("DELETE"),
604
- get: makeMethodFn("GET"),
605
- getConfig,
606
- head: makeMethodFn("HEAD"),
607
- interceptors,
608
- options: makeMethodFn("OPTIONS"),
609
- patch: makeMethodFn("PATCH"),
610
- post: makeMethodFn("POST"),
611
- put: makeMethodFn("PUT"),
612
- request,
613
- setConfig,
614
- sse: {
615
- connect: makeSseFn("CONNECT"),
616
- delete: makeSseFn("DELETE"),
617
- get: makeSseFn("GET"),
618
- head: makeSseFn("HEAD"),
619
- options: makeSseFn("OPTIONS"),
620
- patch: makeSseFn("PATCH"),
621
- post: makeSseFn("POST"),
622
- put: makeSseFn("PUT"),
623
- trace: makeSseFn("TRACE")
624
- },
625
- trace: makeMethodFn("TRACE")
626
- };
627
- };
628
- //#endregion
629
22
  //#region ../packages/api-core/src/api/client.gen.ts
630
23
  const client = createClient(createConfig({ baseUrl: "https://www.primitive.dev/api/v1" }));
631
24
  //#endregion
@@ -648,6 +41,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
648
41
  downloadDomainZoneFile: () => downloadDomainZoneFile,
649
42
  downloadRawEmail: () => downloadRawEmail,
650
43
  getAccount: () => getAccount,
44
+ getConversation: () => getConversation,
651
45
  getEmail: () => getEmail,
652
46
  getFunction: () => getFunction,
653
47
  getFunctionTestRunTrace: () => getFunctionTestRunTrace,
@@ -1267,6 +661,37 @@ const discardEmailContent = (options) => (options.client ?? client).post({
1267
661
  ...options
1268
662
  });
1269
663
  /**
664
+ * Get the conversation an email belongs to
665
+ *
666
+ * Returns the full conversation the given inbound email belongs
667
+ * to, as ordered, ready-to-prompt turns WITH bodies. It resolves
668
+ * the thread from the email and returns every message oldest-first,
669
+ * so an agent that received an email can pass `messages` straight
670
+ * to a chat model in one call instead of walking `/threads/{id}`
671
+ * plus `/emails/{id}` and `/sent-emails/{id}` per message.
672
+ *
673
+ * Each message carries a `direction` (`inbound` | `outbound`) and a
674
+ * derived `role`: `inbound` -> `user`, `outbound` -> `assistant`
675
+ * (your own prior replies). The role mapping assumes the caller
676
+ * owns the outbound side, which is the agent-reply case this exists
677
+ * for. If the email has no thread yet (a brand-new message), the
678
+ * conversation is just that one message as a single user turn.
679
+ *
680
+ * The message list is capped; check `truncated` to detect when
681
+ * older messages were omitted. Consecutive same-role turns are not
682
+ * merged here; that normalization is model-specific and left to the
683
+ * caller.
684
+ *
685
+ */
686
+ const getConversation = (options) => (options.client ?? client).get({
687
+ security: [{
688
+ scheme: "bearer",
689
+ type: "http"
690
+ }],
691
+ url: "/emails/{id}/conversation",
692
+ ...options
693
+ });
694
+ /**
1270
695
  * List webhook endpoints
1271
696
  *
1272
697
  * Returns all active (non-deleted) webhook endpoints.
@@ -3026,6 +2451,27 @@ const openapiDocument = {
3026
2451
  }
3027
2452
  }
3028
2453
  },
2454
+ "/emails/{id}/conversation": {
2455
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
2456
+ "get": {
2457
+ "operationId": "getConversation",
2458
+ "summary": "Get the conversation an email belongs to",
2459
+ "description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
2460
+ "tags": ["Emails"],
2461
+ "responses": {
2462
+ "200": {
2463
+ "description": "Conversation",
2464
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2465
+ "type": "object",
2466
+ "properties": { "data": { "$ref": "#/components/schemas/Conversation" } }
2467
+ }] } } }
2468
+ },
2469
+ "400": { "$ref": "#/components/responses/ValidationError" },
2470
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2471
+ "404": { "$ref": "#/components/responses/NotFound" }
2472
+ }
2473
+ }
2474
+ },
3029
2475
  "/endpoints": {
3030
2476
  "get": {
3031
2477
  "operationId": "listEndpoints",
@@ -5864,6 +5310,78 @@ const openapiDocument = {
5864
5310
  },
5865
5311
  "required": ["direction", "id"]
5866
5312
  },
5313
+ "Conversation": {
5314
+ "type": "object",
5315
+ "description": "The full conversation an inbound email belongs to, as ordered,\nready-to-prompt turns with bodies. Resolves the thread from the\nemail and returns every message oldest-first, so an agent that\nreceived an email can pass `messages` straight to a chat model in\none call.\n",
5316
+ "properties": {
5317
+ "thread_id": {
5318
+ "type": ["string", "null"],
5319
+ "format": "uuid",
5320
+ "description": "The thread this email belongs to, or null when the email\nisn't threaded yet (the conversation is then just this one\nmessage).\n"
5321
+ },
5322
+ "subject": {
5323
+ "type": ["string", "null"],
5324
+ "description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
5325
+ },
5326
+ "message_count": {
5327
+ "type": "integer",
5328
+ "description": "Total messages in the thread. `messages` is capped, so\n`truncated` is true (and this can exceed `messages.length`)\nwhen older messages were omitted.\n"
5329
+ },
5330
+ "truncated": {
5331
+ "type": "boolean",
5332
+ "description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
5333
+ },
5334
+ "messages": {
5335
+ "type": "array",
5336
+ "items": { "$ref": "#/components/schemas/ConversationMessage" }
5337
+ }
5338
+ },
5339
+ "required": [
5340
+ "thread_id",
5341
+ "message_count",
5342
+ "truncated",
5343
+ "messages"
5344
+ ]
5345
+ },
5346
+ "ConversationMessage": {
5347
+ "type": "object",
5348
+ "description": "One message in the conversation, with its body and a chat role.",
5349
+ "properties": {
5350
+ "role": {
5351
+ "type": "string",
5352
+ "enum": ["user", "assistant"],
5353
+ "description": "Chat role derived from `direction`: `user` for inbound\n(received) messages, `assistant` for outbound (your own prior\nreplies). Lets `messages` be passed directly to a chat model.\n"
5354
+ },
5355
+ "direction": {
5356
+ "type": "string",
5357
+ "enum": ["inbound", "outbound"],
5358
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
5359
+ },
5360
+ "id": {
5361
+ "type": "string",
5362
+ "format": "uuid"
5363
+ },
5364
+ "message_id": { "type": ["string", "null"] },
5365
+ "from": { "type": ["string", "null"] },
5366
+ "to": { "type": ["string", "null"] },
5367
+ "subject": { "type": ["string", "null"] },
5368
+ "text": {
5369
+ "type": "string",
5370
+ "description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
5371
+ },
5372
+ "timestamp": {
5373
+ "type": ["string", "null"],
5374
+ "format": "date-time",
5375
+ "description": "received_at for inbound, created_at for outbound."
5376
+ }
5377
+ },
5378
+ "required": [
5379
+ "role",
5380
+ "direction",
5381
+ "id",
5382
+ "text"
5383
+ ]
5384
+ },
5867
5385
  "SendMailAttachment": {
5868
5386
  "type": "object",
5869
5387
  "additionalProperties": false,
@@ -6769,16 +6287,21 @@ const openapiDocument = {
6769
6287
  "type": "string",
6770
6288
  "minLength": 1,
6771
6289
  "maxLength": 1048576,
6772
- "description": "Bundled handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject.\n"
6290
+ "description": "Pre-built handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject. Provide either `code` or `files`, not both.\n"
6773
6291
  },
6774
6292
  "sourceMap": {
6775
6293
  "type": "string",
6776
6294
  "minLength": 1,
6777
6295
  "maxLength": 5242880,
6778
- "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
6296
+ "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs. Only\nvalid with `code`.\n"
6297
+ },
6298
+ "files": {
6299
+ "type": "object",
6300
+ "additionalProperties": { "type": "string" },
6301
+ "description": "Source files for a managed build, as a map of path to file\ncontents (for example {\"package.json\": \"...\",\n\"src/index.ts\": \"...\"}). Provide this INSTEAD of `code` to\nhave the server install dependencies and bundle the source\nfor the Workers runtime before deploying. Include a\npackage.json (its `dependencies` are installed). Provide\neither `code` or `files`, not both.\n"
6779
6302
  }
6780
6303
  },
6781
- "required": ["name", "code"]
6304
+ "required": ["name"]
6782
6305
  },
6783
6306
  "CreateFunctionResult": {
6784
6307
  "type": "object",
@@ -6805,15 +6328,20 @@ const openapiDocument = {
6805
6328
  "type": "string",
6806
6329
  "minLength": 1,
6807
6330
  "maxLength": 1048576,
6808
- "description": "New bundled handler. Same rules as CreateFunctionInput.code."
6331
+ "description": "New pre-built handler. Same rules as CreateFunctionInput.code. Provide either `code` or `files`, not both."
6809
6332
  },
6810
6333
  "sourceMap": {
6811
6334
  "type": "string",
6812
6335
  "minLength": 1,
6813
6336
  "maxLength": 5242880
6337
+ },
6338
+ "files": {
6339
+ "type": "object",
6340
+ "additionalProperties": { "type": "string" },
6341
+ "description": "Source files for a managed build, as a map of path to file\ncontents. Provide this INSTEAD of `code` to rebuild and\nredeploy from source. Same rules as CreateFunctionInput.files.\n"
6814
6342
  }
6815
6343
  },
6816
- "required": ["code"]
6344
+ "required": []
6817
6345
  },
6818
6346
  "TestInvocationResult": {
6819
6347
  "type": "object",
@@ -9013,12 +8541,12 @@ const operationManifest = [
9013
8541
  {
9014
8542
  "binaryResponse": false,
9015
8543
  "bodyRequired": false,
9016
- "command": "get-email",
9017
- "description": "Returns the full record for an inbound email received at one\nof your verified domains, including the parsed text and HTML\nbodies, threading metadata, SMTP envelope detail, webhook\ndelivery state, and a `replies` array for any outbound sends\nrecorded as replies to this inbound.\n\nFor listing inbound emails (with cursor pagination, status\nand date filters, and free-text search), use\n`/emails`. Outbound (sent) email records are NOT returned\nhere; use `/sent-emails/{id}` for those.\n\nThe response carries four sender-shaped fields whose\nmeanings overlap. `from_email` is the canonical \"who sent\nthis\" field for most use cases (parsed bare address from\nthe `From:` header, with a `sender` fallback). `from_header`\nis the raw header including any display name. `sender` and\n`smtp_mail_from` both carry the SMTP envelope MAIL FROM\n(return-path) and are equal by construction; `sender` is\nthe older field name retained for compatibility. See\n`primitive describe emails:get-email | jq '.responseSchema.properties'`\nfor per-field detail.\n",
8544
+ "command": "get-conversation",
8545
+ "description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
9018
8546
  "hasJsonBody": false,
9019
8547
  "method": "GET",
9020
- "operationId": "getEmail",
9021
- "path": "/emails/{id}",
8548
+ "operationId": "getConversation",
8549
+ "path": "/emails/{id}/conversation",
9022
8550
  "pathParams": [{
9023
8551
  "description": "Resource UUID",
9024
8552
  "enum": null,
@@ -9030,41 +8558,135 @@ const operationManifest = [
9030
8558
  "requestSchema": null,
9031
8559
  "responseSchema": {
9032
8560
  "type": "object",
8561
+ "description": "The full conversation an inbound email belongs to, as ordered,\nready-to-prompt turns with bodies. Resolves the thread from the\nemail and returns every message oldest-first, so an agent that\nreceived an email can pass `messages` straight to a chat model in\none call.\n",
9033
8562
  "properties": {
9034
- "id": {
9035
- "type": "string",
9036
- "format": "uuid"
9037
- },
9038
- "message_id": { "type": ["string", "null"] },
9039
- "domain_id": {
8563
+ "thread_id": {
9040
8564
  "type": ["string", "null"],
9041
- "format": "uuid"
8565
+ "format": "uuid",
8566
+ "description": "The thread this email belongs to, or null when the email\nisn't threaded yet (the conversation is then just this one\nmessage).\n"
9042
8567
  },
9043
- "org_id": {
8568
+ "subject": {
9044
8569
  "type": ["string", "null"],
9045
- "format": "uuid"
9046
- },
9047
- "sender": {
9048
- "type": "string",
9049
- "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"
8570
+ "description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
9050
8571
  },
9051
- "recipient": { "type": "string" },
9052
- "subject": { "type": ["string", "null"] },
9053
- "body_text": {
9054
- "type": ["string", "null"],
9055
- "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."
8572
+ "message_count": {
8573
+ "type": "integer",
8574
+ "description": "Total messages in the thread. `messages` is capped, so\n`truncated` is true (and this can exceed `messages.length`)\nwhen older messages were omitted.\n"
9056
8575
  },
9057
- "body_html": {
9058
- "type": ["string", "null"],
9059
- "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."
8576
+ "truncated": {
8577
+ "type": "boolean",
8578
+ "description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
9060
8579
  },
9061
- "status": {
9062
- "type": "string",
9063
- "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",
9064
- "enum": [
9065
- "pending",
9066
- "accepted",
9067
- "completed",
8580
+ "messages": {
8581
+ "type": "array",
8582
+ "items": {
8583
+ "type": "object",
8584
+ "description": "One message in the conversation, with its body and a chat role.",
8585
+ "properties": {
8586
+ "role": {
8587
+ "type": "string",
8588
+ "enum": ["user", "assistant"],
8589
+ "description": "Chat role derived from `direction`: `user` for inbound\n(received) messages, `assistant` for outbound (your own prior\nreplies). Lets `messages` be passed directly to a chat model.\n"
8590
+ },
8591
+ "direction": {
8592
+ "type": "string",
8593
+ "enum": ["inbound", "outbound"],
8594
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
8595
+ },
8596
+ "id": {
8597
+ "type": "string",
8598
+ "format": "uuid"
8599
+ },
8600
+ "message_id": { "type": ["string", "null"] },
8601
+ "from": { "type": ["string", "null"] },
8602
+ "to": { "type": ["string", "null"] },
8603
+ "subject": { "type": ["string", "null"] },
8604
+ "text": {
8605
+ "type": "string",
8606
+ "description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
8607
+ },
8608
+ "timestamp": {
8609
+ "type": ["string", "null"],
8610
+ "format": "date-time",
8611
+ "description": "received_at for inbound, created_at for outbound."
8612
+ }
8613
+ },
8614
+ "required": [
8615
+ "role",
8616
+ "direction",
8617
+ "id",
8618
+ "text"
8619
+ ]
8620
+ }
8621
+ }
8622
+ },
8623
+ "required": [
8624
+ "thread_id",
8625
+ "message_count",
8626
+ "truncated",
8627
+ "messages"
8628
+ ]
8629
+ },
8630
+ "sdkName": "getConversation",
8631
+ "summary": "Get the conversation an email belongs to",
8632
+ "tag": "Emails",
8633
+ "tagCommand": "emails"
8634
+ },
8635
+ {
8636
+ "binaryResponse": false,
8637
+ "bodyRequired": false,
8638
+ "command": "get-email",
8639
+ "description": "Returns the full record for an inbound email received at one\nof your verified domains, including the parsed text and HTML\nbodies, threading metadata, SMTP envelope detail, webhook\ndelivery state, and a `replies` array for any outbound sends\nrecorded as replies to this inbound.\n\nFor listing inbound emails (with cursor pagination, status\nand date filters, and free-text search), use\n`/emails`. Outbound (sent) email records are NOT returned\nhere; use `/sent-emails/{id}` for those.\n\nThe response carries four sender-shaped fields whose\nmeanings overlap. `from_email` is the canonical \"who sent\nthis\" field for most use cases (parsed bare address from\nthe `From:` header, with a `sender` fallback). `from_header`\nis the raw header including any display name. `sender` and\n`smtp_mail_from` both carry the SMTP envelope MAIL FROM\n(return-path) and are equal by construction; `sender` is\nthe older field name retained for compatibility. See\n`primitive describe emails:get-email | jq '.responseSchema.properties'`\nfor per-field detail.\n",
8640
+ "hasJsonBody": false,
8641
+ "method": "GET",
8642
+ "operationId": "getEmail",
8643
+ "path": "/emails/{id}",
8644
+ "pathParams": [{
8645
+ "description": "Resource UUID",
8646
+ "enum": null,
8647
+ "name": "id",
8648
+ "required": true,
8649
+ "type": "string"
8650
+ }],
8651
+ "queryParams": [],
8652
+ "requestSchema": null,
8653
+ "responseSchema": {
8654
+ "type": "object",
8655
+ "properties": {
8656
+ "id": {
8657
+ "type": "string",
8658
+ "format": "uuid"
8659
+ },
8660
+ "message_id": { "type": ["string", "null"] },
8661
+ "domain_id": {
8662
+ "type": ["string", "null"],
8663
+ "format": "uuid"
8664
+ },
8665
+ "org_id": {
8666
+ "type": ["string", "null"],
8667
+ "format": "uuid"
8668
+ },
8669
+ "sender": {
8670
+ "type": "string",
8671
+ "description": "SMTP envelope sender (return-path) the inbound mail server\naccepted. Same value as `smtp_mail_from`; both fields exist\nso protocol-aware tooling can use whichever name it expects.\n\nFor most legitimate mail this equals `from_email`; for\nmailing lists, bounce handlers, and forwarders it is\ntypically the bounce-handling address rather than the\nhuman-visible sender.\n\n**For the canonical \"who sent this email\" value, use\n`from_email`.**\n"
8672
+ },
8673
+ "recipient": { "type": "string" },
8674
+ "subject": { "type": ["string", "null"] },
8675
+ "body_text": {
8676
+ "type": ["string", "null"],
8677
+ "description": "Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed."
8678
+ },
8679
+ "body_html": {
8680
+ "type": ["string", "null"],
8681
+ "description": "HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed."
8682
+ },
8683
+ "status": {
8684
+ "type": "string",
8685
+ "description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
8686
+ "enum": [
8687
+ "pending",
8688
+ "accepted",
8689
+ "completed",
9068
8690
  "rejected"
9069
8691
  ]
9070
8692
  },
@@ -10512,16 +10134,21 @@ const operationManifest = [
10512
10134
  "type": "string",
10513
10135
  "minLength": 1,
10514
10136
  "maxLength": 1048576,
10515
- "description": "Bundled handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject.\n"
10137
+ "description": "Pre-built handler as a single ESM module. Up to 1 MiB UTF-8.\nMust export a default `{ async fetch(req, env, ctx) { ... } }`\nobject. Provide either `code` or `files`, not both.\n"
10516
10138
  },
10517
10139
  "sourceMap": {
10518
10140
  "type": "string",
10519
10141
  "minLength": 1,
10520
10142
  "maxLength": 5242880,
10521
- "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
10143
+ "description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs. Only\nvalid with `code`.\n"
10144
+ },
10145
+ "files": {
10146
+ "type": "object",
10147
+ "additionalProperties": { "type": "string" },
10148
+ "description": "Source files for a managed build, as a map of path to file\ncontents (for example {\"package.json\": \"...\",\n\"src/index.ts\": \"...\"}). Provide this INSTEAD of `code` to\nhave the server install dependencies and bundle the source\nfor the Workers runtime before deploying. Include a\npackage.json (its `dependencies` are installed). Provide\neither `code` or `files`, not both.\n"
10522
10149
  }
10523
10150
  },
10524
- "required": ["name", "code"]
10151
+ "required": ["name"]
10525
10152
  },
10526
10153
  "responseSchema": {
10527
10154
  "type": "object",
@@ -11574,15 +11201,20 @@ const operationManifest = [
11574
11201
  "type": "string",
11575
11202
  "minLength": 1,
11576
11203
  "maxLength": 1048576,
11577
- "description": "New bundled handler. Same rules as CreateFunctionInput.code."
11204
+ "description": "New pre-built handler. Same rules as CreateFunctionInput.code. Provide either `code` or `files`, not both."
11578
11205
  },
11579
11206
  "sourceMap": {
11580
11207
  "type": "string",
11581
11208
  "minLength": 1,
11582
11209
  "maxLength": 5242880
11210
+ },
11211
+ "files": {
11212
+ "type": "object",
11213
+ "additionalProperties": { "type": "string" },
11214
+ "description": "Source files for a managed build, as a map of path to file\ncontents. Provide this INSTEAD of `code` to rebuild and\nredeploy from source. Same rules as CreateFunctionInput.files.\n"
11583
11215
  }
11584
11216
  },
11585
- "required": ["code"]
11217
+ "required": []
11586
11218
  },
11587
11219
  "responseSchema": {
11588
11220
  "type": "object",
@@ -13073,532 +12705,6 @@ const operationManifest = [
13073
12705
  }
13074
12706
  ];
13075
12707
  //#endregion
13076
- //#region ../packages/api-core/src/client.ts
13077
- /**
13078
- * Host-aware Primitive API client and shared error type.
13079
- *
13080
- * Lives in api-core (instead of sdk-node) so the CLI can build a
13081
- * configured request client without taking a dependency on sdk-node.
13082
- * The higher-level `PrimitiveClient` (with `.send`, `.reply`,
13083
- * `.forward`) still lives in sdk-node because it needs the
13084
- * `ReceivedEmail` type from the webhook parsing surface.
13085
- */
13086
- const DEFAULT_API_BASE_URL_1 = "https://www.primitive.dev/api/v1";
13087
- const DEFAULT_API_BASE_URL_2 = "https://api.primitive.dev/v1";
13088
- function createDefaultAuth(apiKey) {
13089
- return (security) => {
13090
- if (security.type === "http" && security.scheme === "bearer") return apiKey;
13091
- };
13092
- }
13093
- var PrimitiveApiClient = class {
13094
- /**
13095
- * Generated client targeting the primary API host (apiBaseUrl1). Use
13096
- * this when passing `client: ...` to a generated operation function
13097
- * for every endpoint EXCEPT /send-mail. The hand-written
13098
- * PrimitiveClient.send / .reply / .forward methods on the subclass
13099
- * route /send-mail to the host-2 client internally.
13100
- */
13101
- client;
13102
- /**
13103
- * @internal Generated client targeting the attachments-supporting
13104
- * send host (apiBaseUrl2). Used by PrimitiveClient.send() under the
13105
- * hood. Exposed for the CLI's hand-rolled send command, which calls
13106
- * the generated sendEmail directly; not part of the publicly-
13107
- * documented SDK surface. Customer code should call .send() on the
13108
- * subclass instead.
13109
- */
13110
- _sendClient;
13111
- constructor(options = {}) {
13112
- const { apiKey, auth, apiBaseUrl1 = DEFAULT_API_BASE_URL_1, apiBaseUrl2 = DEFAULT_API_BASE_URL_2, ...config } = options;
13113
- const resolvedAuth = auth ?? createDefaultAuth(apiKey);
13114
- this.client = createClient(createConfig({
13115
- ...config,
13116
- auth: resolvedAuth,
13117
- baseUrl: apiBaseUrl1
13118
- }));
13119
- this._sendClient = createClient(createConfig({
13120
- ...config,
13121
- auth: resolvedAuth,
13122
- baseUrl: apiBaseUrl2
13123
- }));
13124
- }
13125
- getConfig() {
13126
- return this.client.getConfig();
13127
- }
13128
- setConfig(config) {
13129
- return this.client.setConfig(config);
13130
- }
13131
- };
13132
- //#endregion
13133
- //#region src/oclif/auth.ts
13134
- const CREDENTIALS_FILE = "credentials.json";
13135
- const CREDENTIALS_LOCK_DIR = "credentials.lock";
13136
- const CREDENTIALS_LOCK_OWNER_FILE = "owner.json";
13137
- const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
13138
- const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive signin`.";
13139
- const CREDENTIALS_LOCK_CLEANUP_SIGNALS = [
13140
- "SIGINT",
13141
- "SIGTERM",
13142
- "SIGHUP"
13143
- ];
13144
- function isRecord$2(value) {
13145
- return value !== null && typeof value === "object" && !Array.isArray(value);
13146
- }
13147
- function requireString(value, key) {
13148
- const raw = value[key];
13149
- 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}`);
13150
- return raw;
13151
- }
13152
- /**
13153
- * Sentinel returned by parseCredentials when the on-disk credentials were
13154
- * written by an API-key-based CLI. The caller treats this as "not logged in"
13155
- * after clearing the local file. The backing API key is intentionally not
13156
- * revoked; API keys still work when passed explicitly via --api-key/env.
13157
- */
13158
- var LegacyApiKeyCredentialFormatError = class extends Error {
13159
- constructor() {
13160
- super("legacy_api_key_credential_format");
13161
- this.name = "LegacyApiKeyCredentialFormatError";
13162
- }
13163
- };
13164
- function parseCredentials(raw) {
13165
- if (!isRecord$2(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
13166
- if (raw.auth_method !== "oauth") {
13167
- if (typeof raw.api_key === "string" || typeof raw.key_id === "string" || typeof raw.base_url === "string") throw new LegacyApiKeyCredentialFormatError();
13168
- throw new Error(`Stored Primitive CLI credentials are malformed: auth_method must be oauth. ${MALFORMED_CREDENTIALS_HINT}`);
13169
- }
13170
- const orgName = raw.org_name;
13171
- 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}`);
13172
- if (requireString(raw, "token_type") !== "Bearer") throw new Error(`Stored Primitive CLI credentials are malformed: token_type must be Bearer. ${MALFORMED_CREDENTIALS_HINT}`);
13173
- return {
13174
- auth_method: "oauth",
13175
- access_token: requireString(raw, "access_token"),
13176
- refresh_token: requireString(raw, "refresh_token"),
13177
- token_type: "Bearer",
13178
- expires_at: requireString(raw, "expires_at"),
13179
- oauth_grant_id: requireString(raw, "oauth_grant_id"),
13180
- oauth_client_id: requireString(raw, "oauth_client_id"),
13181
- org_id: requireString(raw, "org_id"),
13182
- org_name: orgName,
13183
- api_base_url_1: requireString(raw, "api_base_url_1"),
13184
- created_at: requireString(raw, "created_at")
13185
- };
13186
- }
13187
- function credentialsPath(configDir) {
13188
- return join(configDir, CREDENTIALS_FILE);
13189
- }
13190
- function credentialsLockPath(configDir) {
13191
- return join(configDir, CREDENTIALS_LOCK_DIR);
13192
- }
13193
- function normalize(url, fallback) {
13194
- const trimmed = url?.trim();
13195
- if (!trimmed) return fallback;
13196
- return trimmed.replace(/\/+$/, "");
13197
- }
13198
- function normalizeApiBaseUrl1(url) {
13199
- return normalize(url, DEFAULT_API_BASE_URL_1);
13200
- }
13201
- function normalizeApiBaseUrl2(url) {
13202
- return normalize(url, DEFAULT_API_BASE_URL_2);
13203
- }
13204
- function cliAccessTokenExpiresAt(expiresInSeconds, now = Date.now) {
13205
- return new Date(now() + expiresInSeconds * 1e3).toISOString();
13206
- }
13207
- function loadCliCredentials(configDir) {
13208
- const path = credentialsPath(configDir);
13209
- let contents;
13210
- try {
13211
- contents = readFileSync(path, "utf8");
13212
- } catch (error) {
13213
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
13214
- const detail = error instanceof Error ? error.message : String(error);
13215
- throw new Error(`Could not read Primitive CLI credentials: ${detail}`);
13216
- }
13217
- try {
13218
- return parseCredentials(JSON.parse(contents));
13219
- } catch (error) {
13220
- if (error instanceof LegacyApiKeyCredentialFormatError) {
13221
- try {
13222
- rmSync(path, { force: true });
13223
- } catch {}
13224
- 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");
13225
- return null;
13226
- }
13227
- if (error instanceof SyntaxError) throw new Error("Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive signin`.");
13228
- throw error;
13229
- }
13230
- }
13231
- function saveCliCredentials(configDir, credentials) {
13232
- mkdirSync(configDir, {
13233
- mode: 448,
13234
- recursive: true
13235
- });
13236
- const path = credentialsPath(configDir);
13237
- const tempPath = join(configDir, `${CREDENTIALS_FILE}.${process.pid}.${randomUUID()}.tmp`);
13238
- try {
13239
- writeFileSync(tempPath, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
13240
- chmodSync(tempPath, 384);
13241
- renameSync(tempPath, path);
13242
- chmodSync(path, 384);
13243
- } catch (error) {
13244
- rmSync(tempPath, { force: true });
13245
- throw error;
13246
- }
13247
- }
13248
- function deleteCliCredentials(configDir) {
13249
- rmSync(credentialsPath(configDir), { force: true });
13250
- }
13251
- function deleteCliCredentialsLock(configDir) {
13252
- rmSync(credentialsLockPath(configDir), {
13253
- force: true,
13254
- recursive: true
13255
- });
13256
- }
13257
- function errorCode(error) {
13258
- return error && typeof error === "object" ? error.code : void 0;
13259
- }
13260
- function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
13261
- try {
13262
- const stats = statSync(lockPath);
13263
- if (now() - stats.mtimeMs < staleMs) return false;
13264
- } catch (error) {
13265
- if (errorCode(error) === "ENOENT") return true;
13266
- throw error;
13267
- }
13268
- rmSync(lockPath, {
13269
- force: true,
13270
- recursive: true
13271
- });
13272
- return true;
13273
- }
13274
- function readCliCredentialsLockOwner(lockPath) {
13275
- let raw;
13276
- try {
13277
- raw = readFileSync(join(lockPath, CREDENTIALS_LOCK_OWNER_FILE), "utf8");
13278
- } catch (error) {
13279
- if (errorCode(error) === "ENOENT") return null;
13280
- throw error;
13281
- }
13282
- try {
13283
- const pid = JSON.parse(raw)?.pid;
13284
- return Number.isInteger(pid) && pid > 0 ? { pid } : null;
13285
- } catch {
13286
- return null;
13287
- }
13288
- }
13289
- function processIsRunning(pid) {
13290
- try {
13291
- process.kill(pid, 0);
13292
- return true;
13293
- } catch (error) {
13294
- if (errorCode(error) === "ESRCH") return false;
13295
- return true;
13296
- }
13297
- }
13298
- function removeRecoverableCliCredentialsLock(params) {
13299
- const owner = readCliCredentialsLockOwner(params.lockPath);
13300
- if (owner && params.isRunning(owner.pid)) return false;
13301
- if (owner) {
13302
- rmSync(params.lockPath, {
13303
- force: true,
13304
- recursive: true
13305
- });
13306
- return true;
13307
- }
13308
- return removeStaleCliCredentialsLock(params.lockPath, params.staleMs, params.now);
13309
- }
13310
- function writeCliCredentialsLockOwner(lockPath) {
13311
- const ownerPath = join(lockPath, CREDENTIALS_LOCK_OWNER_FILE);
13312
- writeFileSync(ownerPath, `${JSON.stringify({
13313
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
13314
- pid: process.pid
13315
- })}\n`, { mode: 384 });
13316
- chmodSync(ownerPath, 384);
13317
- }
13318
- function installCredentialsLockSignalCleanup(lockPath) {
13319
- let active = true;
13320
- const listeners = CREDENTIALS_LOCK_CLEANUP_SIGNALS.map((signal) => {
13321
- const listener = () => {
13322
- if (!active) return;
13323
- active = false;
13324
- rmSync(lockPath, {
13325
- force: true,
13326
- recursive: true
13327
- });
13328
- process.exit(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129);
13329
- };
13330
- process.once(signal, listener);
13331
- return {
13332
- listener,
13333
- signal
13334
- };
13335
- });
13336
- return () => {
13337
- if (!active) return;
13338
- active = false;
13339
- for (const { listener, signal } of listeners) process.removeListener(signal, listener);
13340
- };
13341
- }
13342
- function credentialsLockInProgressMessage(lockPath) {
13343
- 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}.`;
13344
- }
13345
- function acquireCliCredentialsLock(configDir, options = {}) {
13346
- mkdirSync(configDir, {
13347
- mode: 448,
13348
- recursive: true
13349
- });
13350
- const lockPath = credentialsLockPath(configDir);
13351
- const installSignalHandlers = options.installSignalHandlers ?? true;
13352
- const isRunning = options.isProcessRunning ?? processIsRunning;
13353
- const now = options.now ?? Date.now;
13354
- const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
13355
- let acquired = false;
13356
- for (let attempt = 0; attempt < 2; attempt += 1) try {
13357
- mkdirSync(lockPath, { mode: 448 });
13358
- acquired = true;
13359
- break;
13360
- } catch (error) {
13361
- if (errorCode(error) !== "EEXIST") throw error;
13362
- if (removeRecoverableCliCredentialsLock({
13363
- isRunning,
13364
- lockPath,
13365
- now,
13366
- staleMs
13367
- })) continue;
13368
- throw new Error(credentialsLockInProgressMessage(lockPath));
13369
- }
13370
- if (!acquired) throw new Error(credentialsLockInProgressMessage(lockPath));
13371
- try {
13372
- writeCliCredentialsLockOwner(lockPath);
13373
- } catch (error) {
13374
- rmSync(lockPath, {
13375
- force: true,
13376
- recursive: true
13377
- });
13378
- throw error;
13379
- }
13380
- const removeSignalCleanup = installSignalHandlers ? installCredentialsLockSignalCleanup(lockPath) : () => void 0;
13381
- let released = false;
13382
- return () => {
13383
- if (released) return;
13384
- released = true;
13385
- removeSignalCleanup();
13386
- rmSync(lockPath, {
13387
- force: true,
13388
- recursive: true
13389
- });
13390
- };
13391
- }
13392
- function resolveCliAuth(params) {
13393
- const apiKey = params.apiKey?.trim();
13394
- const apiBaseUrl2 = normalizeApiBaseUrl2(params.apiBaseUrl2);
13395
- if (apiKey) return {
13396
- apiKey,
13397
- apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
13398
- apiBaseUrl2,
13399
- credentials: null,
13400
- source: "flag-or-env"
13401
- };
13402
- const credentials = loadCliCredentials(params.configDir);
13403
- if (credentials) return {
13404
- apiKey: credentials.access_token,
13405
- apiBaseUrl1: credentials.api_base_url_1,
13406
- apiBaseUrl2,
13407
- credentials,
13408
- source: "stored"
13409
- };
13410
- return {
13411
- apiKey: void 0,
13412
- apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
13413
- apiBaseUrl2,
13414
- credentials: null,
13415
- source: "none"
13416
- };
13417
- }
13418
- //#endregion
13419
- //#region src/oclif/cli-config.ts
13420
- const CONFIG_FILE = "config.json";
13421
- const CONFIG_VERSION = 1;
13422
- const DEFAULT_ENVIRONMENT = "default";
13423
- function cliConfigPath(configDir) {
13424
- return join(configDir, CONFIG_FILE);
13425
- }
13426
- function cliConfigError(message) {
13427
- return new Errors.CLIError(`${message} Run \`primitive config reset\` to clear the local CLI config.`, { exit: 1 });
13428
- }
13429
- function isRecord$1(value) {
13430
- return value !== null && typeof value === "object" && !Array.isArray(value);
13431
- }
13432
- function normalizeCliEnvironmentName(name) {
13433
- const trimmed = name?.trim();
13434
- if (!trimmed) throw new Errors.CLIError("Environment name must be a non-empty string.", { exit: 1 });
13435
- 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 });
13436
- return trimmed;
13437
- }
13438
- function validateCliHeaderName(name) {
13439
- const trimmed = name.trim();
13440
- if (!trimmed) throw new Errors.CLIError("Header name must be a non-empty string.", { exit: 1 });
13441
- if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(trimmed)) throw new Errors.CLIError(`Invalid header name: ${name}`, { exit: 1 });
13442
- if (trimmed.toLowerCase() === "authorization") throw new Errors.CLIError("The Authorization header is managed by PRIMITIVE_API_KEY or saved OAuth CLI credentials.", { exit: 1 });
13443
- return trimmed;
13444
- }
13445
- function validateCliHeaderValue(value, name) {
13446
- if (value.length === 0) throw new Errors.CLIError(`Header ${name} value must not be empty.`, { exit: 1 });
13447
- if (/[\r\n\0]/.test(value)) throw new Errors.CLIError(`Header ${name} value must not contain CR, LF, or NUL characters.`, { exit: 1 });
13448
- return value;
13449
- }
13450
- function parseHeaderAssignment(assignment) {
13451
- const separator = assignment.indexOf("=");
13452
- if (separator <= 0) throw new Errors.CLIError("Header values must use name=value syntax, for example `x-custom=secret`.", { exit: 1 });
13453
- const name = validateCliHeaderName(assignment.slice(0, separator));
13454
- return [name, validateCliHeaderValue(assignment.slice(separator + 1), name)];
13455
- }
13456
- function parseHeaders(raw, context) {
13457
- if (raw === void 0) return {};
13458
- if (!isRecord$1(raw)) throw cliConfigError(`${context} headers must be a JSON object.`);
13459
- const headers = {};
13460
- for (const [rawName, rawValue] of Object.entries(raw)) {
13461
- const name = validateCliHeaderName(rawName);
13462
- if (typeof rawValue !== "string") throw cliConfigError(`${context} header ${name} must be a string.`);
13463
- headers[name] = validateCliHeaderValue(rawValue, name);
13464
- }
13465
- return headers;
13466
- }
13467
- function parseEnvironmentConfig(raw, context) {
13468
- if (!isRecord$1(raw)) throw cliConfigError(`${context} must be a JSON object.`);
13469
- const env = {};
13470
- if (raw.api_base_url_1 !== void 0) {
13471
- if (typeof raw.api_base_url_1 !== "string") throw cliConfigError(`${context}.api_base_url_1 must be a string.`);
13472
- env.api_base_url_1 = normalizeApiBaseUrl1(raw.api_base_url_1);
13473
- }
13474
- if (raw.api_base_url_2 !== void 0) {
13475
- if (typeof raw.api_base_url_2 !== "string") throw cliConfigError(`${context}.api_base_url_2 must be a string.`);
13476
- env.api_base_url_2 = normalizeApiBaseUrl2(raw.api_base_url_2);
13477
- }
13478
- const headers = parseHeaders(raw.headers, context);
13479
- if (Object.keys(headers).length > 0) env.headers = headers;
13480
- return env;
13481
- }
13482
- function parseStoredCliConfig(raw) {
13483
- if (!isRecord$1(raw)) throw cliConfigError("Primitive CLI config must be a JSON object.");
13484
- if (raw.version !== CONFIG_VERSION) throw cliConfigError(`Primitive CLI config version must be ${CONFIG_VERSION}.`);
13485
- const currentRaw = raw.current_environment;
13486
- const current_environment = currentRaw === null || currentRaw === void 0 ? null : typeof currentRaw === "string" ? normalizeCliEnvironmentName(currentRaw) : (() => {
13487
- throw cliConfigError("Primitive CLI config current_environment must be a string or null.");
13488
- })();
13489
- if (!isRecord$1(raw.environments)) throw cliConfigError("Primitive CLI config environments must be an object.");
13490
- const environments = {};
13491
- for (const [rawName, rawEnv] of Object.entries(raw.environments)) {
13492
- const name = normalizeCliEnvironmentName(rawName);
13493
- environments[name] = parseEnvironmentConfig(rawEnv, `Primitive CLI config environment ${name}`);
13494
- }
13495
- if (current_environment && !environments[current_environment]) throw cliConfigError(`Primitive CLI config current environment ${current_environment} does not exist.`);
13496
- return {
13497
- version: CONFIG_VERSION,
13498
- current_environment,
13499
- environments
13500
- };
13501
- }
13502
- function emptyCliConfig() {
13503
- return {
13504
- version: CONFIG_VERSION,
13505
- current_environment: null,
13506
- environments: {}
13507
- };
13508
- }
13509
- function loadCliConfig(configDir) {
13510
- const path = cliConfigPath(configDir);
13511
- let contents;
13512
- try {
13513
- contents = readFileSync(path, "utf8");
13514
- } catch (error) {
13515
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
13516
- throw cliConfigError(`Could not read Primitive CLI config: ${error instanceof Error ? error.message : String(error)}.`);
13517
- }
13518
- try {
13519
- return parseStoredCliConfig(JSON.parse(contents));
13520
- } catch (error) {
13521
- if (error instanceof SyntaxError) throw cliConfigError("Primitive CLI config is not valid JSON.");
13522
- throw error;
13523
- }
13524
- }
13525
- function saveCliConfig(configDir, config) {
13526
- mkdirSync(configDir, {
13527
- mode: 448,
13528
- recursive: true
13529
- });
13530
- const path = cliConfigPath(configDir);
13531
- const tempPath = join(configDir, `${CONFIG_FILE}.${process.pid}.${randomUUID()}.tmp`);
13532
- try {
13533
- writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
13534
- renameSync(tempPath, path);
13535
- } catch (error) {
13536
- rmSync(tempPath, { force: true });
13537
- throw error;
13538
- }
13539
- }
13540
- function deleteCliConfig(configDir) {
13541
- rmSync(cliConfigPath(configDir), { force: true });
13542
- }
13543
- function resolveConfigEnvironment(config) {
13544
- if (!config) return null;
13545
- const current = config.current_environment;
13546
- if (current) {
13547
- const environment = config.environments[current];
13548
- return environment ? {
13549
- name: current,
13550
- config: environment
13551
- } : null;
13552
- }
13553
- const defaultEnvironment = config.environments[DEFAULT_ENVIRONMENT];
13554
- return defaultEnvironment ? {
13555
- name: DEFAULT_ENVIRONMENT,
13556
- config: defaultEnvironment
13557
- } : null;
13558
- }
13559
- function upsertCliEnvironment(params) {
13560
- const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
13561
- const existing = params.config.environments[name] ?? {};
13562
- const nextHeaders = { ...existing.headers ?? {} };
13563
- for (const assignment of params.headers ?? []) {
13564
- const [headerName, value] = parseHeaderAssignment(assignment);
13565
- nextHeaders[headerName] = value;
13566
- }
13567
- for (const rawName of params.unsetHeaders ?? []) delete nextHeaders[validateCliHeaderName(rawName)];
13568
- const nextEnvironment = {
13569
- ...existing,
13570
- ...params.apiBaseUrl1 !== void 0 ? { api_base_url_1: normalizeApiBaseUrl1(params.apiBaseUrl1) } : {},
13571
- ...params.apiBaseUrl2 !== void 0 ? { api_base_url_2: normalizeApiBaseUrl2(params.apiBaseUrl2) } : {},
13572
- ...Object.keys(nextHeaders).length > 0 ? { headers: nextHeaders } : {}
13573
- };
13574
- if (Object.keys(nextHeaders).length === 0) delete nextEnvironment.headers;
13575
- return {
13576
- ...params.config,
13577
- current_environment: params.use === false ? params.config.current_environment : name,
13578
- environments: {
13579
- ...params.config.environments,
13580
- [name]: nextEnvironment
13581
- }
13582
- };
13583
- }
13584
- function removeCliEnvironment(config, environmentName) {
13585
- const name = normalizeCliEnvironmentName(environmentName);
13586
- const environments = { ...config.environments };
13587
- delete environments[name];
13588
- return {
13589
- ...config,
13590
- current_environment: config.current_environment === name ? null : config.current_environment,
13591
- environments
13592
- };
13593
- }
13594
- function redactCliEnvironment(environment) {
13595
- const headers = environment.headers && Object.keys(environment.headers).length > 0 ? Object.fromEntries(Object.keys(environment.headers).map((name) => [name, "***"])) : void 0;
13596
- return {
13597
- ...environment,
13598
- ...headers ? { headers } : {}
13599
- };
13600
- }
13601
- //#endregion
13602
12708
  //#region src/oclif/api-client.ts
13603
12709
  const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
13604
12710
  const OAUTH_REFRESH_SKEW_MS = 60 * 1e3;
@@ -14515,15 +13621,11 @@ async function fetchEmailSearchPage(params) {
14515
13621
  function sleep$1(ms) {
14516
13622
  return new Promise((resolve) => setTimeout(resolve, ms));
14517
13623
  }
14518
- //#endregion
14519
- //#region src/oclif/commands/chat.ts
14520
- const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
14521
- const DEFAULT_STRICT_PHASE_SECONDS = 60;
14522
13624
  function cliError$6(message) {
14523
13625
  return new Errors.CLIError(message, { exit: 1 });
14524
13626
  }
14525
- async function readStdinToString() {
14526
- if (process.stdin.isTTY) throw cliError$6("No message provided. Pass the message as the second positional argument or pipe it via stdin.");
13627
+ async function readStdinToString(missingMessage = "No message provided. Pass the message as the second positional argument or pipe it via stdin.") {
13628
+ if (process.stdin.isTTY) throw cliError$6(missingMessage);
14527
13629
  const chunks = [];
14528
13630
  for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
14529
13631
  return Buffer.concat(chunks).toString("utf8");
@@ -14635,6 +13737,11 @@ function shellQuote(value) {
14635
13737
  function commandFromArgv(argv) {
14636
13738
  return argv.map(shellQuote).join(" ");
14637
13739
  }
13740
+ function parseLocalChatIdArg(value) {
13741
+ if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value)) return null;
13742
+ const parsed = Number(value);
13743
+ return Number.isSafeInteger(parsed) ? parsed : null;
13744
+ }
14638
13745
  function resolveChatResponseBody(reply) {
14639
13746
  if (reply.body_text && reply.body_text.length > 0) return {
14640
13747
  body: reply.body_text,
@@ -14689,10 +13796,35 @@ function buildCommand(kind, description, argv, options = {}) {
14689
13796
  requires_message: requiresMessage
14690
13797
  };
14691
13798
  }
13799
+ function shouldPreferStrictContinuation(context) {
13800
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
13801
+ return context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
13802
+ }
14692
13803
  function buildChatFollowUpCommands(context) {
14693
13804
  const commands = [];
14694
- const hasCustomStrictPhase = context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS;
14695
- const shouldPreferStrictContinuation = context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
13805
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
13806
+ const preferStrictContinuation = shouldPreferStrictContinuation(context);
13807
+ if (context.localChatId !== void 0) {
13808
+ const localContinueParts = [
13809
+ "primitive",
13810
+ "chat",
13811
+ "reply",
13812
+ String(context.localChatId),
13813
+ "<message>"
13814
+ ];
13815
+ if (context.json) localContinueParts.push("--json");
13816
+ if (context.quiet) localContinueParts.push("--quiet");
13817
+ commands.push(buildCommand("continue_chat", "Continue this chat", localContinueParts, { requiresMessage: true }));
13818
+ const activeContinueParts = [
13819
+ "primitive",
13820
+ "chat",
13821
+ "reply",
13822
+ "<message>"
13823
+ ];
13824
+ if (context.json) activeContinueParts.push("--json");
13825
+ if (context.quiet) activeContinueParts.push("--quiet");
13826
+ commands.push(buildCommand("continue_active_chat", "Continue the active chat", activeContinueParts, { requiresMessage: true }));
13827
+ }
14696
13828
  const continueParts = [
14697
13829
  "primitive",
14698
13830
  "chat",
@@ -14708,9 +13840,9 @@ function buildChatFollowUpCommands(context) {
14708
13840
  ];
14709
13841
  if (context.json) continueParts.push("--json");
14710
13842
  if (context.quiet) continueParts.push("--quiet");
14711
- if (shouldPreferStrictContinuation) continueParts.push("--strict-only");
13843
+ if (preferStrictContinuation) continueParts.push("--strict-only");
14712
13844
  else if (hasCustomStrictPhase) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
14713
- commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
13845
+ commands.push(buildCommand(context.localChatId === void 0 ? "continue_chat" : "continue_chat_explicit", context.localChatId === void 0 ? "Continue this chat" : "Continue this chat explicitly", continueParts, { requiresMessage: true }));
14714
13846
  commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
14715
13847
  "primitive",
14716
13848
  "reply",
@@ -14784,6 +13916,7 @@ function buildChatJsonEnvelope(context) {
14784
13916
  return {
14785
13917
  sent: context.sent,
14786
13918
  reply: context.reply,
13919
+ local_chat_id: context.localChatId ?? null,
14787
13920
  response_body: responseBody.body,
14788
13921
  response_body_format: responseBody.format,
14789
13922
  match: {
@@ -14794,6 +13927,25 @@ function buildChatJsonEnvelope(context) {
14794
13927
  follow_up_commands: buildChatFollowUpCommands(context)
14795
13928
  };
14796
13929
  }
13930
+ function persistActiveChat(params) {
13931
+ try {
13932
+ return saveActiveChatState(params.configDir, {
13933
+ from: params.context.from,
13934
+ last_reply_email_id: params.context.reply.id,
13935
+ last_reply_received_at: params.context.reply.received_at,
13936
+ last_sent_email_id: params.context.sent.id,
13937
+ recipient: params.context.recipient,
13938
+ strict_only: shouldPreferStrictContinuation(params.context),
13939
+ strict_phase_seconds: params.context.strictPhaseSeconds,
13940
+ thread_id: params.context.reply.thread_id ?? null,
13941
+ timeout_seconds: params.context.timeoutSeconds
13942
+ }, { preferredLocalId: params.preferredLocalId }).local_id;
13943
+ } catch (error) {
13944
+ const detail = error instanceof Error ? error.message : String(error);
13945
+ params.writeWarning?.(`Warning: could not save local chat state: ${detail}\n`);
13946
+ return null;
13947
+ }
13948
+ }
14797
13949
  function formatChatResponse(context) {
14798
13950
  const accepted = context.sent.accepted.join(", ") || context.recipient;
14799
13951
  const responseBody = resolveChatResponseBody(context.reply);
@@ -14817,6 +13969,7 @@ function formatChatResponse(context) {
14817
13969
  ];
14818
13970
  if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
14819
13971
  if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
13972
+ if (context.localChatId !== void 0) lines.push(` Local chat id: ${context.localChatId}`);
14820
13973
  lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.", " When shown, --strict-only prefers timing out over matching the wrong reply.");
14821
13974
  for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14822
13975
  lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
@@ -14906,6 +14059,8 @@ var ChatCommand = class ChatCommand extends Command {
14906
14059
  --reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
14907
14060
  reply endpoint, so the reply subject and threading headers are
14908
14061
  derived from the inbound email instead of copied into CLI flags.
14062
+ Successful chat turns also save an active local chat, so the next
14063
+ follow-up can be sent with \`primitive chat reply '<message>'\`.
14909
14064
 
14910
14065
  --json emits a structured envelope with both sides of the exchange,
14911
14066
  a direct response_body field, match details, and follow-up command
@@ -14925,6 +14080,7 @@ var ChatCommand = class ChatCommand extends Command {
14925
14080
  static examples = [
14926
14081
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14927
14082
  "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14083
+ "<%= config.bin %> chat reply 'one more thing'",
14928
14084
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14929
14085
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14930
14086
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
@@ -14960,15 +14116,20 @@ var ChatCommand = class ChatCommand extends Command {
14960
14116
  reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
14961
14117
  "reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
14962
14118
  "in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
14119
+ "chat-local-id": Flags.integer({
14120
+ description: "Local chat id to update after this command succeeds. Internal plumbing for `primitive chat reply`.",
14121
+ hidden: true,
14122
+ min: 0
14123
+ }),
14963
14124
  json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
14964
14125
  quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14965
14126
  timeout: Flags.integer({
14966
- default: DEFAULT_CHAT_TIMEOUT_SECONDS,
14127
+ default: 120,
14967
14128
  description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
14968
14129
  min: 0
14969
14130
  }),
14970
14131
  "strict-phase-seconds": Flags.integer({
14971
- default: DEFAULT_STRICT_PHASE_SECONDS,
14132
+ default: 60,
14972
14133
  description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
14973
14134
  min: 1
14974
14135
  }),
@@ -15152,16 +14313,133 @@ var ChatCommand = class ChatCommand extends Command {
15152
14313
  return;
15153
14314
  }
15154
14315
  progress?.succeed(`Reply received from ${replyResult.reply.from_email}`);
15155
- const outputContext = {
14316
+ let outputContext = {
15156
14317
  ...baseContext,
15157
14318
  matchStrategy: replyResult.matchStrategy,
15158
14319
  reply: replyResult.reply
15159
14320
  };
14321
+ const localChatId = persistActiveChat({
14322
+ configDir: this.config.configDir,
14323
+ context: outputContext,
14324
+ preferredLocalId: flags["chat-local-id"],
14325
+ writeWarning: (message) => process.stderr.write(message)
14326
+ });
14327
+ if (localChatId !== null) outputContext = {
14328
+ ...outputContext,
14329
+ localChatId
14330
+ };
15160
14331
  if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
15161
14332
  else this.log(formatChatResponse(outputContext));
15162
14333
  });
15163
14334
  }
15164
14335
  };
14336
+ var ChatReplyCommand = class ChatReplyCommand extends Command {
14337
+ static description = `Reply in the active chat.
14338
+
14339
+ A successful \`primitive chat <email> <message>\` saves the latest
14340
+ inbound reply as a local chat and makes it active. Use
14341
+ \`primitive chat reply <message>\` for the active chat, or
14342
+ \`primitive chat reply <local-id> <message>\` / \`--id <local-id>\`
14343
+ for a specific local chat. The command uses Primitive's real reply
14344
+ endpoint against the stored inbound email id, so the recipient,
14345
+ subject, and threading headers are derived server-side from the
14346
+ thread.
14347
+
14348
+ If no chat is open, start one with \`primitive chat <email> '<message>'\`.
14349
+ For explicit control, use \`primitive chat <email> --reply '<message>'
14350
+ --reply-to-email-id <inbound-email-id>\`.`;
14351
+ static summary = "Reply in the active chat";
14352
+ static examples = [
14353
+ "<%= config.bin %> chat reply 'one more thing'",
14354
+ "<%= config.bin %> chat reply 0 'one more thing'",
14355
+ "<%= config.bin %> chat reply --id 0 'one more thing'",
14356
+ "cat follow-up.txt | <%= config.bin %> chat reply"
14357
+ ];
14358
+ static args = {
14359
+ idOrMessage: Args.string({ description: "Reply body, or a local chat id when followed by a separate message." }),
14360
+ message: Args.string({ description: "Reply body when the first positional argument is an id." })
14361
+ };
14362
+ static flags = {
14363
+ "api-key": Flags.string({
14364
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive signin` credentials)",
14365
+ env: "PRIMITIVE_API_KEY"
14366
+ }),
14367
+ "api-base-url-1": Flags.string({
14368
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
14369
+ env: "PRIMITIVE_API_BASE_URL_1",
14370
+ hidden: true
14371
+ }),
14372
+ "api-base-url-2": Flags.string({
14373
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
14374
+ env: "PRIMITIVE_API_BASE_URL_2",
14375
+ hidden: true
14376
+ }),
14377
+ id: Flags.integer({
14378
+ description: "Local chat id to reply in. Omit to use the most recent active chat.",
14379
+ min: 0
14380
+ }),
14381
+ json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
14382
+ quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14383
+ timeout: Flags.integer({
14384
+ description: "Seconds to wait for a reply before exiting non-zero. Defaults to the active chat's last timeout.",
14385
+ min: 0
14386
+ }),
14387
+ "strict-phase-seconds": Flags.integer({
14388
+ description: "Seconds to wait in strict-threading mode before falling back. Defaults to the active chat's last setting.",
14389
+ min: 1
14390
+ }),
14391
+ "strict-only": Flags.boolean({ description: "Disable the time-window fallback. If the active chat was saved from a strict match, this is already the default." }),
14392
+ interval: Flags.integer({
14393
+ description: "Seconds between polls while waiting for the reply.",
14394
+ min: 1
14395
+ }),
14396
+ "page-size": Flags.integer({
14397
+ description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
14398
+ max: 100,
14399
+ min: 1,
14400
+ hidden: true
14401
+ }),
14402
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14403
+ };
14404
+ async run() {
14405
+ const { args, flags } = await this.parse(ChatReplyCommand);
14406
+ const positionalLocalId = flags.id === void 0 && args.message !== void 0 ? parseLocalChatIdArg(args.idOrMessage) : void 0;
14407
+ if (flags.id === void 0 && args.message !== void 0 && positionalLocalId === null) throw cliError$6("When passing two positional arguments to `primitive chat reply`, the first must be a local chat id. Use `primitive chat reply '<message>'` for the active chat or `primitive chat reply --id <id> '<message>'` for a specific chat.");
14408
+ if (flags.id !== void 0 && args.message !== void 0) throw cliError$6("With --id, pass the reply body as a single positional argument or pipe it via stdin.");
14409
+ const localId = flags.id ?? (typeof positionalLocalId === "number" ? positionalLocalId : void 0);
14410
+ const state = localId === void 0 ? loadActiveChatState(this.config.configDir) : loadChatConversationByLocalId(this.config.configDir, localId);
14411
+ if (!state) throw cliError$6(localId === void 0 ? "No open chat. Start one with `primitive chat <email> '<message>'`." : `No local chat ${localId}. Start one with \`primitive chat <email> '<message>'\` or omit --id to use the active chat.`);
14412
+ const message = args.message !== void 0 ? args.message : args.idOrMessage !== void 0 && args.idOrMessage !== "" ? args.idOrMessage : await readStdinToString("No reply body provided. Pass the reply body as a positional argument or pipe it via stdin.");
14413
+ if (!message.trim()) throw cliError$6("Reply body is empty.");
14414
+ const argv = [
14415
+ state.recipient,
14416
+ "--reply",
14417
+ message,
14418
+ "--from",
14419
+ state.from,
14420
+ "--reply-to-email-id",
14421
+ state.last_reply_email_id,
14422
+ "--timeout",
14423
+ String(flags.timeout ?? state.timeout_seconds),
14424
+ "--strict-phase-seconds",
14425
+ String(flags["strict-phase-seconds"] ?? state.strict_phase_seconds),
14426
+ "--interval",
14427
+ String(flags.interval ?? 2),
14428
+ "--page-size",
14429
+ String(flags["page-size"] ?? 50),
14430
+ "--chat-local-id",
14431
+ String(state.local_id)
14432
+ ];
14433
+ if (flags["api-key"] !== void 0) argv.push("--api-key", flags["api-key"]);
14434
+ if (flags["api-base-url-1"] !== void 0) argv.push("--api-base-url-1", flags["api-base-url-1"]);
14435
+ if (flags["api-base-url-2"] !== void 0) argv.push("--api-base-url-2", flags["api-base-url-2"]);
14436
+ if (flags.json) argv.push("--json");
14437
+ if (flags.quiet) argv.push("--quiet");
14438
+ if (state.strict_only || flags["strict-only"]) argv.push("--strict-only");
14439
+ if (flags.time) argv.push("--time");
14440
+ await ChatCommand.run(argv, { root: this.config.root });
14441
+ }
14442
+ };
15165
14443
  async function waitForReply(params) {
15166
14444
  const notice = params.notice ?? ((message) => {
15167
14445
  process.stderr.write(`${message}\n`);
@@ -16274,6 +15552,129 @@ async function waitForFunctionDeploy(params) {
16274
15552
  }
16275
15553
  }
16276
15554
  //#endregion
15555
+ //#region src/oclif/function-source.ts
15556
+ function collectSourceFiles(dir) {
15557
+ let pkgRaw;
15558
+ try {
15559
+ pkgRaw = readFileSync(join(dir, "package.json"), "utf8");
15560
+ } catch {
15561
+ return {
15562
+ kind: "error",
15563
+ message: `No package.json found in ${dir}. A managed build needs a package.json (its "dependencies" are installed).`
15564
+ };
15565
+ }
15566
+ let pkg;
15567
+ try {
15568
+ pkg = JSON.parse(pkgRaw);
15569
+ } catch (error) {
15570
+ return {
15571
+ kind: "error",
15572
+ message: `package.json in ${dir} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
15573
+ };
15574
+ }
15575
+ delete pkg.devDependencies;
15576
+ const files = { "package.json": `${JSON.stringify(pkg, null, 2)}\n` };
15577
+ const srcDir = join(dir, "src");
15578
+ if (isDirectory(srcDir)) for (const abs of walk(srcDir)) files[relative(dir, abs).split(sep).join("/")] = readFileSync(abs, "utf8");
15579
+ if (Object.keys(files).length === 1) return {
15580
+ kind: "error",
15581
+ message: `No source files found under ${srcDir}. Put your handler at src/index.ts.`
15582
+ };
15583
+ return {
15584
+ kind: "ok",
15585
+ files
15586
+ };
15587
+ }
15588
+ function isDirectory(path) {
15589
+ try {
15590
+ return statSync(path).isDirectory();
15591
+ } catch {
15592
+ return false;
15593
+ }
15594
+ }
15595
+ function* walk(dir) {
15596
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
15597
+ const abs = join(dir, entry.name);
15598
+ if (entry.isDirectory()) yield* walk(abs);
15599
+ else yield abs;
15600
+ }
15601
+ }
15602
+ async function runSourceDeploy(api, params) {
15603
+ const listed = await api.listFunctions();
15604
+ if (listed.error) return {
15605
+ kind: "error",
15606
+ payload: extractErrorPayload(listed.error),
15607
+ stage: "lookup"
15608
+ };
15609
+ const foundId = (listed.data?.data ?? []).find((f) => f.name === params.name)?.id ?? null;
15610
+ if (foundId !== null) {
15611
+ const updated = await api.updateFunction({
15612
+ files: params.files,
15613
+ id: foundId
15614
+ });
15615
+ if (updated.error) return {
15616
+ kind: "error",
15617
+ payload: extractErrorPayload(updated.error),
15618
+ stage: "redeploy"
15619
+ };
15620
+ const data = updated.data?.data;
15621
+ if (!data) return {
15622
+ kind: "error",
15623
+ payload: {
15624
+ code: "client_error",
15625
+ message: "Redeploy returned no data"
15626
+ },
15627
+ stage: "redeploy"
15628
+ };
15629
+ return {
15630
+ action: "redeployed",
15631
+ kind: "ok",
15632
+ result: data
15633
+ };
15634
+ }
15635
+ const created = await api.createFunction({
15636
+ files: params.files,
15637
+ name: params.name
15638
+ });
15639
+ if (created.error) return {
15640
+ kind: "error",
15641
+ payload: extractErrorPayload(created.error),
15642
+ stage: "create"
15643
+ };
15644
+ const data = created.data?.data;
15645
+ if (!data) return {
15646
+ kind: "error",
15647
+ payload: {
15648
+ code: "client_error",
15649
+ message: "Create returned no data"
15650
+ },
15651
+ stage: "create"
15652
+ };
15653
+ return {
15654
+ action: "created",
15655
+ kind: "ok",
15656
+ result: data
15657
+ };
15658
+ }
15659
+ function renderBuildFailure(payload, write) {
15660
+ if (typeof payload !== "object" || payload === null) return false;
15661
+ const error = payload.error ?? payload;
15662
+ if (typeof error !== "object" || error === null) return false;
15663
+ if (error.code !== "build_failed") return false;
15664
+ const details = error.details;
15665
+ const phase = typeof details === "object" && details !== null ? details.phase : void 0;
15666
+ write(`Build failed${typeof phase === "string" ? ` during ${phase}` : ""}.\n`);
15667
+ const errors = typeof details === "object" && details !== null ? details.errors : void 0;
15668
+ if (Array.isArray(errors)) for (const e of errors) {
15669
+ if (typeof e !== "object" || e === null) continue;
15670
+ const item = e;
15671
+ const loc = typeof item.file === "string" ? ` (${item.file}${typeof item.line === "number" ? `:${item.line}` : ""})` : "";
15672
+ write(` [${String(item.code)}] ${String(item.message)}${loc}\n`);
15673
+ if (typeof item.hint === "string") write(` hint: ${item.hint}\n`);
15674
+ }
15675
+ return true;
15676
+ }
15677
+ //#endregion
16277
15678
  //#region src/oclif/lint/raw-send-mail-fetch.ts
16278
15679
  const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
16279
15680
  const SNIPPET_PADDING = 60;
@@ -16740,6 +16141,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16740
16141
  static summary = "Deploy a new function from a bundled handler file";
16741
16142
  static examples = [
16742
16143
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
16144
+ "<%= config.bin %> functions deploy --name triage --source ./triage-agent",
16145
+ "<%= config.bin %> functions deploy --name triage --source . --wait",
16743
16146
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --wait",
16744
16147
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
16745
16148
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
@@ -16765,10 +16168,8 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16765
16168
  description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
16766
16169
  required: true
16767
16170
  }),
16768
- file: Flags.string({
16769
- description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
16770
- required: true
16771
- }),
16171
+ file: Flags.string({ description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field. Exactly one of --file or --source is required." }),
16172
+ source: Flags.string({ description: "Path to a project directory (containing package.json and src/) to deploy via managed build: the source is uploaded and the server installs dependencies, bundles for the Workers runtime, and deploys. Idempotent by name (creates the function, or redeploys it if --name already exists), so it is safe to run on every push. Exactly one of --file or --source is required." }),
16772
16173
  "source-map-file": Flags.string({ description: "Optional path to a source map for the bundle. Stored with the deployment attempt and used to symbolicate stack traces in function logs." }),
16773
16174
  secret: Flags.string({
16774
16175
  description: `Secret KEY=VALUE to seed on the deployed function. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out the deploy to create-function, set-secret per pair, then a final redeploy so the running handler picks up the bindings. ${SECRET_FLAG_SECURITY_NOTE}`,
@@ -16810,6 +16211,17 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16810
16211
  process.exitCode = 1;
16811
16212
  return;
16812
16213
  }
16214
+ if (flags.file === void 0 === (flags.source === void 0)) {
16215
+ process.stderr.write("Provide exactly one of --file (a pre-built bundle) or --source (a project directory for managed build).\n");
16216
+ process.exitCode = 1;
16217
+ return;
16218
+ }
16219
+ if (flags.source !== void 0) {
16220
+ await this.runSourceMode(flags, flags.source);
16221
+ return;
16222
+ }
16223
+ const file = flags.file;
16224
+ if (file === void 0) return;
16813
16225
  const parsedSecrets = resolveSecretFlags({
16814
16226
  fromEnv: flags["secret-from-env"] ?? [],
16815
16227
  fromEnvFile: flags["secret-from-env-file"] ?? [],
@@ -16822,7 +16234,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16822
16234
  process.exitCode = 1;
16823
16235
  return;
16824
16236
  }
16825
- const code = readTextFileFlag(flags.file, "--file");
16237
+ const code = readTextFileFlag(file, "--file");
16826
16238
  const sourceMap = flags["source-map-file"] ? readTextFileFlag(flags["source-map-file"], "--source-map-file") : void 0;
16827
16239
  emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
16828
16240
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
@@ -16928,6 +16340,101 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16928
16340
  this.log(JSON.stringify(payload, null, 2));
16929
16341
  });
16930
16342
  }
16343
+ async runSourceMode(flags, sourceDir) {
16344
+ if ((flags.secret?.length ?? 0) > 0 || (flags["secret-from-env"]?.length ?? 0) > 0 || (flags["secret-from-file"]?.length ?? 0) > 0 || (flags["secret-from-env-file"]?.length ?? 0) > 0 || flags["secret-from-stdin"] !== void 0) {
16345
+ process.stderr.write("Secret flags are not supported with --source yet. Deploy from source first, then set secrets with `primitive functions set-secret` and redeploy.\n");
16346
+ process.exitCode = 1;
16347
+ return;
16348
+ }
16349
+ const collected = collectSourceFiles(sourceDir);
16350
+ if (collected.kind === "error") {
16351
+ process.stderr.write(`${collected.message}\n`);
16352
+ process.exitCode = 1;
16353
+ return;
16354
+ }
16355
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
16356
+ apiKey: flags["api-key"],
16357
+ apiBaseUrl1: flags["api-base-url-1"],
16358
+ apiBaseUrl2: flags["api-base-url-2"],
16359
+ configDir: this.config.configDir
16360
+ });
16361
+ const authFailureContext = {
16362
+ auth,
16363
+ baseUrlOverridden,
16364
+ configDir: this.config.configDir
16365
+ };
16366
+ const outcome = await runSourceDeploy({
16367
+ createFunction: (p) => createFunction({
16368
+ body: {
16369
+ files: p.files,
16370
+ name: p.name
16371
+ },
16372
+ client: apiClient.client,
16373
+ responseStyle: "fields"
16374
+ }),
16375
+ listFunctions: () => listFunctions({
16376
+ client: apiClient.client,
16377
+ responseStyle: "fields"
16378
+ }),
16379
+ updateFunction: (p) => updateFunction({
16380
+ body: { files: p.files },
16381
+ client: apiClient.client,
16382
+ path: { id: p.id },
16383
+ responseStyle: "fields"
16384
+ })
16385
+ }, {
16386
+ files: collected.files,
16387
+ name: flags.name
16388
+ });
16389
+ if (outcome.kind === "error") {
16390
+ renderBuildFailure(outcome.payload, (chunk) => process.stderr.write(chunk));
16391
+ writeErrorWithHints(outcome.payload);
16392
+ surfaceUnauthorizedHint({
16393
+ ...authFailureContext,
16394
+ payload: outcome.payload
16395
+ });
16396
+ process.exitCode = 1;
16397
+ return;
16398
+ }
16399
+ const payload = outcome.result;
16400
+ if (flags.wait) {
16401
+ const waitResult = await waitForFunctionDeploy({
16402
+ getFunction: (p) => getFunction({
16403
+ client: apiClient.client,
16404
+ path: { id: p.id },
16405
+ responseStyle: "fields"
16406
+ }),
16407
+ id: payload.id,
16408
+ initial: payload,
16409
+ pollIntervalSeconds: flags["poll-interval"],
16410
+ timeoutSeconds: flags.timeout,
16411
+ writeStderr: (chunk) => process.stderr.write(chunk)
16412
+ });
16413
+ if (waitResult.kind === "error") {
16414
+ writeErrorWithHints(waitResult.payload);
16415
+ surfaceUnauthorizedHint({
16416
+ ...authFailureContext,
16417
+ payload: waitResult.payload
16418
+ });
16419
+ process.exitCode = 1;
16420
+ return;
16421
+ }
16422
+ if (waitResult.kind === "timeout") {
16423
+ const status = waitResult.lastFunction?.deploy_status ?? "unknown";
16424
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for function ${payload.id} deploy to finish (last status: ${status}).\n`);
16425
+ process.exitCode = 2;
16426
+ return;
16427
+ }
16428
+ this.log(JSON.stringify(waitResult.function, null, 2));
16429
+ if (waitResult.kind === "failed") {
16430
+ const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
16431
+ process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
16432
+ process.exitCode = 1;
16433
+ }
16434
+ return;
16435
+ }
16436
+ this.log(JSON.stringify(payload, null, 2));
16437
+ }
16931
16438
  };
16932
16439
  //#endregion
16933
16440
  //#region src/oclif/function-templates.ts
@@ -16937,8 +16444,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
16937
16444
  name: "Primitive Team",
16938
16445
  url: "https://primitive.dev"
16939
16446
  };
16940
- const SDK_VERSION_RANGE = "^0.32.0";
16941
- const CLI_VERSION_RANGE = "^0.32.1";
16447
+ const SDK_VERSION_RANGE = "^0.34.0";
16448
+ const CLI_VERSION_RANGE = "^0.34.0";
16942
16449
  const ESBUILD_VERSION_RANGE = "^0.27.0";
16943
16450
  function renderHandler() {
16944
16451
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -18655,6 +18162,7 @@ var LoginCommand$1 = class extends Command {
18655
18162
  if (polled.data) {
18656
18163
  const login = unwrapData$2(polled.data);
18657
18164
  if (!login) throw cliError$3("Primitive API returned an empty CLI poll response.");
18165
+ deleteChatState(this.config.configDir);
18658
18166
  saveCliCredentials(this.config.configDir, {
18659
18167
  access_token: login.access_token,
18660
18168
  api_base_url_1: apiBaseUrl1,
@@ -18876,6 +18384,7 @@ async function checkExistingCredentials(params) {
18876
18384
  throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
18877
18385
  }
18878
18386
  function saveSignupCredentials(params) {
18387
+ deleteChatState(params.configDir);
18879
18388
  saveCliCredentials(params.configDir, {
18880
18389
  access_token: params.signup.access_token,
18881
18390
  api_base_url_1: params.apiBaseUrl1,
@@ -19357,6 +18866,7 @@ function runForceLogout(params) {
19357
18866
  const lockPath = credentialsLockPath(params.configDir);
19358
18867
  const removed = [
19359
18868
  existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
18869
+ existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
19360
18870
  existsSync(pendingPath) ? "pending email-code auth state" : null,
19361
18871
  existsSync(lockPath) ? "credential lock" : null
19362
18872
  ].filter((value) => value !== null);
@@ -20410,6 +19920,7 @@ const CANONICAL_OPERATION_ALIASES = {
20410
19920
  "domains:list": "domains:list-domains",
20411
19921
  "domains:update": "domains:update-domain",
20412
19922
  "domains:verify": "domains:verify-domain",
19923
+ "emails:conversation": "emails:get-conversation",
20413
19924
  "emails:delete": "emails:delete-email",
20414
19925
  "emails:discard-content": "emails:discard-email-content",
20415
19926
  "emails:download-raw": "emails:download-raw-email",
@@ -20469,6 +19980,7 @@ const COMMANDS = {
20469
19980
  send: SendCommand,
20470
19981
  reply: ReplyCommand,
20471
19982
  chat: ChatCommand,
19983
+ "chat:reply": ChatReplyCommand,
20472
19984
  login: LoginCommand,
20473
19985
  "login:browser": LoginBrowserCommand,
20474
19986
  "login:confirm": LoginConfirmCommand,