@primitivedotdev/cli 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/bin/run.js +2 -2
- package/dist/cli-config-SktG2dzR.js +1304 -0
- package/dist/oclif/index.js +1517 -1334
- package/dist/oclif/root-signup-hint.js +105 -2
- package/package.json +10 -3
package/dist/oclif/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as createClient, C as saveCliCredentials, D as loadChatConversationByLocalId, E as loadActiveChatState, O as saveActiveChatState, S as resolveCliAuth, T as deleteChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as normalizeApiBaseUrl1, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createConfig, k as PrimitiveApiClient, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as chatStatePath, x as normalizeApiBaseUrl2, y as loadCliCredentials } from "../cli-config-SktG2dzR.js";
|
|
2
|
+
import { Args, Command, Errors, Flags, ux } from "@oclif/core";
|
|
2
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -18,614 +19,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
18
19
|
return target;
|
|
19
20
|
};
|
|
20
21
|
//#endregion
|
|
21
|
-
//#region ../packages/api-core/src/api/core/bodySerializer.gen.ts
|
|
22
|
-
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region ../packages/api-core/src/api/core/serverSentEvents.gen.ts
|
|
25
|
-
function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
|
|
26
|
-
let lastEventId;
|
|
27
|
-
const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
28
|
-
const createStream = async function* () {
|
|
29
|
-
let retryDelay = sseDefaultRetryDelay ?? 3e3;
|
|
30
|
-
let attempt = 0;
|
|
31
|
-
const signal = options.signal ?? new AbortController().signal;
|
|
32
|
-
while (true) {
|
|
33
|
-
if (signal.aborted) break;
|
|
34
|
-
attempt++;
|
|
35
|
-
const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
|
|
36
|
-
if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
|
|
37
|
-
try {
|
|
38
|
-
const requestInit = {
|
|
39
|
-
redirect: "follow",
|
|
40
|
-
...options,
|
|
41
|
-
body: options.serializedBody,
|
|
42
|
-
headers,
|
|
43
|
-
signal
|
|
44
|
-
};
|
|
45
|
-
let request = new Request(url, requestInit);
|
|
46
|
-
if (onRequest) request = await onRequest(url, requestInit);
|
|
47
|
-
const response = await (options.fetch ?? globalThis.fetch)(request);
|
|
48
|
-
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
|
49
|
-
if (!response.body) throw new Error("No body in SSE response");
|
|
50
|
-
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
51
|
-
let buffer = "";
|
|
52
|
-
const abortHandler = () => {
|
|
53
|
-
try {
|
|
54
|
-
reader.cancel();
|
|
55
|
-
} catch {}
|
|
56
|
-
};
|
|
57
|
-
signal.addEventListener("abort", abortHandler);
|
|
58
|
-
try {
|
|
59
|
-
while (true) {
|
|
60
|
-
const { done, value } = await reader.read();
|
|
61
|
-
if (done) break;
|
|
62
|
-
buffer += value;
|
|
63
|
-
buffer = buffer.replace(/\r\n?/g, "\n");
|
|
64
|
-
const chunks = buffer.split("\n\n");
|
|
65
|
-
buffer = chunks.pop() ?? "";
|
|
66
|
-
for (const chunk of chunks) {
|
|
67
|
-
const lines = chunk.split("\n");
|
|
68
|
-
const dataLines = [];
|
|
69
|
-
let eventName;
|
|
70
|
-
for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
|
|
71
|
-
else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
|
|
72
|
-
else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
|
|
73
|
-
else if (line.startsWith("retry:")) {
|
|
74
|
-
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
|
|
75
|
-
if (!Number.isNaN(parsed)) retryDelay = parsed;
|
|
76
|
-
}
|
|
77
|
-
let data;
|
|
78
|
-
let parsedJson = false;
|
|
79
|
-
if (dataLines.length) {
|
|
80
|
-
const rawData = dataLines.join("\n");
|
|
81
|
-
try {
|
|
82
|
-
data = JSON.parse(rawData);
|
|
83
|
-
parsedJson = true;
|
|
84
|
-
} catch {
|
|
85
|
-
data = rawData;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (parsedJson) {
|
|
89
|
-
if (responseValidator) await responseValidator(data);
|
|
90
|
-
if (responseTransformer) data = await responseTransformer(data);
|
|
91
|
-
}
|
|
92
|
-
onSseEvent?.({
|
|
93
|
-
data,
|
|
94
|
-
event: eventName,
|
|
95
|
-
id: lastEventId,
|
|
96
|
-
retry: retryDelay
|
|
97
|
-
});
|
|
98
|
-
if (dataLines.length) yield data;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} finally {
|
|
102
|
-
signal.removeEventListener("abort", abortHandler);
|
|
103
|
-
reader.releaseLock();
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
onSseError?.(error);
|
|
108
|
-
if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
|
|
109
|
-
await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
return { stream: createStream() };
|
|
114
|
-
}
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region ../packages/api-core/src/api/core/pathSerializer.gen.ts
|
|
117
|
-
const separatorArrayExplode = (style) => {
|
|
118
|
-
switch (style) {
|
|
119
|
-
case "label": return ".";
|
|
120
|
-
case "matrix": return ";";
|
|
121
|
-
case "simple": return ",";
|
|
122
|
-
default: return "&";
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
const separatorArrayNoExplode = (style) => {
|
|
126
|
-
switch (style) {
|
|
127
|
-
case "form": return ",";
|
|
128
|
-
case "pipeDelimited": return "|";
|
|
129
|
-
case "spaceDelimited": return "%20";
|
|
130
|
-
default: return ",";
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const separatorObjectExplode = (style) => {
|
|
134
|
-
switch (style) {
|
|
135
|
-
case "label": return ".";
|
|
136
|
-
case "matrix": return ";";
|
|
137
|
-
case "simple": return ",";
|
|
138
|
-
default: return "&";
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
|
|
142
|
-
if (!explode) {
|
|
143
|
-
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
|
|
144
|
-
switch (style) {
|
|
145
|
-
case "label": return `.${joinedValues}`;
|
|
146
|
-
case "matrix": return `;${name}=${joinedValues}`;
|
|
147
|
-
case "simple": return joinedValues;
|
|
148
|
-
default: return `${name}=${joinedValues}`;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const separator = separatorArrayExplode(style);
|
|
152
|
-
const joinedValues = value.map((v) => {
|
|
153
|
-
if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
|
|
154
|
-
return serializePrimitiveParam({
|
|
155
|
-
allowReserved,
|
|
156
|
-
name,
|
|
157
|
-
value: v
|
|
158
|
-
});
|
|
159
|
-
}).join(separator);
|
|
160
|
-
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
161
|
-
};
|
|
162
|
-
const serializePrimitiveParam = ({ allowReserved, name, value }) => {
|
|
163
|
-
if (value === void 0 || value === null) return "";
|
|
164
|
-
if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
|
|
165
|
-
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
|
166
|
-
};
|
|
167
|
-
const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
|
|
168
|
-
if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
|
169
|
-
if (style !== "deepObject" && !explode) {
|
|
170
|
-
let values = [];
|
|
171
|
-
Object.entries(value).forEach(([key, v]) => {
|
|
172
|
-
values = [
|
|
173
|
-
...values,
|
|
174
|
-
key,
|
|
175
|
-
allowReserved ? v : encodeURIComponent(v)
|
|
176
|
-
];
|
|
177
|
-
});
|
|
178
|
-
const joinedValues = values.join(",");
|
|
179
|
-
switch (style) {
|
|
180
|
-
case "form": return `${name}=${joinedValues}`;
|
|
181
|
-
case "label": return `.${joinedValues}`;
|
|
182
|
-
case "matrix": return `;${name}=${joinedValues}`;
|
|
183
|
-
default: return joinedValues;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const separator = separatorObjectExplode(style);
|
|
187
|
-
const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
|
|
188
|
-
allowReserved,
|
|
189
|
-
name: style === "deepObject" ? `${name}[${key}]` : key,
|
|
190
|
-
value: v
|
|
191
|
-
})).join(separator);
|
|
192
|
-
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
193
|
-
};
|
|
194
|
-
//#endregion
|
|
195
|
-
//#region ../packages/api-core/src/api/core/utils.gen.ts
|
|
196
|
-
const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
197
|
-
const defaultPathSerializer = ({ path, url: _url }) => {
|
|
198
|
-
let url = _url;
|
|
199
|
-
const matches = _url.match(PATH_PARAM_RE);
|
|
200
|
-
if (matches) for (const match of matches) {
|
|
201
|
-
let explode = false;
|
|
202
|
-
let name = match.substring(1, match.length - 1);
|
|
203
|
-
let style = "simple";
|
|
204
|
-
if (name.endsWith("*")) {
|
|
205
|
-
explode = true;
|
|
206
|
-
name = name.substring(0, name.length - 1);
|
|
207
|
-
}
|
|
208
|
-
if (name.startsWith(".")) {
|
|
209
|
-
name = name.substring(1);
|
|
210
|
-
style = "label";
|
|
211
|
-
} else if (name.startsWith(";")) {
|
|
212
|
-
name = name.substring(1);
|
|
213
|
-
style = "matrix";
|
|
214
|
-
}
|
|
215
|
-
const value = path[name];
|
|
216
|
-
if (value === void 0 || value === null) continue;
|
|
217
|
-
if (Array.isArray(value)) {
|
|
218
|
-
url = url.replace(match, serializeArrayParam({
|
|
219
|
-
explode,
|
|
220
|
-
name,
|
|
221
|
-
style,
|
|
222
|
-
value
|
|
223
|
-
}));
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (typeof value === "object") {
|
|
227
|
-
url = url.replace(match, serializeObjectParam({
|
|
228
|
-
explode,
|
|
229
|
-
name,
|
|
230
|
-
style,
|
|
231
|
-
value,
|
|
232
|
-
valueOnly: true
|
|
233
|
-
}));
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (style === "matrix") {
|
|
237
|
-
url = url.replace(match, `;${serializePrimitiveParam({
|
|
238
|
-
name,
|
|
239
|
-
value
|
|
240
|
-
})}`);
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
|
|
244
|
-
url = url.replace(match, replaceValue);
|
|
245
|
-
}
|
|
246
|
-
return url;
|
|
247
|
-
};
|
|
248
|
-
const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
|
|
249
|
-
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
|
250
|
-
let url = (baseUrl ?? "") + pathUrl;
|
|
251
|
-
if (path) url = defaultPathSerializer({
|
|
252
|
-
path,
|
|
253
|
-
url
|
|
254
|
-
});
|
|
255
|
-
let search = query ? querySerializer(query) : "";
|
|
256
|
-
if (search.startsWith("?")) search = search.substring(1);
|
|
257
|
-
if (search) url += `?${search}`;
|
|
258
|
-
return url;
|
|
259
|
-
};
|
|
260
|
-
function getValidRequestBody(options) {
|
|
261
|
-
const hasBody = options.body !== void 0;
|
|
262
|
-
if (hasBody && options.bodySerializer) {
|
|
263
|
-
if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
|
|
264
|
-
return options.body !== "" ? options.body : null;
|
|
265
|
-
}
|
|
266
|
-
if (hasBody) return options.body;
|
|
267
|
-
}
|
|
268
|
-
//#endregion
|
|
269
|
-
//#region ../packages/api-core/src/api/core/auth.gen.ts
|
|
270
|
-
const getAuthToken = async (auth, callback) => {
|
|
271
|
-
const token = typeof callback === "function" ? await callback(auth) : callback;
|
|
272
|
-
if (!token) return;
|
|
273
|
-
if (auth.scheme === "bearer") return `Bearer ${token}`;
|
|
274
|
-
if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
|
|
275
|
-
return token;
|
|
276
|
-
};
|
|
277
|
-
//#endregion
|
|
278
|
-
//#region ../packages/api-core/src/api/client/utils.gen.ts
|
|
279
|
-
const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
|
|
280
|
-
const querySerializer = (queryParams) => {
|
|
281
|
-
const search = [];
|
|
282
|
-
if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
|
|
283
|
-
const value = queryParams[name];
|
|
284
|
-
if (value === void 0 || value === null) continue;
|
|
285
|
-
const options = parameters[name] || args;
|
|
286
|
-
if (Array.isArray(value)) {
|
|
287
|
-
const serializedArray = serializeArrayParam({
|
|
288
|
-
allowReserved: options.allowReserved,
|
|
289
|
-
explode: true,
|
|
290
|
-
name,
|
|
291
|
-
style: "form",
|
|
292
|
-
value,
|
|
293
|
-
...options.array
|
|
294
|
-
});
|
|
295
|
-
if (serializedArray) search.push(serializedArray);
|
|
296
|
-
} else if (typeof value === "object") {
|
|
297
|
-
const serializedObject = serializeObjectParam({
|
|
298
|
-
allowReserved: options.allowReserved,
|
|
299
|
-
explode: true,
|
|
300
|
-
name,
|
|
301
|
-
style: "deepObject",
|
|
302
|
-
value,
|
|
303
|
-
...options.object
|
|
304
|
-
});
|
|
305
|
-
if (serializedObject) search.push(serializedObject);
|
|
306
|
-
} else {
|
|
307
|
-
const serializedPrimitive = serializePrimitiveParam({
|
|
308
|
-
allowReserved: options.allowReserved,
|
|
309
|
-
name,
|
|
310
|
-
value
|
|
311
|
-
});
|
|
312
|
-
if (serializedPrimitive) search.push(serializedPrimitive);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return search.join("&");
|
|
316
|
-
};
|
|
317
|
-
return querySerializer;
|
|
318
|
-
};
|
|
319
|
-
/**
|
|
320
|
-
* Infers parseAs value from provided Content-Type header.
|
|
321
|
-
*/
|
|
322
|
-
const getParseAs = (contentType) => {
|
|
323
|
-
if (!contentType) return "stream";
|
|
324
|
-
const cleanContent = contentType.split(";")[0]?.trim();
|
|
325
|
-
if (!cleanContent) return;
|
|
326
|
-
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
|
|
327
|
-
if (cleanContent === "multipart/form-data") return "formData";
|
|
328
|
-
if ([
|
|
329
|
-
"application/",
|
|
330
|
-
"audio/",
|
|
331
|
-
"image/",
|
|
332
|
-
"video/"
|
|
333
|
-
].some((type) => cleanContent.startsWith(type))) return "blob";
|
|
334
|
-
if (cleanContent.startsWith("text/")) return "text";
|
|
335
|
-
};
|
|
336
|
-
const checkForExistence = (options, name) => {
|
|
337
|
-
if (!name) return false;
|
|
338
|
-
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
|
|
339
|
-
return false;
|
|
340
|
-
};
|
|
341
|
-
const setAuthParams = async ({ security, ...options }) => {
|
|
342
|
-
for (const auth of security) {
|
|
343
|
-
if (checkForExistence(options, auth.name)) continue;
|
|
344
|
-
const token = await getAuthToken(auth, options.auth);
|
|
345
|
-
if (!token) continue;
|
|
346
|
-
const name = auth.name ?? "Authorization";
|
|
347
|
-
switch (auth.in) {
|
|
348
|
-
case "query":
|
|
349
|
-
if (!options.query) options.query = {};
|
|
350
|
-
options.query[name] = token;
|
|
351
|
-
break;
|
|
352
|
-
case "cookie":
|
|
353
|
-
options.headers.append("Cookie", `${name}=${token}`);
|
|
354
|
-
break;
|
|
355
|
-
default:
|
|
356
|
-
options.headers.set(name, token);
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
const buildUrl = (options) => getUrl({
|
|
362
|
-
baseUrl: options.baseUrl,
|
|
363
|
-
path: options.path,
|
|
364
|
-
query: options.query,
|
|
365
|
-
querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
|
|
366
|
-
url: options.url
|
|
367
|
-
});
|
|
368
|
-
const mergeConfigs = (a, b) => {
|
|
369
|
-
const config = {
|
|
370
|
-
...a,
|
|
371
|
-
...b
|
|
372
|
-
};
|
|
373
|
-
if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
|
374
|
-
config.headers = mergeHeaders$1(a.headers, b.headers);
|
|
375
|
-
return config;
|
|
376
|
-
};
|
|
377
|
-
const headersEntries = (headers) => {
|
|
378
|
-
const entries = [];
|
|
379
|
-
headers.forEach((value, key) => {
|
|
380
|
-
entries.push([key, value]);
|
|
381
|
-
});
|
|
382
|
-
return entries;
|
|
383
|
-
};
|
|
384
|
-
const mergeHeaders$1 = (...headers) => {
|
|
385
|
-
const mergedHeaders = new Headers();
|
|
386
|
-
for (const header of headers) {
|
|
387
|
-
if (!header) continue;
|
|
388
|
-
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
|
389
|
-
for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
|
|
390
|
-
else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
|
|
391
|
-
else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
|
|
392
|
-
}
|
|
393
|
-
return mergedHeaders;
|
|
394
|
-
};
|
|
395
|
-
var Interceptors = class {
|
|
396
|
-
fns = [];
|
|
397
|
-
clear() {
|
|
398
|
-
this.fns = [];
|
|
399
|
-
}
|
|
400
|
-
eject(id) {
|
|
401
|
-
const index = this.getInterceptorIndex(id);
|
|
402
|
-
if (this.fns[index]) this.fns[index] = null;
|
|
403
|
-
}
|
|
404
|
-
exists(id) {
|
|
405
|
-
const index = this.getInterceptorIndex(id);
|
|
406
|
-
return Boolean(this.fns[index]);
|
|
407
|
-
}
|
|
408
|
-
getInterceptorIndex(id) {
|
|
409
|
-
if (typeof id === "number") return this.fns[id] ? id : -1;
|
|
410
|
-
return this.fns.indexOf(id);
|
|
411
|
-
}
|
|
412
|
-
update(id, fn) {
|
|
413
|
-
const index = this.getInterceptorIndex(id);
|
|
414
|
-
if (this.fns[index]) {
|
|
415
|
-
this.fns[index] = fn;
|
|
416
|
-
return id;
|
|
417
|
-
}
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
use(fn) {
|
|
421
|
-
this.fns.push(fn);
|
|
422
|
-
return this.fns.length - 1;
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
const createInterceptors = () => ({
|
|
426
|
-
error: new Interceptors(),
|
|
427
|
-
request: new Interceptors(),
|
|
428
|
-
response: new Interceptors()
|
|
429
|
-
});
|
|
430
|
-
const defaultQuerySerializer = createQuerySerializer({
|
|
431
|
-
allowReserved: false,
|
|
432
|
-
array: {
|
|
433
|
-
explode: true,
|
|
434
|
-
style: "form"
|
|
435
|
-
},
|
|
436
|
-
object: {
|
|
437
|
-
explode: true,
|
|
438
|
-
style: "deepObject"
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
const defaultHeaders = { "Content-Type": "application/json" };
|
|
442
|
-
const createConfig = (override = {}) => ({
|
|
443
|
-
...jsonBodySerializer,
|
|
444
|
-
headers: defaultHeaders,
|
|
445
|
-
parseAs: "auto",
|
|
446
|
-
querySerializer: defaultQuerySerializer,
|
|
447
|
-
...override
|
|
448
|
-
});
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region ../packages/api-core/src/api/client/client.gen.ts
|
|
451
|
-
const createClient = (config = {}) => {
|
|
452
|
-
let _config = mergeConfigs(createConfig(), config);
|
|
453
|
-
const getConfig = () => ({ ..._config });
|
|
454
|
-
const setConfig = (config) => {
|
|
455
|
-
_config = mergeConfigs(_config, config);
|
|
456
|
-
return getConfig();
|
|
457
|
-
};
|
|
458
|
-
const interceptors = createInterceptors();
|
|
459
|
-
const beforeRequest = async (options) => {
|
|
460
|
-
const opts = {
|
|
461
|
-
..._config,
|
|
462
|
-
...options,
|
|
463
|
-
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
|
464
|
-
headers: mergeHeaders$1(_config.headers, options.headers),
|
|
465
|
-
serializedBody: void 0
|
|
466
|
-
};
|
|
467
|
-
if (opts.security) await setAuthParams({
|
|
468
|
-
...opts,
|
|
469
|
-
security: opts.security
|
|
470
|
-
});
|
|
471
|
-
if (opts.requestValidator) await opts.requestValidator(opts);
|
|
472
|
-
if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
|
|
473
|
-
if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
|
|
474
|
-
const resolvedOpts = opts;
|
|
475
|
-
return {
|
|
476
|
-
opts: resolvedOpts,
|
|
477
|
-
url: buildUrl(resolvedOpts)
|
|
478
|
-
};
|
|
479
|
-
};
|
|
480
|
-
const request = async (options) => {
|
|
481
|
-
const { opts, url } = await beforeRequest(options);
|
|
482
|
-
const requestInit = {
|
|
483
|
-
redirect: "follow",
|
|
484
|
-
...opts,
|
|
485
|
-
body: getValidRequestBody(opts)
|
|
486
|
-
};
|
|
487
|
-
let request = new Request(url, requestInit);
|
|
488
|
-
for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
|
|
489
|
-
const _fetch = opts.fetch;
|
|
490
|
-
let response;
|
|
491
|
-
try {
|
|
492
|
-
response = await _fetch(request);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
let finalError = error;
|
|
495
|
-
for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, void 0, request, opts);
|
|
496
|
-
finalError = finalError || {};
|
|
497
|
-
if (opts.throwOnError) throw finalError;
|
|
498
|
-
return opts.responseStyle === "data" ? void 0 : {
|
|
499
|
-
error: finalError,
|
|
500
|
-
request,
|
|
501
|
-
response: void 0
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
|
|
505
|
-
const result = {
|
|
506
|
-
request,
|
|
507
|
-
response
|
|
508
|
-
};
|
|
509
|
-
if (response.ok) {
|
|
510
|
-
const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
|
|
511
|
-
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
|
512
|
-
let emptyData;
|
|
513
|
-
switch (parseAs) {
|
|
514
|
-
case "arrayBuffer":
|
|
515
|
-
case "blob":
|
|
516
|
-
case "text":
|
|
517
|
-
emptyData = await response[parseAs]();
|
|
518
|
-
break;
|
|
519
|
-
case "formData":
|
|
520
|
-
emptyData = new FormData();
|
|
521
|
-
break;
|
|
522
|
-
case "stream":
|
|
523
|
-
emptyData = response.body;
|
|
524
|
-
break;
|
|
525
|
-
default:
|
|
526
|
-
emptyData = {};
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
return opts.responseStyle === "data" ? emptyData : {
|
|
530
|
-
data: emptyData,
|
|
531
|
-
...result
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
let data;
|
|
535
|
-
switch (parseAs) {
|
|
536
|
-
case "arrayBuffer":
|
|
537
|
-
case "blob":
|
|
538
|
-
case "formData":
|
|
539
|
-
case "text":
|
|
540
|
-
data = await response[parseAs]();
|
|
541
|
-
break;
|
|
542
|
-
case "json": {
|
|
543
|
-
const text = await response.text();
|
|
544
|
-
data = text ? JSON.parse(text) : {};
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
case "stream": return opts.responseStyle === "data" ? response.body : {
|
|
548
|
-
data: response.body,
|
|
549
|
-
...result
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
if (parseAs === "json") {
|
|
553
|
-
if (opts.responseValidator) await opts.responseValidator(data);
|
|
554
|
-
if (opts.responseTransformer) data = await opts.responseTransformer(data);
|
|
555
|
-
}
|
|
556
|
-
return opts.responseStyle === "data" ? data : {
|
|
557
|
-
data,
|
|
558
|
-
...result
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
const textError = await response.text();
|
|
562
|
-
let jsonError;
|
|
563
|
-
try {
|
|
564
|
-
jsonError = JSON.parse(textError);
|
|
565
|
-
} catch {}
|
|
566
|
-
const error = jsonError ?? textError;
|
|
567
|
-
let finalError = error;
|
|
568
|
-
for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, response, request, opts);
|
|
569
|
-
finalError = finalError || {};
|
|
570
|
-
if (opts.throwOnError) throw finalError;
|
|
571
|
-
return opts.responseStyle === "data" ? void 0 : {
|
|
572
|
-
error: finalError,
|
|
573
|
-
...result
|
|
574
|
-
};
|
|
575
|
-
};
|
|
576
|
-
const makeMethodFn = (method) => (options) => request({
|
|
577
|
-
...options,
|
|
578
|
-
method
|
|
579
|
-
});
|
|
580
|
-
const makeSseFn = (method) => async (options) => {
|
|
581
|
-
const { opts, url } = await beforeRequest(options);
|
|
582
|
-
return createSseClient({
|
|
583
|
-
...opts,
|
|
584
|
-
body: opts.body,
|
|
585
|
-
headers: opts.headers,
|
|
586
|
-
method,
|
|
587
|
-
onRequest: async (url, init) => {
|
|
588
|
-
let request = new Request(url, init);
|
|
589
|
-
for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
|
|
590
|
-
return request;
|
|
591
|
-
},
|
|
592
|
-
serializedBody: getValidRequestBody(opts),
|
|
593
|
-
url
|
|
594
|
-
});
|
|
595
|
-
};
|
|
596
|
-
const _buildUrl = (options) => buildUrl({
|
|
597
|
-
..._config,
|
|
598
|
-
...options
|
|
599
|
-
});
|
|
600
|
-
return {
|
|
601
|
-
buildUrl: _buildUrl,
|
|
602
|
-
connect: makeMethodFn("CONNECT"),
|
|
603
|
-
delete: makeMethodFn("DELETE"),
|
|
604
|
-
get: makeMethodFn("GET"),
|
|
605
|
-
getConfig,
|
|
606
|
-
head: makeMethodFn("HEAD"),
|
|
607
|
-
interceptors,
|
|
608
|
-
options: makeMethodFn("OPTIONS"),
|
|
609
|
-
patch: makeMethodFn("PATCH"),
|
|
610
|
-
post: makeMethodFn("POST"),
|
|
611
|
-
put: makeMethodFn("PUT"),
|
|
612
|
-
request,
|
|
613
|
-
setConfig,
|
|
614
|
-
sse: {
|
|
615
|
-
connect: makeSseFn("CONNECT"),
|
|
616
|
-
delete: makeSseFn("DELETE"),
|
|
617
|
-
get: makeSseFn("GET"),
|
|
618
|
-
head: makeSseFn("HEAD"),
|
|
619
|
-
options: makeSseFn("OPTIONS"),
|
|
620
|
-
patch: makeSseFn("PATCH"),
|
|
621
|
-
post: makeSseFn("POST"),
|
|
622
|
-
put: makeSseFn("PUT"),
|
|
623
|
-
trace: makeSseFn("TRACE")
|
|
624
|
-
},
|
|
625
|
-
trace: makeMethodFn("TRACE")
|
|
626
|
-
};
|
|
627
|
-
};
|
|
628
|
-
//#endregion
|
|
629
22
|
//#region ../packages/api-core/src/api/client.gen.ts
|
|
630
23
|
const client = createClient(createConfig({ baseUrl: "https://www.primitive.dev/api/v1" }));
|
|
631
24
|
//#endregion
|
|
@@ -648,6 +41,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
|
|
|
648
41
|
downloadDomainZoneFile: () => downloadDomainZoneFile,
|
|
649
42
|
downloadRawEmail: () => downloadRawEmail,
|
|
650
43
|
getAccount: () => getAccount,
|
|
44
|
+
getConversation: () => getConversation,
|
|
651
45
|
getEmail: () => getEmail,
|
|
652
46
|
getFunction: () => getFunction,
|
|
653
47
|
getFunctionTestRunTrace: () => getFunctionTestRunTrace,
|
|
@@ -674,6 +68,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
|
|
|
674
68
|
resendCliSignupVerification: () => resendCliSignupVerification,
|
|
675
69
|
rotateWebhookSecret: () => rotateWebhookSecret,
|
|
676
70
|
searchEmails: () => searchEmails,
|
|
71
|
+
semanticSearch: () => semanticSearch,
|
|
677
72
|
sendEmail: () => sendEmail,
|
|
678
73
|
setFunctionSecret: () => setFunctionSecret,
|
|
679
74
|
startAgentSignup: () => startAgentSignup,
|
|
@@ -1201,9 +596,9 @@ const downloadAttachments = (options) => (options.client ?? client).get({
|
|
|
1201
596
|
* derivation (Reply-To, then From, then bare sender), and the
|
|
1202
597
|
* `Re:` subject prefix are all derived server-side from the
|
|
1203
598
|
* stored inbound row. The request body carries only the message
|
|
1204
|
-
* body
|
|
1205
|
-
*
|
|
1206
|
-
* false`).
|
|
599
|
+
* body, optional From override, optional attachments, and optional
|
|
600
|
+
* `wait` flag; passing any header or recipient override is
|
|
601
|
+
* rejected by the schema (`additionalProperties: false`).
|
|
1207
602
|
*
|
|
1208
603
|
* Forwards through the same gates as `/send-mail`: the response
|
|
1209
604
|
* status, error envelope, and `idempotent_replay` flag mirror
|
|
@@ -1267,6 +662,37 @@ const discardEmailContent = (options) => (options.client ?? client).post({
|
|
|
1267
662
|
...options
|
|
1268
663
|
});
|
|
1269
664
|
/**
|
|
665
|
+
* Get the conversation an email belongs to
|
|
666
|
+
*
|
|
667
|
+
* Returns the full conversation the given inbound email belongs
|
|
668
|
+
* to, as ordered, ready-to-prompt turns WITH bodies. It resolves
|
|
669
|
+
* the thread from the email and returns every message oldest-first,
|
|
670
|
+
* so an agent that received an email can pass `messages` straight
|
|
671
|
+
* to a chat model in one call instead of walking `/threads/{id}`
|
|
672
|
+
* plus `/emails/{id}` and `/sent-emails/{id}` per message.
|
|
673
|
+
*
|
|
674
|
+
* Each message carries a `direction` (`inbound` | `outbound`) and a
|
|
675
|
+
* derived `role`: `inbound` -> `user`, `outbound` -> `assistant`
|
|
676
|
+
* (your own prior replies). The role mapping assumes the caller
|
|
677
|
+
* owns the outbound side, which is the agent-reply case this exists
|
|
678
|
+
* for. If the email has no thread yet (a brand-new message), the
|
|
679
|
+
* conversation is just that one message as a single user turn.
|
|
680
|
+
*
|
|
681
|
+
* The message list is capped; check `truncated` to detect when
|
|
682
|
+
* older messages were omitted. Consecutive same-role turns are not
|
|
683
|
+
* merged here; that normalization is model-specific and left to the
|
|
684
|
+
* caller.
|
|
685
|
+
*
|
|
686
|
+
*/
|
|
687
|
+
const getConversation = (options) => (options.client ?? client).get({
|
|
688
|
+
security: [{
|
|
689
|
+
scheme: "bearer",
|
|
690
|
+
type: "http"
|
|
691
|
+
}],
|
|
692
|
+
url: "/emails/{id}/conversation",
|
|
693
|
+
...options
|
|
694
|
+
});
|
|
695
|
+
/**
|
|
1270
696
|
* List webhook endpoints
|
|
1271
697
|
*
|
|
1272
698
|
* Returns all active (non-deleted) webhook endpoints.
|
|
@@ -1531,6 +957,42 @@ const sendEmail = (options) => (options.client ?? client).post({
|
|
|
1531
957
|
}
|
|
1532
958
|
});
|
|
1533
959
|
/**
|
|
960
|
+
* Semantic search across received and sent mail
|
|
961
|
+
*
|
|
962
|
+
* Ranked search across both received and sent mail. The `mode`
|
|
963
|
+
* field selects the ranking strategy:
|
|
964
|
+
*
|
|
965
|
+
* - `keyword`: lexical full-text matching only (no embeddings).
|
|
966
|
+
* - `semantic`: meaning-based matching using vector embeddings.
|
|
967
|
+
* - `hybrid` (default): blends the semantic and keyword signals.
|
|
968
|
+
*
|
|
969
|
+
* Results are ordered by a relevance `score`. Every row reports the
|
|
970
|
+
* fields it matched (`matched_fields`), a match-centered excerpt per
|
|
971
|
+
* field (`snippets`), and a `score_breakdown` whose components account
|
|
972
|
+
* for the `score`. Page through results by passing the prior
|
|
973
|
+
* response's `meta.cursor` back as `cursor`.
|
|
974
|
+
*
|
|
975
|
+
* Requires the Pro plan and the `semantic_search_enabled`
|
|
976
|
+
* entitlement; callers without them receive `403`.
|
|
977
|
+
*
|
|
978
|
+
* Host routing: this operation is served only by the search host
|
|
979
|
+
* (`https://api.primitive.dev/v1`). The typed SDKs route it there
|
|
980
|
+
* automatically.
|
|
981
|
+
*
|
|
982
|
+
*/
|
|
983
|
+
const semanticSearch = (options) => (options.client ?? client).post({
|
|
984
|
+
security: [{
|
|
985
|
+
scheme: "bearer",
|
|
986
|
+
type: "http"
|
|
987
|
+
}],
|
|
988
|
+
url: "/semantic-search",
|
|
989
|
+
...options,
|
|
990
|
+
headers: {
|
|
991
|
+
...options.body !== void 0 && { "Content-Type": "application/json" },
|
|
992
|
+
...options.headers
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
/**
|
|
1534
996
|
* List outbound sent emails
|
|
1535
997
|
*
|
|
1536
998
|
* Returns a paginated list of OUTBOUND emails the caller's
|
|
@@ -1953,6 +1415,10 @@ const openapiDocument = {
|
|
|
1953
1415
|
"name": "Emails",
|
|
1954
1416
|
"description": "List, inspect, and manage received emails"
|
|
1955
1417
|
},
|
|
1418
|
+
{
|
|
1419
|
+
"name": "Search",
|
|
1420
|
+
"description": "Semantic and hybrid search across received and sent mail"
|
|
1421
|
+
},
|
|
1956
1422
|
{
|
|
1957
1423
|
"name": "Sending",
|
|
1958
1424
|
"description": "Send outbound emails through the Primitive API"
|
|
@@ -2941,7 +2407,14 @@ const openapiDocument = {
|
|
|
2941
2407
|
"post": {
|
|
2942
2408
|
"operationId": "replyToEmail",
|
|
2943
2409
|
"summary": "Reply to an inbound email",
|
|
2944
|
-
"description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional
|
|
2410
|
+
"description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody, optional From override, optional attachments, and optional\n`wait` flag; passing any header or recipient override is\nrejected by the schema (`additionalProperties: false`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
|
|
2411
|
+
"servers": [{
|
|
2412
|
+
"url": "https://api.primitive.dev/v1",
|
|
2413
|
+
"description": "Attachments-supporting send host (recommended)"
|
|
2414
|
+
}, {
|
|
2415
|
+
"url": "https://www.primitive.dev/api/v1",
|
|
2416
|
+
"description": "Primary host (attachment-free replies only)"
|
|
2417
|
+
}],
|
|
2945
2418
|
"tags": ["Sending"],
|
|
2946
2419
|
"requestBody": {
|
|
2947
2420
|
"required": true,
|
|
@@ -3026,6 +2499,27 @@ const openapiDocument = {
|
|
|
3026
2499
|
}
|
|
3027
2500
|
}
|
|
3028
2501
|
},
|
|
2502
|
+
"/emails/{id}/conversation": {
|
|
2503
|
+
"parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
|
|
2504
|
+
"get": {
|
|
2505
|
+
"operationId": "getConversation",
|
|
2506
|
+
"summary": "Get the conversation an email belongs to",
|
|
2507
|
+
"description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
|
|
2508
|
+
"tags": ["Emails"],
|
|
2509
|
+
"responses": {
|
|
2510
|
+
"200": {
|
|
2511
|
+
"description": "Conversation",
|
|
2512
|
+
"content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
|
|
2513
|
+
"type": "object",
|
|
2514
|
+
"properties": { "data": { "$ref": "#/components/schemas/Conversation" } }
|
|
2515
|
+
}] } } }
|
|
2516
|
+
},
|
|
2517
|
+
"400": { "$ref": "#/components/responses/ValidationError" },
|
|
2518
|
+
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
2519
|
+
"404": { "$ref": "#/components/responses/NotFound" }
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
},
|
|
3029
2523
|
"/endpoints": {
|
|
3030
2524
|
"get": {
|
|
3031
2525
|
"operationId": "listEndpoints",
|
|
@@ -3370,6 +2864,42 @@ const openapiDocument = {
|
|
|
3370
2864
|
"503": { "$ref": "#/components/responses/ServiceUnavailable" }
|
|
3371
2865
|
}
|
|
3372
2866
|
} },
|
|
2867
|
+
"/semantic-search": { "post": {
|
|
2868
|
+
"operationId": "semanticSearch",
|
|
2869
|
+
"summary": "Semantic search across received and sent mail",
|
|
2870
|
+
"description": "Ranked search across both received and sent mail. The `mode`\nfield selects the ranking strategy:\n\n- `keyword`: lexical full-text matching only (no embeddings).\n- `semantic`: meaning-based matching using vector embeddings.\n- `hybrid` (default): blends the semantic and keyword signals.\n\nResults are ordered by a relevance `score`. Every row reports the\nfields it matched (`matched_fields`), a match-centered excerpt per\nfield (`snippets`), and a `score_breakdown` whose components account\nfor the `score`. Page through results by passing the prior\nresponse's `meta.cursor` back as `cursor`.\n\nRequires the Pro plan and the `semantic_search_enabled`\nentitlement; callers without them receive `403`.\n\nHost routing: this operation is served only by the search host\n(`https://api.primitive.dev/v1`). The typed SDKs route it there\nautomatically.\n",
|
|
2871
|
+
"servers": [{
|
|
2872
|
+
"url": "https://api.primitive.dev/v1",
|
|
2873
|
+
"description": "Search host"
|
|
2874
|
+
}],
|
|
2875
|
+
"tags": ["Search"],
|
|
2876
|
+
"requestBody": {
|
|
2877
|
+
"required": true,
|
|
2878
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SemanticSearchInput" } } }
|
|
2879
|
+
},
|
|
2880
|
+
"responses": {
|
|
2881
|
+
"200": {
|
|
2882
|
+
"description": "Ranked search results",
|
|
2883
|
+
"content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
|
|
2884
|
+
"type": "object",
|
|
2885
|
+
"properties": {
|
|
2886
|
+
"data": {
|
|
2887
|
+
"type": "array",
|
|
2888
|
+
"items": { "$ref": "#/components/schemas/SemanticSearchResult" }
|
|
2889
|
+
},
|
|
2890
|
+
"meta": { "$ref": "#/components/schemas/SemanticSearchMeta" }
|
|
2891
|
+
},
|
|
2892
|
+
"required": ["data", "meta"]
|
|
2893
|
+
}] } } }
|
|
2894
|
+
},
|
|
2895
|
+
"400": { "$ref": "#/components/responses/ValidationError" },
|
|
2896
|
+
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
2897
|
+
"403": { "$ref": "#/components/responses/Forbidden" },
|
|
2898
|
+
"429": { "$ref": "#/components/responses/RateLimited" },
|
|
2899
|
+
"500": { "$ref": "#/components/responses/InternalError" },
|
|
2900
|
+
"503": { "$ref": "#/components/responses/ServiceUnavailable" }
|
|
2901
|
+
}
|
|
2902
|
+
} },
|
|
3373
2903
|
"/sent-emails": { "get": {
|
|
3374
2904
|
"operationId": "listSentEmails",
|
|
3375
2905
|
"summary": "List outbound sent emails",
|
|
@@ -5864,46 +5394,118 @@ const openapiDocument = {
|
|
|
5864
5394
|
},
|
|
5865
5395
|
"required": ["direction", "id"]
|
|
5866
5396
|
},
|
|
5867
|
-
"
|
|
5397
|
+
"Conversation": {
|
|
5868
5398
|
"type": "object",
|
|
5869
|
-
"
|
|
5399
|
+
"description": "The full conversation an inbound email belongs to, as ordered,\nready-to-prompt turns with bodies. Resolves the thread from the\nemail and returns every message oldest-first, so an agent that\nreceived an email can pass `messages` straight to a chat model in\none call.\n",
|
|
5870
5400
|
"properties": {
|
|
5871
|
-
"
|
|
5872
|
-
"type": "string",
|
|
5873
|
-
"
|
|
5874
|
-
"
|
|
5875
|
-
"description": "Attachment filename. Control characters are rejected."
|
|
5401
|
+
"thread_id": {
|
|
5402
|
+
"type": ["string", "null"],
|
|
5403
|
+
"format": "uuid",
|
|
5404
|
+
"description": "The thread this email belongs to, or null when the email\nisn't threaded yet (the conversation is then just this one\nmessage).\n"
|
|
5876
5405
|
},
|
|
5877
|
-
"
|
|
5878
|
-
"type": "string",
|
|
5879
|
-
"
|
|
5880
|
-
"maxLength": 255,
|
|
5881
|
-
"description": "Optional MIME content type. Control characters are rejected."
|
|
5406
|
+
"subject": {
|
|
5407
|
+
"type": ["string", "null"],
|
|
5408
|
+
"description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
|
|
5882
5409
|
},
|
|
5883
|
-
"
|
|
5884
|
-
"type": "
|
|
5885
|
-
"
|
|
5886
|
-
|
|
5887
|
-
|
|
5410
|
+
"message_count": {
|
|
5411
|
+
"type": "integer",
|
|
5412
|
+
"description": "Total messages in the thread. `messages` is capped, so\n`truncated` is true (and this can exceed `messages.length`)\nwhen older messages were omitted.\n"
|
|
5413
|
+
},
|
|
5414
|
+
"truncated": {
|
|
5415
|
+
"type": "boolean",
|
|
5416
|
+
"description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
|
|
5417
|
+
},
|
|
5418
|
+
"messages": {
|
|
5419
|
+
"type": "array",
|
|
5420
|
+
"items": { "$ref": "#/components/schemas/ConversationMessage" }
|
|
5888
5421
|
}
|
|
5889
5422
|
},
|
|
5890
|
-
"required": [
|
|
5423
|
+
"required": [
|
|
5424
|
+
"thread_id",
|
|
5425
|
+
"message_count",
|
|
5426
|
+
"truncated",
|
|
5427
|
+
"messages"
|
|
5428
|
+
]
|
|
5891
5429
|
},
|
|
5892
|
-
"
|
|
5430
|
+
"ConversationMessage": {
|
|
5893
5431
|
"type": "object",
|
|
5894
|
-
"
|
|
5432
|
+
"description": "One message in the conversation, with its body and a chat role.",
|
|
5895
5433
|
"properties": {
|
|
5896
|
-
"
|
|
5434
|
+
"role": {
|
|
5897
5435
|
"type": "string",
|
|
5898
|
-
"
|
|
5899
|
-
"
|
|
5900
|
-
"description": "RFC 5322 From header. The sender domain must be a verified outbound domain for your organization."
|
|
5436
|
+
"enum": ["user", "assistant"],
|
|
5437
|
+
"description": "Chat role derived from `direction`: `user` for inbound\n(received) messages, `assistant` for outbound (your own prior\nreplies). Lets `messages` be passed directly to a chat model.\n"
|
|
5901
5438
|
},
|
|
5902
|
-
"
|
|
5439
|
+
"direction": {
|
|
5903
5440
|
"type": "string",
|
|
5904
|
-
"
|
|
5905
|
-
"
|
|
5906
|
-
|
|
5441
|
+
"enum": ["inbound", "outbound"],
|
|
5442
|
+
"description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
|
|
5443
|
+
},
|
|
5444
|
+
"id": {
|
|
5445
|
+
"type": "string",
|
|
5446
|
+
"format": "uuid"
|
|
5447
|
+
},
|
|
5448
|
+
"message_id": { "type": ["string", "null"] },
|
|
5449
|
+
"from": { "type": ["string", "null"] },
|
|
5450
|
+
"to": { "type": ["string", "null"] },
|
|
5451
|
+
"subject": { "type": ["string", "null"] },
|
|
5452
|
+
"text": {
|
|
5453
|
+
"type": "string",
|
|
5454
|
+
"description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
|
|
5455
|
+
},
|
|
5456
|
+
"timestamp": {
|
|
5457
|
+
"type": ["string", "null"],
|
|
5458
|
+
"format": "date-time",
|
|
5459
|
+
"description": "received_at for inbound, created_at for outbound."
|
|
5460
|
+
}
|
|
5461
|
+
},
|
|
5462
|
+
"required": [
|
|
5463
|
+
"role",
|
|
5464
|
+
"direction",
|
|
5465
|
+
"id",
|
|
5466
|
+
"text"
|
|
5467
|
+
]
|
|
5468
|
+
},
|
|
5469
|
+
"SendMailAttachment": {
|
|
5470
|
+
"type": "object",
|
|
5471
|
+
"additionalProperties": false,
|
|
5472
|
+
"properties": {
|
|
5473
|
+
"filename": {
|
|
5474
|
+
"type": "string",
|
|
5475
|
+
"minLength": 1,
|
|
5476
|
+
"maxLength": 255,
|
|
5477
|
+
"description": "Attachment filename. Control characters are rejected."
|
|
5478
|
+
},
|
|
5479
|
+
"content_type": {
|
|
5480
|
+
"type": "string",
|
|
5481
|
+
"minLength": 1,
|
|
5482
|
+
"maxLength": 255,
|
|
5483
|
+
"description": "Optional MIME content type. Control characters are rejected."
|
|
5484
|
+
},
|
|
5485
|
+
"content_base64": {
|
|
5486
|
+
"type": "string",
|
|
5487
|
+
"minLength": 1,
|
|
5488
|
+
"maxLength": 44040192,
|
|
5489
|
+
"description": "Base64-encoded attachment bytes."
|
|
5490
|
+
}
|
|
5491
|
+
},
|
|
5492
|
+
"required": ["filename", "content_base64"]
|
|
5493
|
+
},
|
|
5494
|
+
"SendMailInput": {
|
|
5495
|
+
"type": "object",
|
|
5496
|
+
"additionalProperties": false,
|
|
5497
|
+
"properties": {
|
|
5498
|
+
"from": {
|
|
5499
|
+
"type": "string",
|
|
5500
|
+
"minLength": 3,
|
|
5501
|
+
"maxLength": 998,
|
|
5502
|
+
"description": "RFC 5322 From header. The sender domain must be a verified outbound domain for your organization."
|
|
5503
|
+
},
|
|
5504
|
+
"to": {
|
|
5505
|
+
"type": "string",
|
|
5506
|
+
"minLength": 3,
|
|
5507
|
+
"maxLength": 320,
|
|
5508
|
+
"description": "Recipient address. Recipient eligibility depends on your account's outbound entitlements."
|
|
5907
5509
|
},
|
|
5908
5510
|
"subject": {
|
|
5909
5511
|
"type": "string",
|
|
@@ -6142,6 +5744,236 @@ const openapiDocument = {
|
|
|
6142
5744
|
"body_size_bytes"
|
|
6143
5745
|
]
|
|
6144
5746
|
},
|
|
5747
|
+
"SemanticSearchField": {
|
|
5748
|
+
"type": "string",
|
|
5749
|
+
"enum": [
|
|
5750
|
+
"subject",
|
|
5751
|
+
"headers",
|
|
5752
|
+
"addresses",
|
|
5753
|
+
"body"
|
|
5754
|
+
],
|
|
5755
|
+
"description": "A searchable email field."
|
|
5756
|
+
},
|
|
5757
|
+
"SemanticSearchInput": {
|
|
5758
|
+
"type": "object",
|
|
5759
|
+
"properties": {
|
|
5760
|
+
"query": {
|
|
5761
|
+
"type": "string",
|
|
5762
|
+
"minLength": 1,
|
|
5763
|
+
"maxLength": 2048,
|
|
5764
|
+
"description": "Free-text query. Required for `semantic` and `hybrid` modes;\noptional for `keyword` mode.\n"
|
|
5765
|
+
},
|
|
5766
|
+
"mode": {
|
|
5767
|
+
"type": "string",
|
|
5768
|
+
"enum": [
|
|
5769
|
+
"hybrid",
|
|
5770
|
+
"semantic",
|
|
5771
|
+
"keyword"
|
|
5772
|
+
],
|
|
5773
|
+
"default": "hybrid",
|
|
5774
|
+
"description": "Ranking strategy. `keyword` is lexical only, `semantic` is\nembedding-based, `hybrid` blends both.\n"
|
|
5775
|
+
},
|
|
5776
|
+
"corpus": {
|
|
5777
|
+
"type": "array",
|
|
5778
|
+
"items": {
|
|
5779
|
+
"type": "string",
|
|
5780
|
+
"enum": ["inbound", "outbound"]
|
|
5781
|
+
},
|
|
5782
|
+
"minItems": 1,
|
|
5783
|
+
"maxItems": 2,
|
|
5784
|
+
"description": "Which mail to search. Defaults to both received (`inbound`)\nand sent (`outbound`).\n"
|
|
5785
|
+
},
|
|
5786
|
+
"search_in": {
|
|
5787
|
+
"type": "array",
|
|
5788
|
+
"items": { "$ref": "#/components/schemas/SemanticSearchField" },
|
|
5789
|
+
"description": "Restrict matching to these fields. Defaults to all."
|
|
5790
|
+
},
|
|
5791
|
+
"exclude": {
|
|
5792
|
+
"type": "array",
|
|
5793
|
+
"items": { "$ref": "#/components/schemas/SemanticSearchField" },
|
|
5794
|
+
"description": "Exclude these fields from matching."
|
|
5795
|
+
},
|
|
5796
|
+
"date_from": {
|
|
5797
|
+
"type": "string",
|
|
5798
|
+
"format": "date-time",
|
|
5799
|
+
"description": "Only include mail at or after this timestamp."
|
|
5800
|
+
},
|
|
5801
|
+
"date_to": {
|
|
5802
|
+
"type": "string",
|
|
5803
|
+
"format": "date-time",
|
|
5804
|
+
"description": "Only include mail at or before this timestamp."
|
|
5805
|
+
},
|
|
5806
|
+
"include": {
|
|
5807
|
+
"type": "array",
|
|
5808
|
+
"items": {
|
|
5809
|
+
"type": "string",
|
|
5810
|
+
"enum": ["coverage"]
|
|
5811
|
+
},
|
|
5812
|
+
"description": "Opt-in extras. `coverage` adds an index-coverage snapshot to\n`meta`. Matched fields, snippets, and the score breakdown are\nalways returned regardless of this field.\n"
|
|
5813
|
+
},
|
|
5814
|
+
"limit": {
|
|
5815
|
+
"type": "integer",
|
|
5816
|
+
"minimum": 1,
|
|
5817
|
+
"maximum": 100,
|
|
5818
|
+
"default": 10,
|
|
5819
|
+
"description": "Maximum number of results to return."
|
|
5820
|
+
},
|
|
5821
|
+
"cursor": {
|
|
5822
|
+
"type": "string",
|
|
5823
|
+
"description": "Opaque pagination cursor from a prior response's `meta.cursor`."
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
},
|
|
5827
|
+
"SemanticSearchSnippet": {
|
|
5828
|
+
"type": "object",
|
|
5829
|
+
"properties": {
|
|
5830
|
+
"field": {
|
|
5831
|
+
"type": "string",
|
|
5832
|
+
"description": "The field this excerpt came from."
|
|
5833
|
+
},
|
|
5834
|
+
"text": {
|
|
5835
|
+
"type": "string",
|
|
5836
|
+
"description": "Plain-text excerpt centered on the match (no markup)."
|
|
5837
|
+
}
|
|
5838
|
+
},
|
|
5839
|
+
"required": ["field", "text"]
|
|
5840
|
+
},
|
|
5841
|
+
"SemanticSearchScoreBreakdown": {
|
|
5842
|
+
"type": "object",
|
|
5843
|
+
"description": "Additive contributions to `score`. `semantic` and `keyword` are the\nraw signals times the mode's weight (null when not applicable);\nthese plus `field_boost` and `recency` sum to `score` before each\nvalue is independently rounded to 5 decimal places.\n",
|
|
5844
|
+
"properties": {
|
|
5845
|
+
"semantic": { "type": ["number", "null"] },
|
|
5846
|
+
"keyword": { "type": ["number", "null"] },
|
|
5847
|
+
"field_boost": { "type": "number" },
|
|
5848
|
+
"recency": { "type": "number" }
|
|
5849
|
+
},
|
|
5850
|
+
"required": [
|
|
5851
|
+
"semantic",
|
|
5852
|
+
"keyword",
|
|
5853
|
+
"field_boost",
|
|
5854
|
+
"recency"
|
|
5855
|
+
]
|
|
5856
|
+
},
|
|
5857
|
+
"SemanticSearchResult": {
|
|
5858
|
+
"type": "object",
|
|
5859
|
+
"properties": {
|
|
5860
|
+
"source_type": {
|
|
5861
|
+
"type": "string",
|
|
5862
|
+
"enum": ["inbound_email", "sent_email"],
|
|
5863
|
+
"description": "Whether this row is a received or sent message."
|
|
5864
|
+
},
|
|
5865
|
+
"id": {
|
|
5866
|
+
"type": "string",
|
|
5867
|
+
"description": "Message id. Combine with `api_url` to fetch the full record."
|
|
5868
|
+
},
|
|
5869
|
+
"subject": { "type": ["string", "null"] },
|
|
5870
|
+
"from": { "type": ["string", "null"] },
|
|
5871
|
+
"to": { "type": ["string", "null"] },
|
|
5872
|
+
"timestamp": {
|
|
5873
|
+
"type": "string",
|
|
5874
|
+
"description": "Message timestamp (received_at for inbound, created_at for sent)."
|
|
5875
|
+
},
|
|
5876
|
+
"status": {
|
|
5877
|
+
"type": "string",
|
|
5878
|
+
"description": "Lifecycle status of the message."
|
|
5879
|
+
},
|
|
5880
|
+
"score": {
|
|
5881
|
+
"type": "number",
|
|
5882
|
+
"description": "Overall relevance score; the `score_breakdown` components account for it."
|
|
5883
|
+
},
|
|
5884
|
+
"semantic_score": {
|
|
5885
|
+
"type": ["number", "null"],
|
|
5886
|
+
"description": "Raw semantic similarity signal, or null when not applicable."
|
|
5887
|
+
},
|
|
5888
|
+
"keyword_score": {
|
|
5889
|
+
"type": ["number", "null"],
|
|
5890
|
+
"description": "Raw keyword (lexical) signal, or null when not applicable."
|
|
5891
|
+
},
|
|
5892
|
+
"matched_fields": {
|
|
5893
|
+
"type": "array",
|
|
5894
|
+
"items": { "$ref": "#/components/schemas/SemanticSearchField" },
|
|
5895
|
+
"description": "Fields where the query matched."
|
|
5896
|
+
},
|
|
5897
|
+
"snippets": {
|
|
5898
|
+
"type": "array",
|
|
5899
|
+
"items": { "$ref": "#/components/schemas/SemanticSearchSnippet" },
|
|
5900
|
+
"description": "Match-centered excerpts, one per matched field."
|
|
5901
|
+
},
|
|
5902
|
+
"score_breakdown": { "$ref": "#/components/schemas/SemanticSearchScoreBreakdown" },
|
|
5903
|
+
"api_url": {
|
|
5904
|
+
"type": ["string", "null"],
|
|
5905
|
+
"description": "Relative API path to fetch the full message."
|
|
5906
|
+
}
|
|
5907
|
+
},
|
|
5908
|
+
"required": [
|
|
5909
|
+
"source_type",
|
|
5910
|
+
"id",
|
|
5911
|
+
"subject",
|
|
5912
|
+
"from",
|
|
5913
|
+
"to",
|
|
5914
|
+
"timestamp",
|
|
5915
|
+
"status",
|
|
5916
|
+
"score",
|
|
5917
|
+
"semantic_score",
|
|
5918
|
+
"keyword_score",
|
|
5919
|
+
"matched_fields",
|
|
5920
|
+
"snippets",
|
|
5921
|
+
"score_breakdown",
|
|
5922
|
+
"api_url"
|
|
5923
|
+
]
|
|
5924
|
+
},
|
|
5925
|
+
"SemanticSearchCoverage": {
|
|
5926
|
+
"type": "object",
|
|
5927
|
+
"description": "Index-coverage snapshot for the org, returned only when the `coverage` include option is requested.",
|
|
5928
|
+
"properties": {
|
|
5929
|
+
"embedded_chunks": { "type": "integer" },
|
|
5930
|
+
"pending_chunks": { "type": "integer" },
|
|
5931
|
+
"skipped_plan_chunks": { "type": "integer" },
|
|
5932
|
+
"skipped_quota_chunks": { "type": "integer" },
|
|
5933
|
+
"unsupported_attachment_chunks": { "type": "integer" },
|
|
5934
|
+
"failed_chunks": { "type": "integer" }
|
|
5935
|
+
},
|
|
5936
|
+
"required": [
|
|
5937
|
+
"embedded_chunks",
|
|
5938
|
+
"pending_chunks",
|
|
5939
|
+
"skipped_plan_chunks",
|
|
5940
|
+
"skipped_quota_chunks",
|
|
5941
|
+
"unsupported_attachment_chunks",
|
|
5942
|
+
"failed_chunks"
|
|
5943
|
+
]
|
|
5944
|
+
},
|
|
5945
|
+
"SemanticSearchMeta": {
|
|
5946
|
+
"type": "object",
|
|
5947
|
+
"properties": {
|
|
5948
|
+
"limit": {
|
|
5949
|
+
"type": "integer",
|
|
5950
|
+
"description": "Page size used for this request."
|
|
5951
|
+
},
|
|
5952
|
+
"cursor": {
|
|
5953
|
+
"type": ["string", "null"],
|
|
5954
|
+
"description": "Cursor for the next page, or null if there are no more results."
|
|
5955
|
+
},
|
|
5956
|
+
"mode": {
|
|
5957
|
+
"type": "string",
|
|
5958
|
+
"enum": [
|
|
5959
|
+
"hybrid",
|
|
5960
|
+
"semantic",
|
|
5961
|
+
"keyword"
|
|
5962
|
+
],
|
|
5963
|
+
"description": "Ranking mode used for this response."
|
|
5964
|
+
},
|
|
5965
|
+
"coverage": {
|
|
5966
|
+
"oneOf": [{ "$ref": "#/components/schemas/SemanticSearchCoverage" }, { "type": "null" }],
|
|
5967
|
+
"description": "Index-coverage snapshot, present only when requested via\n`include: [coverage]`; otherwise null.\n"
|
|
5968
|
+
}
|
|
5969
|
+
},
|
|
5970
|
+
"required": [
|
|
5971
|
+
"limit",
|
|
5972
|
+
"cursor",
|
|
5973
|
+
"mode",
|
|
5974
|
+
"coverage"
|
|
5975
|
+
]
|
|
5976
|
+
},
|
|
6145
5977
|
"SentEmailDetail": {
|
|
6146
5978
|
"description": "Full sent-email record, including `body_text` and\n`body_html`. Returned by /sent-emails/{id}.\n",
|
|
6147
5979
|
"allOf": [{ "$ref": "#/components/schemas/SentEmailSummary" }, {
|
|
@@ -6180,6 +6012,12 @@ const openapiDocument = {
|
|
|
6180
6012
|
"wait": {
|
|
6181
6013
|
"type": "boolean",
|
|
6182
6014
|
"description": "When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics."
|
|
6015
|
+
},
|
|
6016
|
+
"attachments": {
|
|
6017
|
+
"type": "array",
|
|
6018
|
+
"maxItems": 100,
|
|
6019
|
+
"description": "Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.",
|
|
6020
|
+
"items": { "$ref": "#/components/schemas/SendMailAttachment" }
|
|
6183
6021
|
}
|
|
6184
6022
|
}
|
|
6185
6023
|
},
|
|
@@ -9023,12 +8861,12 @@ const operationManifest = [
|
|
|
9023
8861
|
{
|
|
9024
8862
|
"binaryResponse": false,
|
|
9025
8863
|
"bodyRequired": false,
|
|
9026
|
-
"command": "get-
|
|
9027
|
-
"description": "Returns the full
|
|
8864
|
+
"command": "get-conversation",
|
|
8865
|
+
"description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
|
|
9028
8866
|
"hasJsonBody": false,
|
|
9029
8867
|
"method": "GET",
|
|
9030
|
-
"operationId": "
|
|
9031
|
-
"path": "/emails/{id}",
|
|
8868
|
+
"operationId": "getConversation",
|
|
8869
|
+
"path": "/emails/{id}/conversation",
|
|
9032
8870
|
"pathParams": [{
|
|
9033
8871
|
"description": "Resource UUID",
|
|
9034
8872
|
"enum": null,
|
|
@@ -9040,71 +8878,165 @@ const operationManifest = [
|
|
|
9040
8878
|
"requestSchema": null,
|
|
9041
8879
|
"responseSchema": {
|
|
9042
8880
|
"type": "object",
|
|
8881
|
+
"description": "The full conversation an inbound email belongs to, as ordered,\nready-to-prompt turns with bodies. Resolves the thread from the\nemail and returns every message oldest-first, so an agent that\nreceived an email can pass `messages` straight to a chat model in\none call.\n",
|
|
9043
8882
|
"properties": {
|
|
9044
|
-
"
|
|
9045
|
-
"type": "string",
|
|
9046
|
-
"format": "uuid"
|
|
9047
|
-
},
|
|
9048
|
-
"message_id": { "type": ["string", "null"] },
|
|
9049
|
-
"domain_id": {
|
|
9050
|
-
"type": ["string", "null"],
|
|
9051
|
-
"format": "uuid"
|
|
9052
|
-
},
|
|
9053
|
-
"org_id": {
|
|
9054
|
-
"type": ["string", "null"],
|
|
9055
|
-
"format": "uuid"
|
|
9056
|
-
},
|
|
9057
|
-
"sender": {
|
|
9058
|
-
"type": "string",
|
|
9059
|
-
"description": "SMTP envelope sender (return-path) the inbound mail server\naccepted. Same value as `smtp_mail_from`; both fields exist\nso protocol-aware tooling can use whichever name it expects.\n\nFor most legitimate mail this equals `from_email`; for\nmailing lists, bounce handlers, and forwarders it is\ntypically the bounce-handling address rather than the\nhuman-visible sender.\n\n**For the canonical \"who sent this email\" value, use\n`from_email`.**\n"
|
|
9060
|
-
},
|
|
9061
|
-
"recipient": { "type": "string" },
|
|
9062
|
-
"subject": { "type": ["string", "null"] },
|
|
9063
|
-
"body_text": {
|
|
8883
|
+
"thread_id": {
|
|
9064
8884
|
"type": ["string", "null"],
|
|
9065
|
-
"
|
|
8885
|
+
"format": "uuid",
|
|
8886
|
+
"description": "The thread this email belongs to, or null when the email\nisn't threaded yet (the conversation is then just this one\nmessage).\n"
|
|
9066
8887
|
},
|
|
9067
|
-
"
|
|
8888
|
+
"subject": {
|
|
9068
8889
|
"type": ["string", "null"],
|
|
9069
|
-
"description": "
|
|
9070
|
-
},
|
|
9071
|
-
"status": {
|
|
9072
|
-
"type": "string",
|
|
9073
|
-
"description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
|
|
9074
|
-
"enum": [
|
|
9075
|
-
"pending",
|
|
9076
|
-
"accepted",
|
|
9077
|
-
"completed",
|
|
9078
|
-
"rejected"
|
|
9079
|
-
]
|
|
9080
|
-
},
|
|
9081
|
-
"domain": { "type": "string" },
|
|
9082
|
-
"spam_score": { "type": ["number", "null"] },
|
|
9083
|
-
"raw_size_bytes": { "type": ["integer", "null"] },
|
|
9084
|
-
"raw_sha256": { "type": ["string", "null"] },
|
|
9085
|
-
"created_at": {
|
|
9086
|
-
"type": "string",
|
|
9087
|
-
"format": "date-time"
|
|
8890
|
+
"description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
|
|
9088
8891
|
},
|
|
9089
|
-
"
|
|
9090
|
-
"type": "
|
|
9091
|
-
"
|
|
8892
|
+
"message_count": {
|
|
8893
|
+
"type": "integer",
|
|
8894
|
+
"description": "Total messages in the thread. `messages` is capped, so\n`truncated` is true (and this can exceed `messages.length`)\nwhen older messages were omitted.\n"
|
|
9092
8895
|
},
|
|
9093
|
-
"
|
|
9094
|
-
|
|
9095
|
-
"
|
|
9096
|
-
"description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
|
|
9097
|
-
"enum": [
|
|
9098
|
-
"pending",
|
|
9099
|
-
"in_flight",
|
|
9100
|
-
"fired",
|
|
9101
|
-
"failed",
|
|
9102
|
-
"exhausted",
|
|
9103
|
-
null
|
|
9104
|
-
]
|
|
8896
|
+
"truncated": {
|
|
8897
|
+
"type": "boolean",
|
|
8898
|
+
"description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
|
|
9105
8899
|
},
|
|
9106
|
-
"
|
|
9107
|
-
|
|
8900
|
+
"messages": {
|
|
8901
|
+
"type": "array",
|
|
8902
|
+
"items": {
|
|
8903
|
+
"type": "object",
|
|
8904
|
+
"description": "One message in the conversation, with its body and a chat role.",
|
|
8905
|
+
"properties": {
|
|
8906
|
+
"role": {
|
|
8907
|
+
"type": "string",
|
|
8908
|
+
"enum": ["user", "assistant"],
|
|
8909
|
+
"description": "Chat role derived from `direction`: `user` for inbound\n(received) messages, `assistant` for outbound (your own prior\nreplies). Lets `messages` be passed directly to a chat model.\n"
|
|
8910
|
+
},
|
|
8911
|
+
"direction": {
|
|
8912
|
+
"type": "string",
|
|
8913
|
+
"enum": ["inbound", "outbound"],
|
|
8914
|
+
"description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
|
|
8915
|
+
},
|
|
8916
|
+
"id": {
|
|
8917
|
+
"type": "string",
|
|
8918
|
+
"format": "uuid"
|
|
8919
|
+
},
|
|
8920
|
+
"message_id": { "type": ["string", "null"] },
|
|
8921
|
+
"from": { "type": ["string", "null"] },
|
|
8922
|
+
"to": { "type": ["string", "null"] },
|
|
8923
|
+
"subject": { "type": ["string", "null"] },
|
|
8924
|
+
"text": {
|
|
8925
|
+
"type": "string",
|
|
8926
|
+
"description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
|
|
8927
|
+
},
|
|
8928
|
+
"timestamp": {
|
|
8929
|
+
"type": ["string", "null"],
|
|
8930
|
+
"format": "date-time",
|
|
8931
|
+
"description": "received_at for inbound, created_at for outbound."
|
|
8932
|
+
}
|
|
8933
|
+
},
|
|
8934
|
+
"required": [
|
|
8935
|
+
"role",
|
|
8936
|
+
"direction",
|
|
8937
|
+
"id",
|
|
8938
|
+
"text"
|
|
8939
|
+
]
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
},
|
|
8943
|
+
"required": [
|
|
8944
|
+
"thread_id",
|
|
8945
|
+
"message_count",
|
|
8946
|
+
"truncated",
|
|
8947
|
+
"messages"
|
|
8948
|
+
]
|
|
8949
|
+
},
|
|
8950
|
+
"sdkName": "getConversation",
|
|
8951
|
+
"summary": "Get the conversation an email belongs to",
|
|
8952
|
+
"tag": "Emails",
|
|
8953
|
+
"tagCommand": "emails"
|
|
8954
|
+
},
|
|
8955
|
+
{
|
|
8956
|
+
"binaryResponse": false,
|
|
8957
|
+
"bodyRequired": false,
|
|
8958
|
+
"command": "get-email",
|
|
8959
|
+
"description": "Returns the full record for an inbound email received at one\nof your verified domains, including the parsed text and HTML\nbodies, threading metadata, SMTP envelope detail, webhook\ndelivery state, and a `replies` array for any outbound sends\nrecorded as replies to this inbound.\n\nFor listing inbound emails (with cursor pagination, status\nand date filters, and free-text search), use\n`/emails`. Outbound (sent) email records are NOT returned\nhere; use `/sent-emails/{id}` for those.\n\nThe response carries four sender-shaped fields whose\nmeanings overlap. `from_email` is the canonical \"who sent\nthis\" field for most use cases (parsed bare address from\nthe `From:` header, with a `sender` fallback). `from_header`\nis the raw header including any display name. `sender` and\n`smtp_mail_from` both carry the SMTP envelope MAIL FROM\n(return-path) and are equal by construction; `sender` is\nthe older field name retained for compatibility. See\n`primitive describe emails:get-email | jq '.responseSchema.properties'`\nfor per-field detail.\n",
|
|
8960
|
+
"hasJsonBody": false,
|
|
8961
|
+
"method": "GET",
|
|
8962
|
+
"operationId": "getEmail",
|
|
8963
|
+
"path": "/emails/{id}",
|
|
8964
|
+
"pathParams": [{
|
|
8965
|
+
"description": "Resource UUID",
|
|
8966
|
+
"enum": null,
|
|
8967
|
+
"name": "id",
|
|
8968
|
+
"required": true,
|
|
8969
|
+
"type": "string"
|
|
8970
|
+
}],
|
|
8971
|
+
"queryParams": [],
|
|
8972
|
+
"requestSchema": null,
|
|
8973
|
+
"responseSchema": {
|
|
8974
|
+
"type": "object",
|
|
8975
|
+
"properties": {
|
|
8976
|
+
"id": {
|
|
8977
|
+
"type": "string",
|
|
8978
|
+
"format": "uuid"
|
|
8979
|
+
},
|
|
8980
|
+
"message_id": { "type": ["string", "null"] },
|
|
8981
|
+
"domain_id": {
|
|
8982
|
+
"type": ["string", "null"],
|
|
8983
|
+
"format": "uuid"
|
|
8984
|
+
},
|
|
8985
|
+
"org_id": {
|
|
8986
|
+
"type": ["string", "null"],
|
|
8987
|
+
"format": "uuid"
|
|
8988
|
+
},
|
|
8989
|
+
"sender": {
|
|
8990
|
+
"type": "string",
|
|
8991
|
+
"description": "SMTP envelope sender (return-path) the inbound mail server\naccepted. Same value as `smtp_mail_from`; both fields exist\nso protocol-aware tooling can use whichever name it expects.\n\nFor most legitimate mail this equals `from_email`; for\nmailing lists, bounce handlers, and forwarders it is\ntypically the bounce-handling address rather than the\nhuman-visible sender.\n\n**For the canonical \"who sent this email\" value, use\n`from_email`.**\n"
|
|
8992
|
+
},
|
|
8993
|
+
"recipient": { "type": "string" },
|
|
8994
|
+
"subject": { "type": ["string", "null"] },
|
|
8995
|
+
"body_text": {
|
|
8996
|
+
"type": ["string", "null"],
|
|
8997
|
+
"description": "Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed."
|
|
8998
|
+
},
|
|
8999
|
+
"body_html": {
|
|
9000
|
+
"type": ["string", "null"],
|
|
9001
|
+
"description": "HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed."
|
|
9002
|
+
},
|
|
9003
|
+
"status": {
|
|
9004
|
+
"type": "string",
|
|
9005
|
+
"description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
|
|
9006
|
+
"enum": [
|
|
9007
|
+
"pending",
|
|
9008
|
+
"accepted",
|
|
9009
|
+
"completed",
|
|
9010
|
+
"rejected"
|
|
9011
|
+
]
|
|
9012
|
+
},
|
|
9013
|
+
"domain": { "type": "string" },
|
|
9014
|
+
"spam_score": { "type": ["number", "null"] },
|
|
9015
|
+
"raw_size_bytes": { "type": ["integer", "null"] },
|
|
9016
|
+
"raw_sha256": { "type": ["string", "null"] },
|
|
9017
|
+
"created_at": {
|
|
9018
|
+
"type": "string",
|
|
9019
|
+
"format": "date-time"
|
|
9020
|
+
},
|
|
9021
|
+
"received_at": {
|
|
9022
|
+
"type": "string",
|
|
9023
|
+
"format": "date-time"
|
|
9024
|
+
},
|
|
9025
|
+
"rejection_reason": { "type": ["string", "null"] },
|
|
9026
|
+
"webhook_status": {
|
|
9027
|
+
"type": ["string", "null"],
|
|
9028
|
+
"description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
|
|
9029
|
+
"enum": [
|
|
9030
|
+
"pending",
|
|
9031
|
+
"in_flight",
|
|
9032
|
+
"fired",
|
|
9033
|
+
"failed",
|
|
9034
|
+
"exhausted",
|
|
9035
|
+
null
|
|
9036
|
+
]
|
|
9037
|
+
},
|
|
9038
|
+
"webhook_attempt_count": { "type": "integer" },
|
|
9039
|
+
"webhook_last_attempt_at": {
|
|
9108
9040
|
"type": ["string", "null"],
|
|
9109
9041
|
"format": "date-time"
|
|
9110
9042
|
},
|
|
@@ -11844,6 +11776,218 @@ const operationManifest = [
|
|
|
11844
11776
|
"tag": "Inbox",
|
|
11845
11777
|
"tagCommand": "inbox"
|
|
11846
11778
|
},
|
|
11779
|
+
{
|
|
11780
|
+
"binaryResponse": false,
|
|
11781
|
+
"bodyRequired": true,
|
|
11782
|
+
"command": "semantic-search",
|
|
11783
|
+
"description": "Ranked search across both received and sent mail. The `mode`\nfield selects the ranking strategy:\n\n- `keyword`: lexical full-text matching only (no embeddings).\n- `semantic`: meaning-based matching using vector embeddings.\n- `hybrid` (default): blends the semantic and keyword signals.\n\nResults are ordered by a relevance `score`. Every row reports the\nfields it matched (`matched_fields`), a match-centered excerpt per\nfield (`snippets`), and a `score_breakdown` whose components account\nfor the `score`. Page through results by passing the prior\nresponse's `meta.cursor` back as `cursor`.\n\nRequires the Pro plan and the `semantic_search_enabled`\nentitlement; callers without them receive `403`.\n\nHost routing: this operation is served only by the search host\n(`https://api.primitive.dev/v1`). The typed SDKs route it there\nautomatically.\n",
|
|
11784
|
+
"hasJsonBody": true,
|
|
11785
|
+
"method": "POST",
|
|
11786
|
+
"operationId": "semanticSearch",
|
|
11787
|
+
"path": "/semantic-search",
|
|
11788
|
+
"pathParams": [],
|
|
11789
|
+
"queryParams": [],
|
|
11790
|
+
"requestSchema": {
|
|
11791
|
+
"type": "object",
|
|
11792
|
+
"properties": {
|
|
11793
|
+
"query": {
|
|
11794
|
+
"type": "string",
|
|
11795
|
+
"minLength": 1,
|
|
11796
|
+
"maxLength": 2048,
|
|
11797
|
+
"description": "Free-text query. Required for `semantic` and `hybrid` modes;\noptional for `keyword` mode.\n"
|
|
11798
|
+
},
|
|
11799
|
+
"mode": {
|
|
11800
|
+
"type": "string",
|
|
11801
|
+
"enum": [
|
|
11802
|
+
"hybrid",
|
|
11803
|
+
"semantic",
|
|
11804
|
+
"keyword"
|
|
11805
|
+
],
|
|
11806
|
+
"default": "hybrid",
|
|
11807
|
+
"description": "Ranking strategy. `keyword` is lexical only, `semantic` is\nembedding-based, `hybrid` blends both.\n"
|
|
11808
|
+
},
|
|
11809
|
+
"corpus": {
|
|
11810
|
+
"type": "array",
|
|
11811
|
+
"items": {
|
|
11812
|
+
"type": "string",
|
|
11813
|
+
"enum": ["inbound", "outbound"]
|
|
11814
|
+
},
|
|
11815
|
+
"minItems": 1,
|
|
11816
|
+
"maxItems": 2,
|
|
11817
|
+
"description": "Which mail to search. Defaults to both received (`inbound`)\nand sent (`outbound`).\n"
|
|
11818
|
+
},
|
|
11819
|
+
"search_in": {
|
|
11820
|
+
"type": "array",
|
|
11821
|
+
"items": {
|
|
11822
|
+
"type": "string",
|
|
11823
|
+
"enum": [
|
|
11824
|
+
"subject",
|
|
11825
|
+
"headers",
|
|
11826
|
+
"addresses",
|
|
11827
|
+
"body"
|
|
11828
|
+
],
|
|
11829
|
+
"description": "A searchable email field."
|
|
11830
|
+
},
|
|
11831
|
+
"description": "Restrict matching to these fields. Defaults to all."
|
|
11832
|
+
},
|
|
11833
|
+
"exclude": {
|
|
11834
|
+
"type": "array",
|
|
11835
|
+
"items": {
|
|
11836
|
+
"type": "string",
|
|
11837
|
+
"enum": [
|
|
11838
|
+
"subject",
|
|
11839
|
+
"headers",
|
|
11840
|
+
"addresses",
|
|
11841
|
+
"body"
|
|
11842
|
+
],
|
|
11843
|
+
"description": "A searchable email field."
|
|
11844
|
+
},
|
|
11845
|
+
"description": "Exclude these fields from matching."
|
|
11846
|
+
},
|
|
11847
|
+
"date_from": {
|
|
11848
|
+
"type": "string",
|
|
11849
|
+
"format": "date-time",
|
|
11850
|
+
"description": "Only include mail at or after this timestamp."
|
|
11851
|
+
},
|
|
11852
|
+
"date_to": {
|
|
11853
|
+
"type": "string",
|
|
11854
|
+
"format": "date-time",
|
|
11855
|
+
"description": "Only include mail at or before this timestamp."
|
|
11856
|
+
},
|
|
11857
|
+
"include": {
|
|
11858
|
+
"type": "array",
|
|
11859
|
+
"items": {
|
|
11860
|
+
"type": "string",
|
|
11861
|
+
"enum": ["coverage"]
|
|
11862
|
+
},
|
|
11863
|
+
"description": "Opt-in extras. `coverage` adds an index-coverage snapshot to\n`meta`. Matched fields, snippets, and the score breakdown are\nalways returned regardless of this field.\n"
|
|
11864
|
+
},
|
|
11865
|
+
"limit": {
|
|
11866
|
+
"type": "integer",
|
|
11867
|
+
"minimum": 1,
|
|
11868
|
+
"maximum": 100,
|
|
11869
|
+
"default": 10,
|
|
11870
|
+
"description": "Maximum number of results to return."
|
|
11871
|
+
},
|
|
11872
|
+
"cursor": {
|
|
11873
|
+
"type": "string",
|
|
11874
|
+
"description": "Opaque pagination cursor from a prior response's `meta.cursor`."
|
|
11875
|
+
}
|
|
11876
|
+
}
|
|
11877
|
+
},
|
|
11878
|
+
"responseSchema": {
|
|
11879
|
+
"type": "array",
|
|
11880
|
+
"items": {
|
|
11881
|
+
"type": "object",
|
|
11882
|
+
"properties": {
|
|
11883
|
+
"source_type": {
|
|
11884
|
+
"type": "string",
|
|
11885
|
+
"enum": ["inbound_email", "sent_email"],
|
|
11886
|
+
"description": "Whether this row is a received or sent message."
|
|
11887
|
+
},
|
|
11888
|
+
"id": {
|
|
11889
|
+
"type": "string",
|
|
11890
|
+
"description": "Message id. Combine with `api_url` to fetch the full record."
|
|
11891
|
+
},
|
|
11892
|
+
"subject": { "type": ["string", "null"] },
|
|
11893
|
+
"from": { "type": ["string", "null"] },
|
|
11894
|
+
"to": { "type": ["string", "null"] },
|
|
11895
|
+
"timestamp": {
|
|
11896
|
+
"type": "string",
|
|
11897
|
+
"description": "Message timestamp (received_at for inbound, created_at for sent)."
|
|
11898
|
+
},
|
|
11899
|
+
"status": {
|
|
11900
|
+
"type": "string",
|
|
11901
|
+
"description": "Lifecycle status of the message."
|
|
11902
|
+
},
|
|
11903
|
+
"score": {
|
|
11904
|
+
"type": "number",
|
|
11905
|
+
"description": "Overall relevance score; the `score_breakdown` components account for it."
|
|
11906
|
+
},
|
|
11907
|
+
"semantic_score": {
|
|
11908
|
+
"type": ["number", "null"],
|
|
11909
|
+
"description": "Raw semantic similarity signal, or null when not applicable."
|
|
11910
|
+
},
|
|
11911
|
+
"keyword_score": {
|
|
11912
|
+
"type": ["number", "null"],
|
|
11913
|
+
"description": "Raw keyword (lexical) signal, or null when not applicable."
|
|
11914
|
+
},
|
|
11915
|
+
"matched_fields": {
|
|
11916
|
+
"type": "array",
|
|
11917
|
+
"items": {
|
|
11918
|
+
"type": "string",
|
|
11919
|
+
"enum": [
|
|
11920
|
+
"subject",
|
|
11921
|
+
"headers",
|
|
11922
|
+
"addresses",
|
|
11923
|
+
"body"
|
|
11924
|
+
],
|
|
11925
|
+
"description": "A searchable email field."
|
|
11926
|
+
},
|
|
11927
|
+
"description": "Fields where the query matched."
|
|
11928
|
+
},
|
|
11929
|
+
"snippets": {
|
|
11930
|
+
"type": "array",
|
|
11931
|
+
"items": {
|
|
11932
|
+
"type": "object",
|
|
11933
|
+
"properties": {
|
|
11934
|
+
"field": {
|
|
11935
|
+
"type": "string",
|
|
11936
|
+
"description": "The field this excerpt came from."
|
|
11937
|
+
},
|
|
11938
|
+
"text": {
|
|
11939
|
+
"type": "string",
|
|
11940
|
+
"description": "Plain-text excerpt centered on the match (no markup)."
|
|
11941
|
+
}
|
|
11942
|
+
},
|
|
11943
|
+
"required": ["field", "text"]
|
|
11944
|
+
},
|
|
11945
|
+
"description": "Match-centered excerpts, one per matched field."
|
|
11946
|
+
},
|
|
11947
|
+
"score_breakdown": {
|
|
11948
|
+
"type": "object",
|
|
11949
|
+
"description": "Additive contributions to `score`. `semantic` and `keyword` are the\nraw signals times the mode's weight (null when not applicable);\nthese plus `field_boost` and `recency` sum to `score` before each\nvalue is independently rounded to 5 decimal places.\n",
|
|
11950
|
+
"properties": {
|
|
11951
|
+
"semantic": { "type": ["number", "null"] },
|
|
11952
|
+
"keyword": { "type": ["number", "null"] },
|
|
11953
|
+
"field_boost": { "type": "number" },
|
|
11954
|
+
"recency": { "type": "number" }
|
|
11955
|
+
},
|
|
11956
|
+
"required": [
|
|
11957
|
+
"semantic",
|
|
11958
|
+
"keyword",
|
|
11959
|
+
"field_boost",
|
|
11960
|
+
"recency"
|
|
11961
|
+
]
|
|
11962
|
+
},
|
|
11963
|
+
"api_url": {
|
|
11964
|
+
"type": ["string", "null"],
|
|
11965
|
+
"description": "Relative API path to fetch the full message."
|
|
11966
|
+
}
|
|
11967
|
+
},
|
|
11968
|
+
"required": [
|
|
11969
|
+
"source_type",
|
|
11970
|
+
"id",
|
|
11971
|
+
"subject",
|
|
11972
|
+
"from",
|
|
11973
|
+
"to",
|
|
11974
|
+
"timestamp",
|
|
11975
|
+
"status",
|
|
11976
|
+
"score",
|
|
11977
|
+
"semantic_score",
|
|
11978
|
+
"keyword_score",
|
|
11979
|
+
"matched_fields",
|
|
11980
|
+
"snippets",
|
|
11981
|
+
"score_breakdown",
|
|
11982
|
+
"api_url"
|
|
11983
|
+
]
|
|
11984
|
+
}
|
|
11985
|
+
},
|
|
11986
|
+
"sdkName": "semanticSearch",
|
|
11987
|
+
"summary": "Semantic search across received and sent mail",
|
|
11988
|
+
"tag": "Search",
|
|
11989
|
+
"tagCommand": "search"
|
|
11990
|
+
},
|
|
11847
11991
|
{
|
|
11848
11992
|
"binaryResponse": false,
|
|
11849
11993
|
"bodyRequired": false,
|
|
@@ -12496,7 +12640,7 @@ const operationManifest = [
|
|
|
12496
12640
|
"binaryResponse": false,
|
|
12497
12641
|
"bodyRequired": true,
|
|
12498
12642
|
"command": "reply-to-email",
|
|
12499
|
-
"description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional
|
|
12643
|
+
"description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody, optional From override, optional attachments, and optional\n`wait` flag; passing any header or recipient override is\nrejected by the schema (`additionalProperties: false`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
|
|
12500
12644
|
"hasJsonBody": true,
|
|
12501
12645
|
"method": "POST",
|
|
12502
12646
|
"operationId": "replyToEmail",
|
|
@@ -12531,6 +12675,36 @@ const operationManifest = [
|
|
|
12531
12675
|
"wait": {
|
|
12532
12676
|
"type": "boolean",
|
|
12533
12677
|
"description": "When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics."
|
|
12678
|
+
},
|
|
12679
|
+
"attachments": {
|
|
12680
|
+
"type": "array",
|
|
12681
|
+
"maxItems": 100,
|
|
12682
|
+
"description": "Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.",
|
|
12683
|
+
"items": {
|
|
12684
|
+
"type": "object",
|
|
12685
|
+
"additionalProperties": false,
|
|
12686
|
+
"properties": {
|
|
12687
|
+
"filename": {
|
|
12688
|
+
"type": "string",
|
|
12689
|
+
"minLength": 1,
|
|
12690
|
+
"maxLength": 255,
|
|
12691
|
+
"description": "Attachment filename. Control characters are rejected."
|
|
12692
|
+
},
|
|
12693
|
+
"content_type": {
|
|
12694
|
+
"type": "string",
|
|
12695
|
+
"minLength": 1,
|
|
12696
|
+
"maxLength": 255,
|
|
12697
|
+
"description": "Optional MIME content type. Control characters are rejected."
|
|
12698
|
+
},
|
|
12699
|
+
"content_base64": {
|
|
12700
|
+
"type": "string",
|
|
12701
|
+
"minLength": 1,
|
|
12702
|
+
"maxLength": 44040192,
|
|
12703
|
+
"description": "Base64-encoded attachment bytes."
|
|
12704
|
+
}
|
|
12705
|
+
},
|
|
12706
|
+
"required": ["filename", "content_base64"]
|
|
12707
|
+
}
|
|
12534
12708
|
}
|
|
12535
12709
|
}
|
|
12536
12710
|
},
|
|
@@ -13093,532 +13267,6 @@ const operationManifest = [
|
|
|
13093
13267
|
}
|
|
13094
13268
|
];
|
|
13095
13269
|
//#endregion
|
|
13096
|
-
//#region ../packages/api-core/src/client.ts
|
|
13097
|
-
/**
|
|
13098
|
-
* Host-aware Primitive API client and shared error type.
|
|
13099
|
-
*
|
|
13100
|
-
* Lives in api-core (instead of sdk-node) so the CLI can build a
|
|
13101
|
-
* configured request client without taking a dependency on sdk-node.
|
|
13102
|
-
* The higher-level `PrimitiveClient` (with `.send`, `.reply`,
|
|
13103
|
-
* `.forward`) still lives in sdk-node because it needs the
|
|
13104
|
-
* `ReceivedEmail` type from the webhook parsing surface.
|
|
13105
|
-
*/
|
|
13106
|
-
const DEFAULT_API_BASE_URL_1 = "https://www.primitive.dev/api/v1";
|
|
13107
|
-
const DEFAULT_API_BASE_URL_2 = "https://api.primitive.dev/v1";
|
|
13108
|
-
function createDefaultAuth(apiKey) {
|
|
13109
|
-
return (security) => {
|
|
13110
|
-
if (security.type === "http" && security.scheme === "bearer") return apiKey;
|
|
13111
|
-
};
|
|
13112
|
-
}
|
|
13113
|
-
var PrimitiveApiClient = class {
|
|
13114
|
-
/**
|
|
13115
|
-
* Generated client targeting the primary API host (apiBaseUrl1). Use
|
|
13116
|
-
* this when passing `client: ...` to a generated operation function
|
|
13117
|
-
* for every endpoint EXCEPT /send-mail. The hand-written
|
|
13118
|
-
* PrimitiveClient.send / .reply / .forward methods on the subclass
|
|
13119
|
-
* route /send-mail to the host-2 client internally.
|
|
13120
|
-
*/
|
|
13121
|
-
client;
|
|
13122
|
-
/**
|
|
13123
|
-
* @internal Generated client targeting the attachments-supporting
|
|
13124
|
-
* send host (apiBaseUrl2). Used by PrimitiveClient.send() under the
|
|
13125
|
-
* hood. Exposed for the CLI's hand-rolled send command, which calls
|
|
13126
|
-
* the generated sendEmail directly; not part of the publicly-
|
|
13127
|
-
* documented SDK surface. Customer code should call .send() on the
|
|
13128
|
-
* subclass instead.
|
|
13129
|
-
*/
|
|
13130
|
-
_sendClient;
|
|
13131
|
-
constructor(options = {}) {
|
|
13132
|
-
const { apiKey, auth, apiBaseUrl1 = DEFAULT_API_BASE_URL_1, apiBaseUrl2 = DEFAULT_API_BASE_URL_2, ...config } = options;
|
|
13133
|
-
const resolvedAuth = auth ?? createDefaultAuth(apiKey);
|
|
13134
|
-
this.client = createClient(createConfig({
|
|
13135
|
-
...config,
|
|
13136
|
-
auth: resolvedAuth,
|
|
13137
|
-
baseUrl: apiBaseUrl1
|
|
13138
|
-
}));
|
|
13139
|
-
this._sendClient = createClient(createConfig({
|
|
13140
|
-
...config,
|
|
13141
|
-
auth: resolvedAuth,
|
|
13142
|
-
baseUrl: apiBaseUrl2
|
|
13143
|
-
}));
|
|
13144
|
-
}
|
|
13145
|
-
getConfig() {
|
|
13146
|
-
return this.client.getConfig();
|
|
13147
|
-
}
|
|
13148
|
-
setConfig(config) {
|
|
13149
|
-
return this.client.setConfig(config);
|
|
13150
|
-
}
|
|
13151
|
-
};
|
|
13152
|
-
//#endregion
|
|
13153
|
-
//#region src/oclif/auth.ts
|
|
13154
|
-
const CREDENTIALS_FILE = "credentials.json";
|
|
13155
|
-
const CREDENTIALS_LOCK_DIR = "credentials.lock";
|
|
13156
|
-
const CREDENTIALS_LOCK_OWNER_FILE = "owner.json";
|
|
13157
|
-
const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
|
|
13158
|
-
const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive signin`.";
|
|
13159
|
-
const CREDENTIALS_LOCK_CLEANUP_SIGNALS = [
|
|
13160
|
-
"SIGINT",
|
|
13161
|
-
"SIGTERM",
|
|
13162
|
-
"SIGHUP"
|
|
13163
|
-
];
|
|
13164
|
-
function isRecord$2(value) {
|
|
13165
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13166
|
-
}
|
|
13167
|
-
function requireString(value, key) {
|
|
13168
|
-
const raw = value[key];
|
|
13169
|
-
if (typeof raw !== "string" || raw.trim().length === 0) throw new Error(`Stored Primitive CLI credentials are malformed: ${key} must be a non-empty string. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
13170
|
-
return raw;
|
|
13171
|
-
}
|
|
13172
|
-
/**
|
|
13173
|
-
* Sentinel returned by parseCredentials when the on-disk credentials were
|
|
13174
|
-
* written by an API-key-based CLI. The caller treats this as "not logged in"
|
|
13175
|
-
* after clearing the local file. The backing API key is intentionally not
|
|
13176
|
-
* revoked; API keys still work when passed explicitly via --api-key/env.
|
|
13177
|
-
*/
|
|
13178
|
-
var LegacyApiKeyCredentialFormatError = class extends Error {
|
|
13179
|
-
constructor() {
|
|
13180
|
-
super("legacy_api_key_credential_format");
|
|
13181
|
-
this.name = "LegacyApiKeyCredentialFormatError";
|
|
13182
|
-
}
|
|
13183
|
-
};
|
|
13184
|
-
function parseCredentials(raw) {
|
|
13185
|
-
if (!isRecord$2(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
13186
|
-
if (raw.auth_method !== "oauth") {
|
|
13187
|
-
if (typeof raw.api_key === "string" || typeof raw.key_id === "string" || typeof raw.base_url === "string") throw new LegacyApiKeyCredentialFormatError();
|
|
13188
|
-
throw new Error(`Stored Primitive CLI credentials are malformed: auth_method must be oauth. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
13189
|
-
}
|
|
13190
|
-
const orgName = raw.org_name;
|
|
13191
|
-
if (orgName !== null && typeof orgName !== "string") throw new Error(`Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
13192
|
-
if (requireString(raw, "token_type") !== "Bearer") throw new Error(`Stored Primitive CLI credentials are malformed: token_type must be Bearer. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
13193
|
-
return {
|
|
13194
|
-
auth_method: "oauth",
|
|
13195
|
-
access_token: requireString(raw, "access_token"),
|
|
13196
|
-
refresh_token: requireString(raw, "refresh_token"),
|
|
13197
|
-
token_type: "Bearer",
|
|
13198
|
-
expires_at: requireString(raw, "expires_at"),
|
|
13199
|
-
oauth_grant_id: requireString(raw, "oauth_grant_id"),
|
|
13200
|
-
oauth_client_id: requireString(raw, "oauth_client_id"),
|
|
13201
|
-
org_id: requireString(raw, "org_id"),
|
|
13202
|
-
org_name: orgName,
|
|
13203
|
-
api_base_url_1: requireString(raw, "api_base_url_1"),
|
|
13204
|
-
created_at: requireString(raw, "created_at")
|
|
13205
|
-
};
|
|
13206
|
-
}
|
|
13207
|
-
function credentialsPath(configDir) {
|
|
13208
|
-
return join(configDir, CREDENTIALS_FILE);
|
|
13209
|
-
}
|
|
13210
|
-
function credentialsLockPath(configDir) {
|
|
13211
|
-
return join(configDir, CREDENTIALS_LOCK_DIR);
|
|
13212
|
-
}
|
|
13213
|
-
function normalize(url, fallback) {
|
|
13214
|
-
const trimmed = url?.trim();
|
|
13215
|
-
if (!trimmed) return fallback;
|
|
13216
|
-
return trimmed.replace(/\/+$/, "");
|
|
13217
|
-
}
|
|
13218
|
-
function normalizeApiBaseUrl1(url) {
|
|
13219
|
-
return normalize(url, DEFAULT_API_BASE_URL_1);
|
|
13220
|
-
}
|
|
13221
|
-
function normalizeApiBaseUrl2(url) {
|
|
13222
|
-
return normalize(url, DEFAULT_API_BASE_URL_2);
|
|
13223
|
-
}
|
|
13224
|
-
function cliAccessTokenExpiresAt(expiresInSeconds, now = Date.now) {
|
|
13225
|
-
return new Date(now() + expiresInSeconds * 1e3).toISOString();
|
|
13226
|
-
}
|
|
13227
|
-
function loadCliCredentials(configDir) {
|
|
13228
|
-
const path = credentialsPath(configDir);
|
|
13229
|
-
let contents;
|
|
13230
|
-
try {
|
|
13231
|
-
contents = readFileSync(path, "utf8");
|
|
13232
|
-
} catch (error) {
|
|
13233
|
-
if (error && typeof error === "object" && error.code === "ENOENT") return null;
|
|
13234
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
13235
|
-
throw new Error(`Could not read Primitive CLI credentials: ${detail}`);
|
|
13236
|
-
}
|
|
13237
|
-
try {
|
|
13238
|
-
return parseCredentials(JSON.parse(contents));
|
|
13239
|
-
} catch (error) {
|
|
13240
|
-
if (error instanceof LegacyApiKeyCredentialFormatError) {
|
|
13241
|
-
try {
|
|
13242
|
-
rmSync(path, { force: true });
|
|
13243
|
-
} catch {}
|
|
13244
|
-
process.stderr.write("Removed local Primitive CLI API-key login state. API keys are still valid when passed explicitly, but saved CLI auth now uses OAuth. Run `primitive signin` to create an OAuth session. No API key was revoked.\n");
|
|
13245
|
-
return null;
|
|
13246
|
-
}
|
|
13247
|
-
if (error instanceof SyntaxError) throw new Error("Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive signin`.");
|
|
13248
|
-
throw error;
|
|
13249
|
-
}
|
|
13250
|
-
}
|
|
13251
|
-
function saveCliCredentials(configDir, credentials) {
|
|
13252
|
-
mkdirSync(configDir, {
|
|
13253
|
-
mode: 448,
|
|
13254
|
-
recursive: true
|
|
13255
|
-
});
|
|
13256
|
-
const path = credentialsPath(configDir);
|
|
13257
|
-
const tempPath = join(configDir, `${CREDENTIALS_FILE}.${process.pid}.${randomUUID()}.tmp`);
|
|
13258
|
-
try {
|
|
13259
|
-
writeFileSync(tempPath, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
13260
|
-
chmodSync(tempPath, 384);
|
|
13261
|
-
renameSync(tempPath, path);
|
|
13262
|
-
chmodSync(path, 384);
|
|
13263
|
-
} catch (error) {
|
|
13264
|
-
rmSync(tempPath, { force: true });
|
|
13265
|
-
throw error;
|
|
13266
|
-
}
|
|
13267
|
-
}
|
|
13268
|
-
function deleteCliCredentials(configDir) {
|
|
13269
|
-
rmSync(credentialsPath(configDir), { force: true });
|
|
13270
|
-
}
|
|
13271
|
-
function deleteCliCredentialsLock(configDir) {
|
|
13272
|
-
rmSync(credentialsLockPath(configDir), {
|
|
13273
|
-
force: true,
|
|
13274
|
-
recursive: true
|
|
13275
|
-
});
|
|
13276
|
-
}
|
|
13277
|
-
function errorCode(error) {
|
|
13278
|
-
return error && typeof error === "object" ? error.code : void 0;
|
|
13279
|
-
}
|
|
13280
|
-
function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
|
|
13281
|
-
try {
|
|
13282
|
-
const stats = statSync(lockPath);
|
|
13283
|
-
if (now() - stats.mtimeMs < staleMs) return false;
|
|
13284
|
-
} catch (error) {
|
|
13285
|
-
if (errorCode(error) === "ENOENT") return true;
|
|
13286
|
-
throw error;
|
|
13287
|
-
}
|
|
13288
|
-
rmSync(lockPath, {
|
|
13289
|
-
force: true,
|
|
13290
|
-
recursive: true
|
|
13291
|
-
});
|
|
13292
|
-
return true;
|
|
13293
|
-
}
|
|
13294
|
-
function readCliCredentialsLockOwner(lockPath) {
|
|
13295
|
-
let raw;
|
|
13296
|
-
try {
|
|
13297
|
-
raw = readFileSync(join(lockPath, CREDENTIALS_LOCK_OWNER_FILE), "utf8");
|
|
13298
|
-
} catch (error) {
|
|
13299
|
-
if (errorCode(error) === "ENOENT") return null;
|
|
13300
|
-
throw error;
|
|
13301
|
-
}
|
|
13302
|
-
try {
|
|
13303
|
-
const pid = JSON.parse(raw)?.pid;
|
|
13304
|
-
return Number.isInteger(pid) && pid > 0 ? { pid } : null;
|
|
13305
|
-
} catch {
|
|
13306
|
-
return null;
|
|
13307
|
-
}
|
|
13308
|
-
}
|
|
13309
|
-
function processIsRunning(pid) {
|
|
13310
|
-
try {
|
|
13311
|
-
process.kill(pid, 0);
|
|
13312
|
-
return true;
|
|
13313
|
-
} catch (error) {
|
|
13314
|
-
if (errorCode(error) === "ESRCH") return false;
|
|
13315
|
-
return true;
|
|
13316
|
-
}
|
|
13317
|
-
}
|
|
13318
|
-
function removeRecoverableCliCredentialsLock(params) {
|
|
13319
|
-
const owner = readCliCredentialsLockOwner(params.lockPath);
|
|
13320
|
-
if (owner && params.isRunning(owner.pid)) return false;
|
|
13321
|
-
if (owner) {
|
|
13322
|
-
rmSync(params.lockPath, {
|
|
13323
|
-
force: true,
|
|
13324
|
-
recursive: true
|
|
13325
|
-
});
|
|
13326
|
-
return true;
|
|
13327
|
-
}
|
|
13328
|
-
return removeStaleCliCredentialsLock(params.lockPath, params.staleMs, params.now);
|
|
13329
|
-
}
|
|
13330
|
-
function writeCliCredentialsLockOwner(lockPath) {
|
|
13331
|
-
const ownerPath = join(lockPath, CREDENTIALS_LOCK_OWNER_FILE);
|
|
13332
|
-
writeFileSync(ownerPath, `${JSON.stringify({
|
|
13333
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13334
|
-
pid: process.pid
|
|
13335
|
-
})}\n`, { mode: 384 });
|
|
13336
|
-
chmodSync(ownerPath, 384);
|
|
13337
|
-
}
|
|
13338
|
-
function installCredentialsLockSignalCleanup(lockPath) {
|
|
13339
|
-
let active = true;
|
|
13340
|
-
const listeners = CREDENTIALS_LOCK_CLEANUP_SIGNALS.map((signal) => {
|
|
13341
|
-
const listener = () => {
|
|
13342
|
-
if (!active) return;
|
|
13343
|
-
active = false;
|
|
13344
|
-
rmSync(lockPath, {
|
|
13345
|
-
force: true,
|
|
13346
|
-
recursive: true
|
|
13347
|
-
});
|
|
13348
|
-
process.exit(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129);
|
|
13349
|
-
};
|
|
13350
|
-
process.once(signal, listener);
|
|
13351
|
-
return {
|
|
13352
|
-
listener,
|
|
13353
|
-
signal
|
|
13354
|
-
};
|
|
13355
|
-
});
|
|
13356
|
-
return () => {
|
|
13357
|
-
if (!active) return;
|
|
13358
|
-
active = false;
|
|
13359
|
-
for (const { listener, signal } of listeners) process.removeListener(signal, listener);
|
|
13360
|
-
};
|
|
13361
|
-
}
|
|
13362
|
-
function credentialsLockInProgressMessage(lockPath) {
|
|
13363
|
-
return `Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry. If no Primitive auth command is still running, run \`primitive logout --force\` to clear local CLI auth state and remove ${lockPath}.`;
|
|
13364
|
-
}
|
|
13365
|
-
function acquireCliCredentialsLock(configDir, options = {}) {
|
|
13366
|
-
mkdirSync(configDir, {
|
|
13367
|
-
mode: 448,
|
|
13368
|
-
recursive: true
|
|
13369
|
-
});
|
|
13370
|
-
const lockPath = credentialsLockPath(configDir);
|
|
13371
|
-
const installSignalHandlers = options.installSignalHandlers ?? true;
|
|
13372
|
-
const isRunning = options.isProcessRunning ?? processIsRunning;
|
|
13373
|
-
const now = options.now ?? Date.now;
|
|
13374
|
-
const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
|
|
13375
|
-
let acquired = false;
|
|
13376
|
-
for (let attempt = 0; attempt < 2; attempt += 1) try {
|
|
13377
|
-
mkdirSync(lockPath, { mode: 448 });
|
|
13378
|
-
acquired = true;
|
|
13379
|
-
break;
|
|
13380
|
-
} catch (error) {
|
|
13381
|
-
if (errorCode(error) !== "EEXIST") throw error;
|
|
13382
|
-
if (removeRecoverableCliCredentialsLock({
|
|
13383
|
-
isRunning,
|
|
13384
|
-
lockPath,
|
|
13385
|
-
now,
|
|
13386
|
-
staleMs
|
|
13387
|
-
})) continue;
|
|
13388
|
-
throw new Error(credentialsLockInProgressMessage(lockPath));
|
|
13389
|
-
}
|
|
13390
|
-
if (!acquired) throw new Error(credentialsLockInProgressMessage(lockPath));
|
|
13391
|
-
try {
|
|
13392
|
-
writeCliCredentialsLockOwner(lockPath);
|
|
13393
|
-
} catch (error) {
|
|
13394
|
-
rmSync(lockPath, {
|
|
13395
|
-
force: true,
|
|
13396
|
-
recursive: true
|
|
13397
|
-
});
|
|
13398
|
-
throw error;
|
|
13399
|
-
}
|
|
13400
|
-
const removeSignalCleanup = installSignalHandlers ? installCredentialsLockSignalCleanup(lockPath) : () => void 0;
|
|
13401
|
-
let released = false;
|
|
13402
|
-
return () => {
|
|
13403
|
-
if (released) return;
|
|
13404
|
-
released = true;
|
|
13405
|
-
removeSignalCleanup();
|
|
13406
|
-
rmSync(lockPath, {
|
|
13407
|
-
force: true,
|
|
13408
|
-
recursive: true
|
|
13409
|
-
});
|
|
13410
|
-
};
|
|
13411
|
-
}
|
|
13412
|
-
function resolveCliAuth(params) {
|
|
13413
|
-
const apiKey = params.apiKey?.trim();
|
|
13414
|
-
const apiBaseUrl2 = normalizeApiBaseUrl2(params.apiBaseUrl2);
|
|
13415
|
-
if (apiKey) return {
|
|
13416
|
-
apiKey,
|
|
13417
|
-
apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
|
|
13418
|
-
apiBaseUrl2,
|
|
13419
|
-
credentials: null,
|
|
13420
|
-
source: "flag-or-env"
|
|
13421
|
-
};
|
|
13422
|
-
const credentials = loadCliCredentials(params.configDir);
|
|
13423
|
-
if (credentials) return {
|
|
13424
|
-
apiKey: credentials.access_token,
|
|
13425
|
-
apiBaseUrl1: credentials.api_base_url_1,
|
|
13426
|
-
apiBaseUrl2,
|
|
13427
|
-
credentials,
|
|
13428
|
-
source: "stored"
|
|
13429
|
-
};
|
|
13430
|
-
return {
|
|
13431
|
-
apiKey: void 0,
|
|
13432
|
-
apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
|
|
13433
|
-
apiBaseUrl2,
|
|
13434
|
-
credentials: null,
|
|
13435
|
-
source: "none"
|
|
13436
|
-
};
|
|
13437
|
-
}
|
|
13438
|
-
//#endregion
|
|
13439
|
-
//#region src/oclif/cli-config.ts
|
|
13440
|
-
const CONFIG_FILE = "config.json";
|
|
13441
|
-
const CONFIG_VERSION = 1;
|
|
13442
|
-
const DEFAULT_ENVIRONMENT = "default";
|
|
13443
|
-
function cliConfigPath(configDir) {
|
|
13444
|
-
return join(configDir, CONFIG_FILE);
|
|
13445
|
-
}
|
|
13446
|
-
function cliConfigError(message) {
|
|
13447
|
-
return new Errors.CLIError(`${message} Run \`primitive config reset\` to clear the local CLI config.`, { exit: 1 });
|
|
13448
|
-
}
|
|
13449
|
-
function isRecord$1(value) {
|
|
13450
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13451
|
-
}
|
|
13452
|
-
function normalizeCliEnvironmentName(name) {
|
|
13453
|
-
const trimmed = name?.trim();
|
|
13454
|
-
if (!trimmed) throw new Errors.CLIError("Environment name must be a non-empty string.", { exit: 1 });
|
|
13455
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/.test(trimmed)) throw new Errors.CLIError("Environment name must start with a letter or number and may only contain letters, numbers, '.', '_', or '-'.", { exit: 1 });
|
|
13456
|
-
return trimmed;
|
|
13457
|
-
}
|
|
13458
|
-
function validateCliHeaderName(name) {
|
|
13459
|
-
const trimmed = name.trim();
|
|
13460
|
-
if (!trimmed) throw new Errors.CLIError("Header name must be a non-empty string.", { exit: 1 });
|
|
13461
|
-
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(trimmed)) throw new Errors.CLIError(`Invalid header name: ${name}`, { exit: 1 });
|
|
13462
|
-
if (trimmed.toLowerCase() === "authorization") throw new Errors.CLIError("The Authorization header is managed by PRIMITIVE_API_KEY or saved OAuth CLI credentials.", { exit: 1 });
|
|
13463
|
-
return trimmed;
|
|
13464
|
-
}
|
|
13465
|
-
function validateCliHeaderValue(value, name) {
|
|
13466
|
-
if (value.length === 0) throw new Errors.CLIError(`Header ${name} value must not be empty.`, { exit: 1 });
|
|
13467
|
-
if (/[\r\n\0]/.test(value)) throw new Errors.CLIError(`Header ${name} value must not contain CR, LF, or NUL characters.`, { exit: 1 });
|
|
13468
|
-
return value;
|
|
13469
|
-
}
|
|
13470
|
-
function parseHeaderAssignment(assignment) {
|
|
13471
|
-
const separator = assignment.indexOf("=");
|
|
13472
|
-
if (separator <= 0) throw new Errors.CLIError("Header values must use name=value syntax, for example `x-custom=secret`.", { exit: 1 });
|
|
13473
|
-
const name = validateCliHeaderName(assignment.slice(0, separator));
|
|
13474
|
-
return [name, validateCliHeaderValue(assignment.slice(separator + 1), name)];
|
|
13475
|
-
}
|
|
13476
|
-
function parseHeaders(raw, context) {
|
|
13477
|
-
if (raw === void 0) return {};
|
|
13478
|
-
if (!isRecord$1(raw)) throw cliConfigError(`${context} headers must be a JSON object.`);
|
|
13479
|
-
const headers = {};
|
|
13480
|
-
for (const [rawName, rawValue] of Object.entries(raw)) {
|
|
13481
|
-
const name = validateCliHeaderName(rawName);
|
|
13482
|
-
if (typeof rawValue !== "string") throw cliConfigError(`${context} header ${name} must be a string.`);
|
|
13483
|
-
headers[name] = validateCliHeaderValue(rawValue, name);
|
|
13484
|
-
}
|
|
13485
|
-
return headers;
|
|
13486
|
-
}
|
|
13487
|
-
function parseEnvironmentConfig(raw, context) {
|
|
13488
|
-
if (!isRecord$1(raw)) throw cliConfigError(`${context} must be a JSON object.`);
|
|
13489
|
-
const env = {};
|
|
13490
|
-
if (raw.api_base_url_1 !== void 0) {
|
|
13491
|
-
if (typeof raw.api_base_url_1 !== "string") throw cliConfigError(`${context}.api_base_url_1 must be a string.`);
|
|
13492
|
-
env.api_base_url_1 = normalizeApiBaseUrl1(raw.api_base_url_1);
|
|
13493
|
-
}
|
|
13494
|
-
if (raw.api_base_url_2 !== void 0) {
|
|
13495
|
-
if (typeof raw.api_base_url_2 !== "string") throw cliConfigError(`${context}.api_base_url_2 must be a string.`);
|
|
13496
|
-
env.api_base_url_2 = normalizeApiBaseUrl2(raw.api_base_url_2);
|
|
13497
|
-
}
|
|
13498
|
-
const headers = parseHeaders(raw.headers, context);
|
|
13499
|
-
if (Object.keys(headers).length > 0) env.headers = headers;
|
|
13500
|
-
return env;
|
|
13501
|
-
}
|
|
13502
|
-
function parseStoredCliConfig(raw) {
|
|
13503
|
-
if (!isRecord$1(raw)) throw cliConfigError("Primitive CLI config must be a JSON object.");
|
|
13504
|
-
if (raw.version !== CONFIG_VERSION) throw cliConfigError(`Primitive CLI config version must be ${CONFIG_VERSION}.`);
|
|
13505
|
-
const currentRaw = raw.current_environment;
|
|
13506
|
-
const current_environment = currentRaw === null || currentRaw === void 0 ? null : typeof currentRaw === "string" ? normalizeCliEnvironmentName(currentRaw) : (() => {
|
|
13507
|
-
throw cliConfigError("Primitive CLI config current_environment must be a string or null.");
|
|
13508
|
-
})();
|
|
13509
|
-
if (!isRecord$1(raw.environments)) throw cliConfigError("Primitive CLI config environments must be an object.");
|
|
13510
|
-
const environments = {};
|
|
13511
|
-
for (const [rawName, rawEnv] of Object.entries(raw.environments)) {
|
|
13512
|
-
const name = normalizeCliEnvironmentName(rawName);
|
|
13513
|
-
environments[name] = parseEnvironmentConfig(rawEnv, `Primitive CLI config environment ${name}`);
|
|
13514
|
-
}
|
|
13515
|
-
if (current_environment && !environments[current_environment]) throw cliConfigError(`Primitive CLI config current environment ${current_environment} does not exist.`);
|
|
13516
|
-
return {
|
|
13517
|
-
version: CONFIG_VERSION,
|
|
13518
|
-
current_environment,
|
|
13519
|
-
environments
|
|
13520
|
-
};
|
|
13521
|
-
}
|
|
13522
|
-
function emptyCliConfig() {
|
|
13523
|
-
return {
|
|
13524
|
-
version: CONFIG_VERSION,
|
|
13525
|
-
current_environment: null,
|
|
13526
|
-
environments: {}
|
|
13527
|
-
};
|
|
13528
|
-
}
|
|
13529
|
-
function loadCliConfig(configDir) {
|
|
13530
|
-
const path = cliConfigPath(configDir);
|
|
13531
|
-
let contents;
|
|
13532
|
-
try {
|
|
13533
|
-
contents = readFileSync(path, "utf8");
|
|
13534
|
-
} catch (error) {
|
|
13535
|
-
if (error && typeof error === "object" && error.code === "ENOENT") return null;
|
|
13536
|
-
throw cliConfigError(`Could not read Primitive CLI config: ${error instanceof Error ? error.message : String(error)}.`);
|
|
13537
|
-
}
|
|
13538
|
-
try {
|
|
13539
|
-
return parseStoredCliConfig(JSON.parse(contents));
|
|
13540
|
-
} catch (error) {
|
|
13541
|
-
if (error instanceof SyntaxError) throw cliConfigError("Primitive CLI config is not valid JSON.");
|
|
13542
|
-
throw error;
|
|
13543
|
-
}
|
|
13544
|
-
}
|
|
13545
|
-
function saveCliConfig(configDir, config) {
|
|
13546
|
-
mkdirSync(configDir, {
|
|
13547
|
-
mode: 448,
|
|
13548
|
-
recursive: true
|
|
13549
|
-
});
|
|
13550
|
-
const path = cliConfigPath(configDir);
|
|
13551
|
-
const tempPath = join(configDir, `${CONFIG_FILE}.${process.pid}.${randomUUID()}.tmp`);
|
|
13552
|
-
try {
|
|
13553
|
-
writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
13554
|
-
renameSync(tempPath, path);
|
|
13555
|
-
} catch (error) {
|
|
13556
|
-
rmSync(tempPath, { force: true });
|
|
13557
|
-
throw error;
|
|
13558
|
-
}
|
|
13559
|
-
}
|
|
13560
|
-
function deleteCliConfig(configDir) {
|
|
13561
|
-
rmSync(cliConfigPath(configDir), { force: true });
|
|
13562
|
-
}
|
|
13563
|
-
function resolveConfigEnvironment(config) {
|
|
13564
|
-
if (!config) return null;
|
|
13565
|
-
const current = config.current_environment;
|
|
13566
|
-
if (current) {
|
|
13567
|
-
const environment = config.environments[current];
|
|
13568
|
-
return environment ? {
|
|
13569
|
-
name: current,
|
|
13570
|
-
config: environment
|
|
13571
|
-
} : null;
|
|
13572
|
-
}
|
|
13573
|
-
const defaultEnvironment = config.environments[DEFAULT_ENVIRONMENT];
|
|
13574
|
-
return defaultEnvironment ? {
|
|
13575
|
-
name: DEFAULT_ENVIRONMENT,
|
|
13576
|
-
config: defaultEnvironment
|
|
13577
|
-
} : null;
|
|
13578
|
-
}
|
|
13579
|
-
function upsertCliEnvironment(params) {
|
|
13580
|
-
const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
|
|
13581
|
-
const existing = params.config.environments[name] ?? {};
|
|
13582
|
-
const nextHeaders = { ...existing.headers ?? {} };
|
|
13583
|
-
for (const assignment of params.headers ?? []) {
|
|
13584
|
-
const [headerName, value] = parseHeaderAssignment(assignment);
|
|
13585
|
-
nextHeaders[headerName] = value;
|
|
13586
|
-
}
|
|
13587
|
-
for (const rawName of params.unsetHeaders ?? []) delete nextHeaders[validateCliHeaderName(rawName)];
|
|
13588
|
-
const nextEnvironment = {
|
|
13589
|
-
...existing,
|
|
13590
|
-
...params.apiBaseUrl1 !== void 0 ? { api_base_url_1: normalizeApiBaseUrl1(params.apiBaseUrl1) } : {},
|
|
13591
|
-
...params.apiBaseUrl2 !== void 0 ? { api_base_url_2: normalizeApiBaseUrl2(params.apiBaseUrl2) } : {},
|
|
13592
|
-
...Object.keys(nextHeaders).length > 0 ? { headers: nextHeaders } : {}
|
|
13593
|
-
};
|
|
13594
|
-
if (Object.keys(nextHeaders).length === 0) delete nextEnvironment.headers;
|
|
13595
|
-
return {
|
|
13596
|
-
...params.config,
|
|
13597
|
-
current_environment: params.use === false ? params.config.current_environment : name,
|
|
13598
|
-
environments: {
|
|
13599
|
-
...params.config.environments,
|
|
13600
|
-
[name]: nextEnvironment
|
|
13601
|
-
}
|
|
13602
|
-
};
|
|
13603
|
-
}
|
|
13604
|
-
function removeCliEnvironment(config, environmentName) {
|
|
13605
|
-
const name = normalizeCliEnvironmentName(environmentName);
|
|
13606
|
-
const environments = { ...config.environments };
|
|
13607
|
-
delete environments[name];
|
|
13608
|
-
return {
|
|
13609
|
-
...config,
|
|
13610
|
-
current_environment: config.current_environment === name ? null : config.current_environment,
|
|
13611
|
-
environments
|
|
13612
|
-
};
|
|
13613
|
-
}
|
|
13614
|
-
function redactCliEnvironment(environment) {
|
|
13615
|
-
const headers = environment.headers && Object.keys(environment.headers).length > 0 ? Object.fromEntries(Object.keys(environment.headers).map((name) => [name, "***"])) : void 0;
|
|
13616
|
-
return {
|
|
13617
|
-
...environment,
|
|
13618
|
-
...headers ? { headers } : {}
|
|
13619
|
-
};
|
|
13620
|
-
}
|
|
13621
|
-
//#endregion
|
|
13622
13270
|
//#region src/oclif/api-client.ts
|
|
13623
13271
|
const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
|
|
13624
13272
|
const OAUTH_REFRESH_SKEW_MS = 60 * 1e3;
|
|
@@ -14163,7 +13811,7 @@ async function runWithTiming(enabled, fn) {
|
|
|
14163
13811
|
const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
|
|
14164
13812
|
const API_BASE_URL_1_FLAG_DESCRIPTION = "Override the primary API base URL. Internal testing only; not documented to customers.";
|
|
14165
13813
|
const API_BASE_URL_2_FLAG_DESCRIPTION = "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.";
|
|
14166
|
-
const HOST_2_OPERATIONS = new Set(["sendEmail"]);
|
|
13814
|
+
const HOST_2_OPERATIONS = new Set(["sendEmail", "replyToEmail"]);
|
|
14167
13815
|
const RESERVED_FLAG_NAMES = new Set([
|
|
14168
13816
|
"api-key",
|
|
14169
13817
|
"api-base-url-1",
|
|
@@ -14394,6 +14042,39 @@ function canonicalizeCliReferences(description) {
|
|
|
14394
14042
|
return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
|
|
14395
14043
|
}
|
|
14396
14044
|
//#endregion
|
|
14045
|
+
//#region src/oclif/attachments.ts
|
|
14046
|
+
function readAttachmentBytes(path, readFile) {
|
|
14047
|
+
try {
|
|
14048
|
+
return Buffer.from(readFile(path));
|
|
14049
|
+
} catch (error) {
|
|
14050
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
14051
|
+
throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
|
|
14052
|
+
}
|
|
14053
|
+
}
|
|
14054
|
+
function hasControlCharacter(value) {
|
|
14055
|
+
return Array.from(value).some((character) => {
|
|
14056
|
+
const code = character.charCodeAt(0);
|
|
14057
|
+
return code <= 31 || code >= 127 && code <= 159;
|
|
14058
|
+
});
|
|
14059
|
+
}
|
|
14060
|
+
function validateAttachmentFilename(path, filename) {
|
|
14061
|
+
if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
|
|
14062
|
+
if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
|
|
14063
|
+
}
|
|
14064
|
+
function readAttachmentFiles(paths, readFile = readFileSync) {
|
|
14065
|
+
if (!paths || paths.length === 0) return void 0;
|
|
14066
|
+
return paths.map((path) => {
|
|
14067
|
+
const filename = basename(path);
|
|
14068
|
+
validateAttachmentFilename(path, filename);
|
|
14069
|
+
const bytes = readAttachmentBytes(path, readFile);
|
|
14070
|
+
if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
|
|
14071
|
+
return {
|
|
14072
|
+
content_base64: bytes.toString("base64"),
|
|
14073
|
+
filename
|
|
14074
|
+
};
|
|
14075
|
+
});
|
|
14076
|
+
}
|
|
14077
|
+
//#endregion
|
|
14397
14078
|
//#region src/oclif/outbound-defaults.ts
|
|
14398
14079
|
const SUBJECT_MAX_LENGTH = 200;
|
|
14399
14080
|
function deriveSubject(body) {
|
|
@@ -14535,19 +14216,39 @@ async function fetchEmailSearchPage(params) {
|
|
|
14535
14216
|
function sleep$1(ms) {
|
|
14536
14217
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14537
14218
|
}
|
|
14538
|
-
//#endregion
|
|
14539
|
-
//#region src/oclif/commands/chat.ts
|
|
14540
|
-
const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
|
|
14541
|
-
const DEFAULT_STRICT_PHASE_SECONDS = 60;
|
|
14542
14219
|
function cliError$6(message) {
|
|
14543
14220
|
return new Errors.CLIError(message, { exit: 1 });
|
|
14544
14221
|
}
|
|
14545
|
-
async function readStdinToString() {
|
|
14546
|
-
if (process.stdin.isTTY) throw cliError$6(
|
|
14222
|
+
async function readStdinToString(missingMessage = "No message provided. Pass the message as the second positional argument or pipe it via stdin.") {
|
|
14223
|
+
if (process.stdin.isTTY) throw cliError$6(missingMessage);
|
|
14547
14224
|
const chunks = [];
|
|
14548
14225
|
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
14549
14226
|
return Buffer.concat(chunks).toString("utf8");
|
|
14550
14227
|
}
|
|
14228
|
+
function chatColor(color, text) {
|
|
14229
|
+
return ux.colorize(color, text);
|
|
14230
|
+
}
|
|
14231
|
+
function chatCommandText(command) {
|
|
14232
|
+
return chatColor("cyan", command);
|
|
14233
|
+
}
|
|
14234
|
+
function chatDetailLine(line) {
|
|
14235
|
+
return chatColor("dim", line);
|
|
14236
|
+
}
|
|
14237
|
+
function chatFailureText(message) {
|
|
14238
|
+
return chatColor("red", message);
|
|
14239
|
+
}
|
|
14240
|
+
function chatHeading(text) {
|
|
14241
|
+
return chatColor("bold", text);
|
|
14242
|
+
}
|
|
14243
|
+
function chatNoticeText(message) {
|
|
14244
|
+
return chatColor("yellow", message);
|
|
14245
|
+
}
|
|
14246
|
+
function chatProgressText(message) {
|
|
14247
|
+
return chatColor("cyan", message);
|
|
14248
|
+
}
|
|
14249
|
+
function chatSuccessText(message) {
|
|
14250
|
+
return chatColor("greenBright", message);
|
|
14251
|
+
}
|
|
14551
14252
|
var ChatProgressIndicator = class {
|
|
14552
14253
|
currentMessage = null;
|
|
14553
14254
|
frameIndex = 0;
|
|
@@ -14568,7 +14269,7 @@ var ChatProgressIndicator = class {
|
|
|
14568
14269
|
this.timer.unref?.();
|
|
14569
14270
|
return;
|
|
14570
14271
|
}
|
|
14571
|
-
this.stream.write(`${message}\n`);
|
|
14272
|
+
this.stream.write(`${chatProgressText(message)}\n`);
|
|
14572
14273
|
}
|
|
14573
14274
|
update(message, options = {}) {
|
|
14574
14275
|
this.currentMessage = message;
|
|
@@ -14581,10 +14282,10 @@ var ChatProgressIndicator = class {
|
|
|
14581
14282
|
return;
|
|
14582
14283
|
}
|
|
14583
14284
|
this.stopTimer();
|
|
14584
|
-
this.stream.write(`${message}\n`);
|
|
14285
|
+
this.stream.write(`${chatProgressText(message)}\n`);
|
|
14585
14286
|
if (options.heartbeatMs !== void 0) {
|
|
14586
14287
|
this.timer = setInterval(() => {
|
|
14587
|
-
this.stream.write(`${formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds)}\n`);
|
|
14288
|
+
this.stream.write(`${chatProgressText(formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds))}\n`);
|
|
14588
14289
|
}, options.heartbeatMs);
|
|
14589
14290
|
this.timer.unref?.();
|
|
14590
14291
|
}
|
|
@@ -14593,17 +14294,17 @@ var ChatProgressIndicator = class {
|
|
|
14593
14294
|
if (this.stream.isTTY) {
|
|
14594
14295
|
const currentMessage = this.currentMessage;
|
|
14595
14296
|
this.clearLine();
|
|
14596
|
-
this.stream.write(`${message}\n`);
|
|
14297
|
+
this.stream.write(`${chatNoticeText(message)}\n`);
|
|
14597
14298
|
if (currentMessage !== null && this.timer !== null) this.render(currentMessage);
|
|
14598
14299
|
return;
|
|
14599
14300
|
}
|
|
14600
|
-
this.stream.write(`${message}\n`);
|
|
14301
|
+
this.stream.write(`${chatNoticeText(message)}\n`);
|
|
14601
14302
|
}
|
|
14602
14303
|
succeed(message) {
|
|
14603
|
-
this.finish(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`);
|
|
14304
|
+
this.finish(chatSuccessText(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`));
|
|
14604
14305
|
}
|
|
14605
14306
|
fail(message) {
|
|
14606
|
-
this.finish(message);
|
|
14307
|
+
this.finish(chatFailureText(message));
|
|
14607
14308
|
}
|
|
14608
14309
|
finish(message) {
|
|
14609
14310
|
this.stopTimer();
|
|
@@ -14620,9 +14321,10 @@ var ChatProgressIndicator = class {
|
|
|
14620
14321
|
];
|
|
14621
14322
|
const frame = frames[this.frameIndex % frames.length];
|
|
14622
14323
|
this.frameIndex += 1;
|
|
14623
|
-
const
|
|
14624
|
-
|
|
14625
|
-
this.
|
|
14324
|
+
const elapsed = `(${formatElapsed(this.now() - this.startedAt)})`;
|
|
14325
|
+
const plainLine = `${frame} ${message} ${elapsed}`;
|
|
14326
|
+
this.lastLineLength = Math.max(this.lastLineLength, plainLine.length);
|
|
14327
|
+
this.stream.write(`\r${chatProgressText(`${frame} ${message}`)} ${chatColor("dim", elapsed)}`);
|
|
14626
14328
|
}
|
|
14627
14329
|
clearLine() {
|
|
14628
14330
|
if (this.lastLineLength > 0) {
|
|
@@ -14655,6 +14357,11 @@ function shellQuote(value) {
|
|
|
14655
14357
|
function commandFromArgv(argv) {
|
|
14656
14358
|
return argv.map(shellQuote).join(" ");
|
|
14657
14359
|
}
|
|
14360
|
+
function parseLocalChatIdArg(value) {
|
|
14361
|
+
if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value)) return null;
|
|
14362
|
+
const parsed = Number(value);
|
|
14363
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
14364
|
+
}
|
|
14658
14365
|
function resolveChatResponseBody(reply) {
|
|
14659
14366
|
if (reply.body_text && reply.body_text.length > 0) return {
|
|
14660
14367
|
body: reply.body_text,
|
|
@@ -14709,10 +14416,35 @@ function buildCommand(kind, description, argv, options = {}) {
|
|
|
14709
14416
|
requires_message: requiresMessage
|
|
14710
14417
|
};
|
|
14711
14418
|
}
|
|
14419
|
+
function shouldPreferStrictContinuation(context) {
|
|
14420
|
+
const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
|
|
14421
|
+
return context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
|
|
14422
|
+
}
|
|
14712
14423
|
function buildChatFollowUpCommands(context) {
|
|
14713
14424
|
const commands = [];
|
|
14714
|
-
const hasCustomStrictPhase = context.strictPhaseSeconds !==
|
|
14715
|
-
const
|
|
14425
|
+
const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
|
|
14426
|
+
const preferStrictContinuation = shouldPreferStrictContinuation(context);
|
|
14427
|
+
if (context.localChatId !== void 0) {
|
|
14428
|
+
const localContinueParts = [
|
|
14429
|
+
"primitive",
|
|
14430
|
+
"chat",
|
|
14431
|
+
"reply",
|
|
14432
|
+
String(context.localChatId),
|
|
14433
|
+
"<message>"
|
|
14434
|
+
];
|
|
14435
|
+
if (context.json) localContinueParts.push("--json");
|
|
14436
|
+
if (context.quiet) localContinueParts.push("--quiet");
|
|
14437
|
+
commands.push(buildCommand("continue_chat", "Continue this chat", localContinueParts, { requiresMessage: true }));
|
|
14438
|
+
const activeContinueParts = [
|
|
14439
|
+
"primitive",
|
|
14440
|
+
"chat",
|
|
14441
|
+
"reply",
|
|
14442
|
+
"<message>"
|
|
14443
|
+
];
|
|
14444
|
+
if (context.json) activeContinueParts.push("--json");
|
|
14445
|
+
if (context.quiet) activeContinueParts.push("--quiet");
|
|
14446
|
+
commands.push(buildCommand("continue_active_chat", "Continue the active chat", activeContinueParts, { requiresMessage: true }));
|
|
14447
|
+
}
|
|
14716
14448
|
const continueParts = [
|
|
14717
14449
|
"primitive",
|
|
14718
14450
|
"chat",
|
|
@@ -14728,9 +14460,9 @@ function buildChatFollowUpCommands(context) {
|
|
|
14728
14460
|
];
|
|
14729
14461
|
if (context.json) continueParts.push("--json");
|
|
14730
14462
|
if (context.quiet) continueParts.push("--quiet");
|
|
14731
|
-
if (
|
|
14463
|
+
if (preferStrictContinuation) continueParts.push("--strict-only");
|
|
14732
14464
|
else if (hasCustomStrictPhase) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
|
|
14733
|
-
commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
|
|
14465
|
+
commands.push(buildCommand(context.localChatId === void 0 ? "continue_chat" : "continue_chat_explicit", context.localChatId === void 0 ? "Continue this chat" : "Continue this chat explicitly", continueParts, { requiresMessage: true }));
|
|
14734
14466
|
commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
|
|
14735
14467
|
"primitive",
|
|
14736
14468
|
"reply",
|
|
@@ -14804,6 +14536,7 @@ function buildChatJsonEnvelope(context) {
|
|
|
14804
14536
|
return {
|
|
14805
14537
|
sent: context.sent,
|
|
14806
14538
|
reply: context.reply,
|
|
14539
|
+
local_chat_id: context.localChatId ?? null,
|
|
14807
14540
|
response_body: responseBody.body,
|
|
14808
14541
|
response_body_format: responseBody.format,
|
|
14809
14542
|
match: {
|
|
@@ -14814,48 +14547,69 @@ function buildChatJsonEnvelope(context) {
|
|
|
14814
14547
|
follow_up_commands: buildChatFollowUpCommands(context)
|
|
14815
14548
|
};
|
|
14816
14549
|
}
|
|
14550
|
+
function persistActiveChat(params) {
|
|
14551
|
+
try {
|
|
14552
|
+
return saveActiveChatState(params.configDir, {
|
|
14553
|
+
from: params.context.from,
|
|
14554
|
+
last_reply_email_id: params.context.reply.id,
|
|
14555
|
+
last_reply_received_at: params.context.reply.received_at,
|
|
14556
|
+
last_sent_email_id: params.context.sent.id,
|
|
14557
|
+
recipient: params.context.recipient,
|
|
14558
|
+
strict_only: shouldPreferStrictContinuation(params.context),
|
|
14559
|
+
strict_phase_seconds: params.context.strictPhaseSeconds,
|
|
14560
|
+
thread_id: params.context.reply.thread_id ?? null,
|
|
14561
|
+
timeout_seconds: params.context.timeoutSeconds
|
|
14562
|
+
}, { preferredLocalId: params.preferredLocalId }).local_id;
|
|
14563
|
+
} catch (error) {
|
|
14564
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
14565
|
+
params.writeWarning?.(`Warning: could not save local chat state: ${detail}\n`);
|
|
14566
|
+
return null;
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14817
14569
|
function formatChatResponse(context) {
|
|
14818
14570
|
const accepted = context.sent.accepted.join(", ") || context.recipient;
|
|
14819
14571
|
const responseBody = resolveChatResponseBody(context.reply);
|
|
14820
14572
|
const lines = [
|
|
14821
|
-
"Reply received",
|
|
14573
|
+
chatSuccessText("Reply received"),
|
|
14822
14574
|
"",
|
|
14823
|
-
"Sent",
|
|
14824
|
-
` To: ${accepted}
|
|
14825
|
-
` From: ${context.sent.from || context.from}
|
|
14826
|
-
` Subject: ${context.subject}
|
|
14827
|
-
` Sent email id: ${context.sent.id}
|
|
14828
|
-
` Delivery status: ${context.sent.delivery_status ?? context.sent.status}
|
|
14575
|
+
chatHeading("Sent"),
|
|
14576
|
+
chatDetailLine(` To: ${accepted}`),
|
|
14577
|
+
chatDetailLine(` From: ${context.sent.from || context.from}`),
|
|
14578
|
+
chatDetailLine(` Subject: ${context.subject}`),
|
|
14579
|
+
chatDetailLine(` Sent email id: ${context.sent.id}`),
|
|
14580
|
+
chatColor("green", ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`),
|
|
14829
14581
|
"",
|
|
14830
|
-
"Reply",
|
|
14831
|
-
` Email id: ${context.reply.id}
|
|
14832
|
-
` From: ${context.reply.from_email}
|
|
14833
|
-
` To: ${context.reply.to_email}
|
|
14834
|
-
` Subject: ${context.reply.subject ?? "(no subject)"}
|
|
14835
|
-
` Received: ${context.reply.received_at}
|
|
14836
|
-
` Match: ${matchDescription(context.matchStrategy)}`
|
|
14582
|
+
chatHeading("Reply"),
|
|
14583
|
+
chatDetailLine(` Email id: ${context.reply.id}`),
|
|
14584
|
+
chatDetailLine(` From: ${context.reply.from_email}`),
|
|
14585
|
+
chatDetailLine(` To: ${context.reply.to_email}`),
|
|
14586
|
+
chatDetailLine(` Subject: ${context.reply.subject ?? "(no subject)"}`),
|
|
14587
|
+
chatDetailLine(` Received: ${context.reply.received_at}`),
|
|
14588
|
+
chatDetailLine(` Match: ${matchDescription(context.matchStrategy)}`)
|
|
14837
14589
|
];
|
|
14838
|
-
if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
|
|
14839
|
-
if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14590
|
+
if (context.reply.reply_to_sent_email_id) lines.push(chatDetailLine(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`));
|
|
14591
|
+
if (context.reply.message_id) lines.push(chatDetailLine(` Message-Id: ${context.reply.message_id}`));
|
|
14592
|
+
if (context.localChatId !== void 0) lines.push(chatDetailLine(` Local chat id: ${context.localChatId}`));
|
|
14593
|
+
lines.push("", chatHeading("Helpful follow-up commands"), chatDetailLine(" Replace <message> before running commands that include it."), chatDetailLine(" Commands are templates; use --json for parse-safe output."), chatDetailLine(" When shown, --strict-only prefers timing out over matching the wrong reply."));
|
|
14594
|
+
for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(chatHeading(` ${description}:`), ` ${chatCommandText(command)}`);
|
|
14595
|
+
lines.push("", chatHeading(`Response body (${responseBody.format}; use --json for parsing)`), "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
|
|
14843
14596
|
return lines.join("\n");
|
|
14844
14597
|
}
|
|
14845
14598
|
function formatChatRecoveryContext(context) {
|
|
14599
|
+
const accepted = context.sent.accepted.join(", ") || context.recipient;
|
|
14846
14600
|
const lines = [
|
|
14847
14601
|
"",
|
|
14848
|
-
"Sent message context",
|
|
14849
|
-
` To: ${
|
|
14850
|
-
` From: ${context.sent.from || context.from}
|
|
14851
|
-
` Subject: ${context.subject}
|
|
14852
|
-
` Sent email id: ${context.sent.id}
|
|
14853
|
-
` Delivery status: ${context.sent.delivery_status ?? context.sent.status}
|
|
14854
|
-
` Poll since: ${context.sentAtIso}
|
|
14602
|
+
chatHeading("Sent message context"),
|
|
14603
|
+
chatDetailLine(` To: ${accepted}`),
|
|
14604
|
+
chatDetailLine(` From: ${context.sent.from || context.from}`),
|
|
14605
|
+
chatDetailLine(` Subject: ${context.subject}`),
|
|
14606
|
+
chatDetailLine(` Sent email id: ${context.sent.id}`),
|
|
14607
|
+
chatColor("green", ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`),
|
|
14608
|
+
chatDetailLine(` Poll since: ${context.sentAtIso}`),
|
|
14855
14609
|
"",
|
|
14856
|
-
"Helpful recovery commands"
|
|
14610
|
+
chatHeading("Helpful recovery commands")
|
|
14857
14611
|
];
|
|
14858
|
-
for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}
|
|
14612
|
+
for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(chatHeading(` ${description}:`), ` ${chatCommandText(command)}`);
|
|
14859
14613
|
return lines.join("\n");
|
|
14860
14614
|
}
|
|
14861
14615
|
async function loadInboundEmailDetail(params) {
|
|
@@ -14926,6 +14680,8 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
14926
14680
|
--reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
|
|
14927
14681
|
reply endpoint, so the reply subject and threading headers are
|
|
14928
14682
|
derived from the inbound email instead of copied into CLI flags.
|
|
14683
|
+
Successful chat turns also save an active local chat, so the next
|
|
14684
|
+
follow-up can be sent with \`primitive chat reply '<message>'\`.
|
|
14929
14685
|
|
|
14930
14686
|
--json emits a structured envelope with both sides of the exchange,
|
|
14931
14687
|
a direct response_body field, match details, and follow-up command
|
|
@@ -14945,8 +14701,11 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
14945
14701
|
static examples = [
|
|
14946
14702
|
"<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
|
|
14947
14703
|
"cat error.log | <%= config.bin %> chat help@agent.acme.dev",
|
|
14704
|
+
"<%= config.bin %> chat reply 'one more thing'",
|
|
14705
|
+
"<%= config.bin %> chat reply 'see attached' --attachment ./report.pdf",
|
|
14948
14706
|
"<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
|
|
14949
14707
|
"<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
|
|
14708
|
+
"<%= config.bin %> chat help@agent.acme.dev 'can you review this?' --attachment ./report.pdf",
|
|
14950
14709
|
"<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
|
|
14951
14710
|
"<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
|
|
14952
14711
|
];
|
|
@@ -14980,15 +14739,25 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
14980
14739
|
reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
|
|
14981
14740
|
"reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
|
|
14982
14741
|
"in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
|
|
14742
|
+
attachment: Flags.string({
|
|
14743
|
+
char: "a",
|
|
14744
|
+
description: "Attach a file to this chat message. Repeat --attachment to attach multiple files.",
|
|
14745
|
+
multiple: true
|
|
14746
|
+
}),
|
|
14747
|
+
"chat-local-id": Flags.integer({
|
|
14748
|
+
description: "Local chat id to update after this command succeeds. Internal plumbing for `primitive chat reply`.",
|
|
14749
|
+
hidden: true,
|
|
14750
|
+
min: 0
|
|
14751
|
+
}),
|
|
14983
14752
|
json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
|
|
14984
14753
|
quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
|
|
14985
14754
|
timeout: Flags.integer({
|
|
14986
|
-
default:
|
|
14755
|
+
default: 120,
|
|
14987
14756
|
description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
|
|
14988
14757
|
min: 0
|
|
14989
14758
|
}),
|
|
14990
14759
|
"strict-phase-seconds": Flags.integer({
|
|
14991
|
-
default:
|
|
14760
|
+
default: 60,
|
|
14992
14761
|
description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
|
|
14993
14762
|
min: 1
|
|
14994
14763
|
}),
|
|
@@ -15028,6 +14797,7 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
15028
14797
|
configDir: this.config.configDir
|
|
15029
14798
|
};
|
|
15030
14799
|
const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
|
|
14800
|
+
const attachments = readAttachmentFiles(flags.attachment);
|
|
15031
14801
|
let from;
|
|
15032
14802
|
let parentReply;
|
|
15033
14803
|
let subject;
|
|
@@ -15086,9 +14856,10 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
15086
14856
|
const sendResult = parentReply !== void 0 ? await replyToEmail({
|
|
15087
14857
|
body: {
|
|
15088
14858
|
body_text: message,
|
|
15089
|
-
from
|
|
14859
|
+
from,
|
|
14860
|
+
...attachments !== void 0 ? { attachments } : {}
|
|
15090
14861
|
},
|
|
15091
|
-
client: apiClient.
|
|
14862
|
+
client: apiClient._sendClient,
|
|
15092
14863
|
path: { id: parentReply.id },
|
|
15093
14864
|
responseStyle: "fields"
|
|
15094
14865
|
}) : await sendEmail({
|
|
@@ -15097,7 +14868,8 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
15097
14868
|
to: args.recipient,
|
|
15098
14869
|
subject,
|
|
15099
14870
|
body_text: message,
|
|
15100
|
-
...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {}
|
|
14871
|
+
...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
|
|
14872
|
+
...attachments !== void 0 ? { attachments } : {}
|
|
15101
14873
|
},
|
|
15102
14874
|
client: apiClient._sendClient,
|
|
15103
14875
|
responseStyle: "fields"
|
|
@@ -15172,16 +14944,140 @@ var ChatCommand = class ChatCommand extends Command {
|
|
|
15172
14944
|
return;
|
|
15173
14945
|
}
|
|
15174
14946
|
progress?.succeed(`Reply received from ${replyResult.reply.from_email}`);
|
|
15175
|
-
|
|
14947
|
+
let outputContext = {
|
|
15176
14948
|
...baseContext,
|
|
15177
14949
|
matchStrategy: replyResult.matchStrategy,
|
|
15178
14950
|
reply: replyResult.reply
|
|
15179
14951
|
};
|
|
14952
|
+
const localChatId = persistActiveChat({
|
|
14953
|
+
configDir: this.config.configDir,
|
|
14954
|
+
context: outputContext,
|
|
14955
|
+
preferredLocalId: flags["chat-local-id"],
|
|
14956
|
+
writeWarning: (message) => process.stderr.write(message)
|
|
14957
|
+
});
|
|
14958
|
+
if (localChatId !== null) outputContext = {
|
|
14959
|
+
...outputContext,
|
|
14960
|
+
localChatId
|
|
14961
|
+
};
|
|
15180
14962
|
if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
|
|
15181
14963
|
else this.log(formatChatResponse(outputContext));
|
|
15182
14964
|
});
|
|
15183
14965
|
}
|
|
15184
14966
|
};
|
|
14967
|
+
var ChatReplyCommand = class ChatReplyCommand extends Command {
|
|
14968
|
+
static description = `Reply in the active chat.
|
|
14969
|
+
|
|
14970
|
+
A successful \`primitive chat <email> <message>\` saves the latest
|
|
14971
|
+
inbound reply as a local chat and makes it active. Use
|
|
14972
|
+
\`primitive chat reply <message>\` for the active chat, or
|
|
14973
|
+
\`primitive chat reply <local-id> <message>\` / \`--id <local-id>\`
|
|
14974
|
+
for a specific local chat. The command uses Primitive's real reply
|
|
14975
|
+
endpoint against the stored inbound email id, so the recipient,
|
|
14976
|
+
subject, and threading headers are derived server-side from the
|
|
14977
|
+
thread.
|
|
14978
|
+
|
|
14979
|
+
If no chat is open, start one with \`primitive chat <email> '<message>'\`.
|
|
14980
|
+
For explicit control, use \`primitive chat <email> --reply '<message>'
|
|
14981
|
+
--reply-to-email-id <inbound-email-id>\`.`;
|
|
14982
|
+
static summary = "Reply in the active chat";
|
|
14983
|
+
static examples = [
|
|
14984
|
+
"<%= config.bin %> chat reply 'one more thing'",
|
|
14985
|
+
"<%= config.bin %> chat reply 0 'one more thing'",
|
|
14986
|
+
"<%= config.bin %> chat reply --id 0 'one more thing'",
|
|
14987
|
+
"<%= config.bin %> chat reply 'see attached' --attachment ./report.pdf",
|
|
14988
|
+
"cat follow-up.txt | <%= config.bin %> chat reply"
|
|
14989
|
+
];
|
|
14990
|
+
static args = {
|
|
14991
|
+
idOrMessage: Args.string({ description: "Reply body, or a local chat id when followed by a separate message." }),
|
|
14992
|
+
message: Args.string({ description: "Reply body when the first positional argument is an id." })
|
|
14993
|
+
};
|
|
14994
|
+
static flags = {
|
|
14995
|
+
"api-key": Flags.string({
|
|
14996
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive signin` credentials)",
|
|
14997
|
+
env: "PRIMITIVE_API_KEY"
|
|
14998
|
+
}),
|
|
14999
|
+
"api-base-url-1": Flags.string({
|
|
15000
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
15001
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
15002
|
+
hidden: true
|
|
15003
|
+
}),
|
|
15004
|
+
"api-base-url-2": Flags.string({
|
|
15005
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
15006
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
15007
|
+
hidden: true
|
|
15008
|
+
}),
|
|
15009
|
+
id: Flags.integer({
|
|
15010
|
+
description: "Local chat id to reply in. Omit to use the most recent active chat.",
|
|
15011
|
+
min: 0
|
|
15012
|
+
}),
|
|
15013
|
+
json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
|
|
15014
|
+
quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
|
|
15015
|
+
timeout: Flags.integer({
|
|
15016
|
+
description: "Seconds to wait for a reply before exiting non-zero. Defaults to the active chat's last timeout.",
|
|
15017
|
+
min: 0
|
|
15018
|
+
}),
|
|
15019
|
+
"strict-phase-seconds": Flags.integer({
|
|
15020
|
+
description: "Seconds to wait in strict-threading mode before falling back. Defaults to the active chat's last setting.",
|
|
15021
|
+
min: 1
|
|
15022
|
+
}),
|
|
15023
|
+
"strict-only": Flags.boolean({ description: "Disable the time-window fallback. If the active chat was saved from a strict match, this is already the default." }),
|
|
15024
|
+
attachment: Flags.string({
|
|
15025
|
+
char: "a",
|
|
15026
|
+
description: "Attach a file to the reply. Repeat --attachment to attach multiple files.",
|
|
15027
|
+
multiple: true
|
|
15028
|
+
}),
|
|
15029
|
+
interval: Flags.integer({
|
|
15030
|
+
description: "Seconds between polls while waiting for the reply.",
|
|
15031
|
+
min: 1
|
|
15032
|
+
}),
|
|
15033
|
+
"page-size": Flags.integer({
|
|
15034
|
+
description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
|
|
15035
|
+
max: 100,
|
|
15036
|
+
min: 1,
|
|
15037
|
+
hidden: true
|
|
15038
|
+
}),
|
|
15039
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
15040
|
+
};
|
|
15041
|
+
async run() {
|
|
15042
|
+
const { args, flags } = await this.parse(ChatReplyCommand);
|
|
15043
|
+
const positionalLocalId = flags.id === void 0 && args.message !== void 0 ? parseLocalChatIdArg(args.idOrMessage) : void 0;
|
|
15044
|
+
if (flags.id === void 0 && args.message !== void 0 && positionalLocalId === null) throw cliError$6("When passing two positional arguments to `primitive chat reply`, the first must be a local chat id. Use `primitive chat reply '<message>'` for the active chat or `primitive chat reply --id <id> '<message>'` for a specific chat.");
|
|
15045
|
+
if (flags.id !== void 0 && args.message !== void 0) throw cliError$6("With --id, pass the reply body as a single positional argument or pipe it via stdin.");
|
|
15046
|
+
const localId = flags.id ?? (typeof positionalLocalId === "number" ? positionalLocalId : void 0);
|
|
15047
|
+
const state = localId === void 0 ? loadActiveChatState(this.config.configDir) : loadChatConversationByLocalId(this.config.configDir, localId);
|
|
15048
|
+
if (!state) throw cliError$6(localId === void 0 ? "No open chat. Start one with `primitive chat <email> '<message>'`." : `No local chat ${localId}. Start one with \`primitive chat <email> '<message>'\` or omit --id to use the active chat.`);
|
|
15049
|
+
const message = args.message !== void 0 ? args.message : args.idOrMessage !== void 0 && args.idOrMessage !== "" ? args.idOrMessage : await readStdinToString("No reply body provided. Pass the reply body as a positional argument or pipe it via stdin.");
|
|
15050
|
+
if (!message.trim()) throw cliError$6("Reply body is empty.");
|
|
15051
|
+
const argv = [
|
|
15052
|
+
state.recipient,
|
|
15053
|
+
"--reply",
|
|
15054
|
+
message,
|
|
15055
|
+
"--from",
|
|
15056
|
+
state.from,
|
|
15057
|
+
"--reply-to-email-id",
|
|
15058
|
+
state.last_reply_email_id,
|
|
15059
|
+
"--timeout",
|
|
15060
|
+
String(flags.timeout ?? state.timeout_seconds),
|
|
15061
|
+
"--strict-phase-seconds",
|
|
15062
|
+
String(flags["strict-phase-seconds"] ?? state.strict_phase_seconds),
|
|
15063
|
+
"--interval",
|
|
15064
|
+
String(flags.interval ?? 2),
|
|
15065
|
+
"--page-size",
|
|
15066
|
+
String(flags["page-size"] ?? 50),
|
|
15067
|
+
"--chat-local-id",
|
|
15068
|
+
String(state.local_id)
|
|
15069
|
+
];
|
|
15070
|
+
if (flags["api-key"] !== void 0) argv.push("--api-key", flags["api-key"]);
|
|
15071
|
+
if (flags["api-base-url-1"] !== void 0) argv.push("--api-base-url-1", flags["api-base-url-1"]);
|
|
15072
|
+
if (flags["api-base-url-2"] !== void 0) argv.push("--api-base-url-2", flags["api-base-url-2"]);
|
|
15073
|
+
if (flags.json) argv.push("--json");
|
|
15074
|
+
if (flags.quiet) argv.push("--quiet");
|
|
15075
|
+
for (const attachment of flags.attachment ?? []) argv.push("--attachment", attachment);
|
|
15076
|
+
if (state.strict_only || flags["strict-only"]) argv.push("--strict-only");
|
|
15077
|
+
if (flags.time) argv.push("--time");
|
|
15078
|
+
await ChatCommand.run(argv, { root: this.config.root });
|
|
15079
|
+
}
|
|
15080
|
+
};
|
|
15185
15081
|
async function waitForReply(params) {
|
|
15186
15082
|
const notice = params.notice ?? ((message) => {
|
|
15187
15083
|
process.stderr.write(`${message}\n`);
|
|
@@ -17186,8 +17082,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
|
|
|
17186
17082
|
name: "Primitive Team",
|
|
17187
17083
|
url: "https://primitive.dev"
|
|
17188
17084
|
};
|
|
17189
|
-
const SDK_VERSION_RANGE = "^0.
|
|
17190
|
-
const CLI_VERSION_RANGE = "^0.
|
|
17085
|
+
const SDK_VERSION_RANGE = "^0.35.0";
|
|
17086
|
+
const CLI_VERSION_RANGE = "^0.35.0";
|
|
17191
17087
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
17192
17088
|
function renderHandler() {
|
|
17193
17089
|
return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
|
|
@@ -18560,6 +18456,7 @@ const DOMAIN_DISPLAY_WIDTH = 34;
|
|
|
18560
18456
|
const STATUS_DISPLAY_WIDTH = 12;
|
|
18561
18457
|
const BOOL_DISPLAY_WIDTH = 7;
|
|
18562
18458
|
const NUM_DISPLAY_WIDTH = 6;
|
|
18459
|
+
const DEFAULT_PRIMITIVE_LOCAL_PART = "agent";
|
|
18563
18460
|
function plural(count, singular, pluralValue = `${singular}s`) {
|
|
18564
18461
|
return `${count} ${count === 1 ? singular : pluralValue}`;
|
|
18565
18462
|
}
|
|
@@ -18595,6 +18492,14 @@ function domainSummary(domain) {
|
|
|
18595
18492
|
default: return `${domain.domain} has status ${String(domain.status)}.`;
|
|
18596
18493
|
}
|
|
18597
18494
|
}
|
|
18495
|
+
function findSuggestedPrimitiveAddress(domains) {
|
|
18496
|
+
const domain = domains.find((entry) => entry.managed && entry.active && entry.receiving_ready);
|
|
18497
|
+
if (!domain) return null;
|
|
18498
|
+
return {
|
|
18499
|
+
address: `${DEFAULT_PRIMITIVE_LOCAL_PART}@${domain.domain}`,
|
|
18500
|
+
domain: domain.domain
|
|
18501
|
+
};
|
|
18502
|
+
}
|
|
18598
18503
|
function focusInboxStatus(status, domainName) {
|
|
18599
18504
|
const normalized = domainName.toLowerCase();
|
|
18600
18505
|
const domain = status.domains.find((entry) => entry.domain.toLowerCase() === normalized);
|
|
@@ -18641,12 +18546,14 @@ function formatInboxStatus(status) {
|
|
|
18641
18546
|
"",
|
|
18642
18547
|
"Domains"
|
|
18643
18548
|
];
|
|
18549
|
+
const suggestedAddress = findSuggestedPrimitiveAddress(status.domains);
|
|
18644
18550
|
if (status.domains.length === 0) lines.push("No domains configured.");
|
|
18645
18551
|
else {
|
|
18646
18552
|
lines.push(formatDomainHeader());
|
|
18647
18553
|
for (const domain of status.domains) lines.push(formatDomainRow(domain));
|
|
18648
18554
|
}
|
|
18649
18555
|
lines.push("", `Endpoints: ${status.endpoints.enabled}/${status.endpoints.total} enabled (${status.endpoints.fallback_enabled} fallback, ${status.endpoints.domain_scoped_enabled} domain-scoped, ${status.endpoints.function_enabled} function)`, `Functions: ${status.functions.deployed}/${status.functions.total} deployed (${status.functions.pending} pending, ${status.functions.failed} failed)`, `Recent inbound: ${plural(status.recent_emails.total, "email")} latest ${formatInboxDate(status.recent_emails.latest_received_at)}`);
|
|
18556
|
+
if (suggestedAddress) lines.push("", `Primitive address: ${suggestedAddress.address}`, ` Any local-part at ${suggestedAddress.domain} can receive mail.`, ` Try: primitive send --to ${suggestedAddress.address} --subject "hello" --body "test"`);
|
|
18650
18557
|
if (status.next_actions.length > 0) {
|
|
18651
18558
|
lines.push("", "Next actions");
|
|
18652
18559
|
for (const action of status.next_actions) lines.push(formatNextAction(action));
|
|
@@ -18723,6 +18630,180 @@ var InboxStatusCommand = class InboxStatusCommand extends Command {
|
|
|
18723
18630
|
}
|
|
18724
18631
|
};
|
|
18725
18632
|
//#endregion
|
|
18633
|
+
//#region src/oclif/commands/inbox-setup.ts
|
|
18634
|
+
const DEFAULT_FUNCTION_NAME = "inbound-reply";
|
|
18635
|
+
const DEFAULT_LOCAL_PART = "inbox";
|
|
18636
|
+
const FUNCTION_ID_PLACEHOLDER = "<function-id>";
|
|
18637
|
+
function firstUsableManagedDomain(status) {
|
|
18638
|
+
return status.domains.find((domain) => domain.managed && domain.receiving_ready && domain.active) ?? status.domains.find((domain) => domain.managed && domain.receiving_ready) ?? null;
|
|
18639
|
+
}
|
|
18640
|
+
function buildInboxSetupCommands(functionName = DEFAULT_FUNCTION_NAME) {
|
|
18641
|
+
return {
|
|
18642
|
+
scaffold: [
|
|
18643
|
+
`primitive functions init ${functionName}`,
|
|
18644
|
+
`cd ${functionName}`,
|
|
18645
|
+
"npm install",
|
|
18646
|
+
"npm run build",
|
|
18647
|
+
`primitive functions deploy --name ${functionName} --file ./dist/handler.js --wait`,
|
|
18648
|
+
`primitive functions test --id ${FUNCTION_ID_PLACEHOLDER} --wait --show-sends`
|
|
18649
|
+
],
|
|
18650
|
+
logs: `primitive functions logs --id ${FUNCTION_ID_PLACEHOLDER}`,
|
|
18651
|
+
status: "primitive inbox status"
|
|
18652
|
+
};
|
|
18653
|
+
}
|
|
18654
|
+
function buildInboxSetupProof(commands) {
|
|
18655
|
+
return {
|
|
18656
|
+
after_test: [
|
|
18657
|
+
"inbound id for the generated test email",
|
|
18658
|
+
"function id matching the deployed Function",
|
|
18659
|
+
"invocation status completed, failed, or send_failed",
|
|
18660
|
+
"reply/send result emitted by the handler"
|
|
18661
|
+
],
|
|
18662
|
+
logs_command: commands.logs
|
|
18663
|
+
};
|
|
18664
|
+
}
|
|
18665
|
+
function buildInboxSetupGuide(status) {
|
|
18666
|
+
const domain = firstUsableManagedDomain(status);
|
|
18667
|
+
const commands = buildInboxSetupCommands();
|
|
18668
|
+
const mode = !status.receiving_ready ? "not_receiving" : status.processing_ready ? "actively_processed" : "stored_only";
|
|
18669
|
+
return {
|
|
18670
|
+
readiness: {
|
|
18671
|
+
ready: status.ready,
|
|
18672
|
+
receiving_ready: status.receiving_ready,
|
|
18673
|
+
processing_ready: status.processing_ready,
|
|
18674
|
+
mode,
|
|
18675
|
+
summary: status.summary
|
|
18676
|
+
},
|
|
18677
|
+
receive: {
|
|
18678
|
+
address: domain ? `${DEFAULT_LOCAL_PART}@${domain.domain}` : null,
|
|
18679
|
+
domain: domain?.domain ?? null,
|
|
18680
|
+
managed: domain?.managed ?? false,
|
|
18681
|
+
placeholder_local_part: domain ? DEFAULT_LOCAL_PART : null
|
|
18682
|
+
},
|
|
18683
|
+
processing: {
|
|
18684
|
+
stored_only: status.receiving_ready && !status.processing_ready,
|
|
18685
|
+
active: status.processing_ready,
|
|
18686
|
+
enabled_endpoints: status.endpoints.enabled,
|
|
18687
|
+
deployed_functions: status.functions.deployed
|
|
18688
|
+
},
|
|
18689
|
+
commands,
|
|
18690
|
+
proof: buildInboxSetupProof(commands),
|
|
18691
|
+
status
|
|
18692
|
+
};
|
|
18693
|
+
}
|
|
18694
|
+
function formatReadiness(guide) {
|
|
18695
|
+
const readiness = guide.readiness.ready ? "ready" : "not ready";
|
|
18696
|
+
const receiving = guide.readiness.receiving_ready ? "yes" : "no";
|
|
18697
|
+
const processing = guide.readiness.processing_ready ? "yes" : "no";
|
|
18698
|
+
const mode = guide.readiness.mode === "actively_processed" ? "actively processed" : guide.readiness.mode === "stored_only" ? "stored-only" : "not receiving";
|
|
18699
|
+
return [
|
|
18700
|
+
`Readiness: ${readiness}`,
|
|
18701
|
+
`Receiving: ${receiving}`,
|
|
18702
|
+
`Processing: ${processing}`,
|
|
18703
|
+
`Mode: ${mode}`
|
|
18704
|
+
].join("\n");
|
|
18705
|
+
}
|
|
18706
|
+
function formatReceiveAddress(guide) {
|
|
18707
|
+
if (!guide.receive.domain || !guide.receive.address) return "Receive address: none found on a receiving-ready Primitive-managed domain";
|
|
18708
|
+
return [`Receive address: ${guide.receive.address}`, `Receive domain: ${guide.receive.domain} (Primitive-managed)`].join("\n");
|
|
18709
|
+
}
|
|
18710
|
+
function formatDomainDetails(status) {
|
|
18711
|
+
if (status.domains.length === 0) return ["Domains: none configured"];
|
|
18712
|
+
return status.domains.map((domain) => `- ${domain.domain}: ${statusText(domain.status)}, receive ${domain.receiving_ready ? "yes" : "no"}, process ${domain.processing_ready ? "yes" : "no"}, routes ${domain.processing_route_count}`);
|
|
18713
|
+
}
|
|
18714
|
+
function formatScaffoldCommands(commands) {
|
|
18715
|
+
return commands.scaffold.map((command) => ` ${command}`);
|
|
18716
|
+
}
|
|
18717
|
+
function formatInboxSetupGuide(guide) {
|
|
18718
|
+
const lines = [
|
|
18719
|
+
"Inbound setup",
|
|
18720
|
+
"",
|
|
18721
|
+
guide.readiness.summary,
|
|
18722
|
+
"",
|
|
18723
|
+
formatReadiness(guide),
|
|
18724
|
+
"",
|
|
18725
|
+
formatReceiveAddress(guide),
|
|
18726
|
+
"",
|
|
18727
|
+
"Domains",
|
|
18728
|
+
...formatDomainDetails(guide.status),
|
|
18729
|
+
"",
|
|
18730
|
+
`Processing routes: ${guide.processing.enabled_endpoints} enabled endpoint(s), ${guide.processing.deployed_functions} deployed Function(s)`
|
|
18731
|
+
];
|
|
18732
|
+
if (guide.readiness.mode === "not_receiving") lines.push("", "Next actions", "Make a receiving-ready domain available, then re-run:", ` ${guide.commands.status}`);
|
|
18733
|
+
else if (!guide.processing.active) lines.push("", "Next actions", "No processing route is enabled. Scaffold, deploy, and test an email Function:", ...formatScaffoldCommands(guide.commands));
|
|
18734
|
+
else lines.push("", "Next actions", "Inbound mail has an active processing route. Run a Function test when you know the Function id:", ` primitive functions test --id ${FUNCTION_ID_PLACEHOLDER} --wait --show-sends`);
|
|
18735
|
+
if (guide.status.next_actions.length > 0) {
|
|
18736
|
+
lines.push("", "API suggested actions");
|
|
18737
|
+
for (const action of guide.status.next_actions) lines.push(action.command ? `- ${action.message}\n ${action.command}` : `- ${action.message}`);
|
|
18738
|
+
}
|
|
18739
|
+
lines.push("", "Proof after functions test", "- Inbound id: the generated test email should have an inbound id.", "- Function id: the run should point at the Function id you deployed.", "- Invocation status: expect completed; failed or send_failed identifies the failing stage.", "- Reply/send result: --show-sends should show the handler's outbound result when it replies or sends.", "- Logs:", ` ${guide.proof.logs_command}`);
|
|
18740
|
+
return lines.join("\n");
|
|
18741
|
+
}
|
|
18742
|
+
var InboxSetupCommand = class InboxSetupCommand extends Command {
|
|
18743
|
+
static description = `Guide inbound email setup from the server-owned inbox status API.
|
|
18744
|
+
|
|
18745
|
+
This command does not scaffold, deploy, or run tests. It verifies auth, fetches inbox readiness, shows the first usable Primitive-managed receive address/domain, explains whether inbound mail is stored-only or actively processed, and prints the exact commands to add a Function processing route when one is missing.`;
|
|
18746
|
+
static summary = "Guide inbound email setup";
|
|
18747
|
+
static examples = ["<%= config.bin %> inbox setup", "<%= config.bin %> inbox setup --json"];
|
|
18748
|
+
static flags = {
|
|
18749
|
+
"api-key": Flags.string({
|
|
18750
|
+
description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
|
|
18751
|
+
env: "PRIMITIVE_API_KEY"
|
|
18752
|
+
}),
|
|
18753
|
+
"api-base-url-1": Flags.string({
|
|
18754
|
+
description: API_BASE_URL_1_FLAG_DESCRIPTION,
|
|
18755
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
18756
|
+
hidden: true
|
|
18757
|
+
}),
|
|
18758
|
+
"api-base-url-2": Flags.string({
|
|
18759
|
+
description: API_BASE_URL_2_FLAG_DESCRIPTION,
|
|
18760
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
18761
|
+
hidden: true
|
|
18762
|
+
}),
|
|
18763
|
+
json: Flags.boolean({ description: "Print structured readiness, receive address, commands, proof metadata, and raw status as JSON." }),
|
|
18764
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
18765
|
+
};
|
|
18766
|
+
async run() {
|
|
18767
|
+
const { flags } = await this.parse(InboxSetupCommand);
|
|
18768
|
+
await runWithTiming(flags.time, async () => {
|
|
18769
|
+
const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
|
|
18770
|
+
apiKey: flags["api-key"],
|
|
18771
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
18772
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
18773
|
+
configDir: this.config.configDir
|
|
18774
|
+
});
|
|
18775
|
+
const result = await getInboxStatus({
|
|
18776
|
+
client: apiClient.client,
|
|
18777
|
+
responseStyle: "fields"
|
|
18778
|
+
});
|
|
18779
|
+
if (result.error) {
|
|
18780
|
+
const errorPayload = extractErrorPayload(result.error);
|
|
18781
|
+
writeErrorWithHints(errorPayload);
|
|
18782
|
+
surfaceUnauthorizedHint({
|
|
18783
|
+
auth,
|
|
18784
|
+
baseUrlOverridden,
|
|
18785
|
+
configDir: this.config.configDir,
|
|
18786
|
+
payload: errorPayload
|
|
18787
|
+
});
|
|
18788
|
+
process.exitCode = 1;
|
|
18789
|
+
return;
|
|
18790
|
+
}
|
|
18791
|
+
const envelope = result.data ?? {};
|
|
18792
|
+
const status = envelope.data;
|
|
18793
|
+
if (!status) throw new Errors.CLIError("Primitive API returned no inbox status.", { exit: 1 });
|
|
18794
|
+
const guide = buildInboxSetupGuide(status);
|
|
18795
|
+
if (flags.json) {
|
|
18796
|
+
this.log(JSON.stringify({
|
|
18797
|
+
...envelope,
|
|
18798
|
+
data: guide
|
|
18799
|
+
}, null, 2));
|
|
18800
|
+
return;
|
|
18801
|
+
}
|
|
18802
|
+
this.log(formatInboxSetupGuide(guide));
|
|
18803
|
+
});
|
|
18804
|
+
}
|
|
18805
|
+
};
|
|
18806
|
+
//#endregion
|
|
18726
18807
|
//#region src/oclif/commands/login.ts
|
|
18727
18808
|
const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
|
|
18728
18809
|
function cliError$3(message) {
|
|
@@ -18904,6 +18985,7 @@ var LoginCommand$1 = class extends Command {
|
|
|
18904
18985
|
if (polled.data) {
|
|
18905
18986
|
const login = unwrapData$2(polled.data);
|
|
18906
18987
|
if (!login) throw cliError$3("Primitive API returned an empty CLI poll response.");
|
|
18988
|
+
deleteChatState(this.config.configDir);
|
|
18907
18989
|
saveCliCredentials(this.config.configDir, {
|
|
18908
18990
|
access_token: login.access_token,
|
|
18909
18991
|
api_base_url_1: apiBaseUrl1,
|
|
@@ -19044,11 +19126,100 @@ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
|
|
|
19044
19126
|
expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
|
|
19045
19127
|
};
|
|
19046
19128
|
}
|
|
19129
|
+
function readPendingAgentSignupState(configDir, apiBaseUrl1) {
|
|
19130
|
+
const path = pendingSignupPath(configDir);
|
|
19131
|
+
let contents;
|
|
19132
|
+
try {
|
|
19133
|
+
contents = readFileSync(path, "utf8");
|
|
19134
|
+
} catch (error) {
|
|
19135
|
+
if (error && typeof error === "object" && error.code === "ENOENT") return null;
|
|
19136
|
+
throw error;
|
|
19137
|
+
}
|
|
19138
|
+
let pending;
|
|
19139
|
+
try {
|
|
19140
|
+
pending = pendingSignupFromJson(JSON.parse(contents));
|
|
19141
|
+
} catch {
|
|
19142
|
+
pending = null;
|
|
19143
|
+
}
|
|
19144
|
+
if (!pending) {
|
|
19145
|
+
deletePendingAgentSignup(configDir);
|
|
19146
|
+
return null;
|
|
19147
|
+
}
|
|
19148
|
+
if (pending.api_base_url_1 !== apiBaseUrl1) return null;
|
|
19149
|
+
return pending;
|
|
19150
|
+
}
|
|
19151
|
+
function pendingSignupStartCommand(email) {
|
|
19152
|
+
return `primitive signup ${email ?? "<email>"} --signup-code <invite-code> --accept-terms`;
|
|
19153
|
+
}
|
|
19154
|
+
function buildSignupStatus(params) {
|
|
19155
|
+
const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
|
|
19156
|
+
const pending = readPendingAgentSignupState(params.configDir, params.apiBaseUrl1);
|
|
19157
|
+
if (!pending) return {
|
|
19158
|
+
code_length: null,
|
|
19159
|
+
confirm_command: null,
|
|
19160
|
+
email: null,
|
|
19161
|
+
expired: false,
|
|
19162
|
+
expires_at: null,
|
|
19163
|
+
expires_in: null,
|
|
19164
|
+
pending: false,
|
|
19165
|
+
resend_after: null,
|
|
19166
|
+
resend_command: null,
|
|
19167
|
+
signup_command: pendingSignupStartCommand(params.email)
|
|
19168
|
+
};
|
|
19169
|
+
if (params.email && normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive signup status\` without an email argument to inspect it.`);
|
|
19170
|
+
const expiresAtMs = new Date(pending.expires_at).getTime();
|
|
19171
|
+
const expiresIn = Number.isFinite(expiresAtMs) ? Math.ceil((expiresAtMs - Date.now()) / 1e3) : null;
|
|
19172
|
+
return {
|
|
19173
|
+
code_length: pending.verification_code_length,
|
|
19174
|
+
confirm_command: `primitive ${copy.confirmCommand(pending.email)}`,
|
|
19175
|
+
email: pending.email,
|
|
19176
|
+
expired: expiresIn !== null && expiresIn <= 0,
|
|
19177
|
+
expires_at: pending.expires_at,
|
|
19178
|
+
expires_in: expiresIn === null ? null : Math.max(0, expiresIn),
|
|
19179
|
+
pending: true,
|
|
19180
|
+
resend_after: pending.resend_after,
|
|
19181
|
+
resend_command: `primitive ${copy.resendCommand(pending.email)}`
|
|
19182
|
+
};
|
|
19183
|
+
}
|
|
19184
|
+
function writeSignupStatus(status) {
|
|
19185
|
+
if (!status.pending) {
|
|
19186
|
+
process$1.stdout.write("No pending Primitive signup found.\n");
|
|
19187
|
+
process$1.stdout.write(`Start one with \`${status.signup_command ?? pendingSignupStartCommand()}\`.\n`);
|
|
19188
|
+
return;
|
|
19189
|
+
}
|
|
19190
|
+
process$1.stdout.write(`Pending Primitive signup for ${status.email}.\n`);
|
|
19191
|
+
if (status.code_length !== null) process$1.stdout.write(`Verification code length: ${status.code_length}\n`);
|
|
19192
|
+
if (status.expires_at) if (status.expired) process$1.stdout.write(`Expired at: ${status.expires_at}\n`);
|
|
19193
|
+
else {
|
|
19194
|
+
process$1.stdout.write(`Expires at: ${status.expires_at}\n`);
|
|
19195
|
+
process$1.stdout.write(`Expires in: ${formatSignupSeconds(status.expires_in)}\n`);
|
|
19196
|
+
}
|
|
19197
|
+
if (status.resend_after !== null) process$1.stdout.write(`Resend after: ${formatSignupSeconds(status.resend_after)}\n`);
|
|
19198
|
+
if (status.confirm_command) process$1.stdout.write(`Confirm: ${status.confirm_command}\n`);
|
|
19199
|
+
if (status.resend_command) process$1.stdout.write(`Resend: ${status.resend_command}\n`);
|
|
19200
|
+
}
|
|
19201
|
+
function runSignupStatus(params) {
|
|
19202
|
+
const { requestConfig } = createCliApiClient({
|
|
19203
|
+
apiBaseUrl1: params.flags["api-base-url-1"],
|
|
19204
|
+
configDir: params.configDir
|
|
19205
|
+
});
|
|
19206
|
+
const status = buildSignupStatus({
|
|
19207
|
+
apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
|
|
19208
|
+
configDir: params.configDir,
|
|
19209
|
+
copy: params.copy,
|
|
19210
|
+
email: params.email
|
|
19211
|
+
});
|
|
19212
|
+
if (params.flags.json) {
|
|
19213
|
+
process$1.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
19214
|
+
return;
|
|
19215
|
+
}
|
|
19216
|
+
writeSignupStatus(status);
|
|
19217
|
+
}
|
|
19047
19218
|
function requirePendingSignupForEmail(params) {
|
|
19048
19219
|
const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
|
|
19049
19220
|
const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
|
|
19050
|
-
if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
|
|
19051
|
-
if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
|
|
19221
|
+
if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive signup status ${params.email}\` to inspect pending state, or \`primitive ${copy.startCommand(params.email)}\` first.`);
|
|
19222
|
+
if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive signup status\` to inspect it, or \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
|
|
19052
19223
|
return pending;
|
|
19053
19224
|
}
|
|
19054
19225
|
function retryAfterSeconds(result) {
|
|
@@ -19125,6 +19296,7 @@ async function checkExistingCredentials(params) {
|
|
|
19125
19296
|
throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
|
|
19126
19297
|
}
|
|
19127
19298
|
function saveSignupCredentials(params) {
|
|
19299
|
+
deleteChatState(params.configDir);
|
|
19128
19300
|
saveCliCredentials(params.configDir, {
|
|
19129
19301
|
access_token: params.signup.access_token,
|
|
19130
19302
|
api_base_url_1: params.apiBaseUrl1,
|
|
@@ -19150,13 +19322,13 @@ async function startSignup(params) {
|
|
|
19150
19322
|
if (existingPending && !params.flags.force) {
|
|
19151
19323
|
if (normalizeEmail(existingPending.email) === normalizeEmail(params.email)) {
|
|
19152
19324
|
process$1.stderr.write(`Continuing pending Primitive ${copy.actionNoun} for ${existingPending.email}.\n`);
|
|
19153
|
-
process$1.stderr.write(`Run \`primitive ${copy.confirmCommand(existingPending.email)}\` to finish,
|
|
19325
|
+
process$1.stderr.write(`Run \`primitive ${copy.confirmCommand(existingPending.email)}\` to finish, \`primitive ${copy.resendCommand(existingPending.email)}\` to send a new code, or \`primitive signup status\` to inspect it.\n`);
|
|
19154
19326
|
return {
|
|
19155
19327
|
pending: existingPending,
|
|
19156
19328
|
started: false
|
|
19157
19329
|
};
|
|
19158
19330
|
}
|
|
19159
|
-
throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
|
|
19331
|
+
throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive signup status\` to inspect it, or \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
|
|
19160
19332
|
}
|
|
19161
19333
|
if (params.flags.force) deletePendingAgentSignup(params.configDir);
|
|
19162
19334
|
const promptRequiredFn = params.deps.promptRequired ?? promptRequired;
|
|
@@ -19292,23 +19464,25 @@ async function runSignupConfirmWithCredentialLock(params) {
|
|
|
19292
19464
|
}
|
|
19293
19465
|
const payload = extractErrorPayload(verified.error);
|
|
19294
19466
|
const code = extractErrorCode(payload);
|
|
19295
|
-
if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again
|
|
19467
|
+
if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again, run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}, or run primitive signup status.`);
|
|
19296
19468
|
if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(configDir);
|
|
19297
19469
|
writeErrorWithHints(payload);
|
|
19298
19470
|
throw cliError$2("Primitive agent signup failed while verifying the account.");
|
|
19299
19471
|
}
|
|
19300
19472
|
async function runSignupResendWithCredentialLock(params) {
|
|
19301
19473
|
const deps = params.deps ?? {};
|
|
19474
|
+
const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
|
|
19302
19475
|
const { apiClient, requestConfig } = createCliApiClient({
|
|
19303
19476
|
apiBaseUrl1: params.flags["api-base-url-1"],
|
|
19304
19477
|
configDir: params.configDir
|
|
19305
19478
|
});
|
|
19306
|
-
const pending = requirePendingSignupForEmail({
|
|
19479
|
+
const pending = params.email ? requirePendingSignupForEmail({
|
|
19307
19480
|
apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
|
|
19308
|
-
copy
|
|
19481
|
+
copy,
|
|
19309
19482
|
configDir: params.configDir,
|
|
19310
19483
|
email: params.email
|
|
19311
|
-
});
|
|
19484
|
+
}) : loadPendingAgentSignup(params.configDir, requestConfig.resolvedApiBaseUrl1);
|
|
19485
|
+
if (!pending) throw cliError$2(`No pending ${copy.actionNoun} found. Run \`primitive signup status\` to inspect pending state, or start one with \`${pendingSignupStartCommand()}\`.`);
|
|
19312
19486
|
const resend = await resendVerificationCode({
|
|
19313
19487
|
apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
|
|
19314
19488
|
apiClient,
|
|
@@ -19481,12 +19655,12 @@ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
|
|
|
19481
19655
|
};
|
|
19482
19656
|
var SignupResendCommand = class SignupResendCommand extends Command {
|
|
19483
19657
|
static args = { email: Args.string({
|
|
19484
|
-
description: "Email address used to start signup",
|
|
19485
|
-
required:
|
|
19658
|
+
description: "Email address used to start signup. Defaults to the saved pending signup.",
|
|
19659
|
+
required: false
|
|
19486
19660
|
}) };
|
|
19487
19661
|
static description = "Resend the verification code for a pending signup.";
|
|
19488
19662
|
static summary = "Resend signup verification code";
|
|
19489
|
-
static examples = ["<%= config.bin %> signup resend user@example.com"];
|
|
19663
|
+
static examples = ["<%= config.bin %> signup resend", "<%= config.bin %> signup resend user@example.com"];
|
|
19490
19664
|
static flags = { "api-base-url-1": Flags.string({
|
|
19491
19665
|
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
19492
19666
|
env: "PRIMITIVE_API_BASE_URL_1",
|
|
@@ -19511,6 +19685,35 @@ var SignupResendCommand = class SignupResendCommand extends Command {
|
|
|
19511
19685
|
}
|
|
19512
19686
|
}
|
|
19513
19687
|
};
|
|
19688
|
+
var SignupStatusCommand = class SignupStatusCommand extends Command {
|
|
19689
|
+
static args = { email: Args.string({
|
|
19690
|
+
description: "Email address expected in the pending signup",
|
|
19691
|
+
required: false
|
|
19692
|
+
}) };
|
|
19693
|
+
static description = "Inspect the locally saved pending Primitive signup state.";
|
|
19694
|
+
static summary = "Show pending signup status";
|
|
19695
|
+
static examples = [
|
|
19696
|
+
"<%= config.bin %> signup status",
|
|
19697
|
+
"<%= config.bin %> signup status user@example.com",
|
|
19698
|
+
"<%= config.bin %> signup status --json"
|
|
19699
|
+
];
|
|
19700
|
+
static flags = {
|
|
19701
|
+
"api-base-url-1": Flags.string({
|
|
19702
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
19703
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
19704
|
+
hidden: true
|
|
19705
|
+
}),
|
|
19706
|
+
json: Flags.boolean({ description: "Print pending signup status as JSON" })
|
|
19707
|
+
};
|
|
19708
|
+
async run() {
|
|
19709
|
+
const { args, flags } = await this.parse(SignupStatusCommand);
|
|
19710
|
+
runSignupStatus({
|
|
19711
|
+
configDir: this.config.configDir,
|
|
19712
|
+
email: args.email,
|
|
19713
|
+
flags
|
|
19714
|
+
});
|
|
19715
|
+
}
|
|
19716
|
+
};
|
|
19514
19717
|
var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
|
|
19515
19718
|
static description = "Run the full signup flow in one interactive terminal session.";
|
|
19516
19719
|
static summary = "Run interactive account signup";
|
|
@@ -19606,6 +19809,7 @@ function runForceLogout(params) {
|
|
|
19606
19809
|
const lockPath = credentialsLockPath(params.configDir);
|
|
19607
19810
|
const removed = [
|
|
19608
19811
|
existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
|
|
19812
|
+
existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
|
|
19609
19813
|
existsSync(pendingPath) ? "pending email-code auth state" : null,
|
|
19610
19814
|
existsSync(lockPath) ? "credential lock" : null
|
|
19611
19815
|
].filter((value) => value !== null);
|
|
@@ -19770,6 +19974,7 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
19770
19974
|
static examples = [
|
|
19771
19975
|
"<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
|
|
19772
19976
|
"<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
|
|
19977
|
+
"<%= config.bin %> reply --id <inbound-email-id> --body 'See attached.' --attachment ./report.pdf",
|
|
19773
19978
|
"<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
|
|
19774
19979
|
"<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
|
|
19775
19980
|
];
|
|
@@ -19793,12 +19998,17 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
19793
19998
|
required: true
|
|
19794
19999
|
}),
|
|
19795
20000
|
body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
|
|
19796
|
-
"body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
|
|
20001
|
+
"body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file; this does not attach the file. Use --attachment for attachments. Mutually exclusive with --body and --body-stdin." }),
|
|
19797
20002
|
"body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
|
|
19798
20003
|
html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
|
|
19799
|
-
"html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
|
|
20004
|
+
"html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file; this does not attach the file. Use --attachment for attachments. Mutually exclusive with --html and --html-stdin." }),
|
|
19800
20005
|
"html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
|
|
19801
20006
|
from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
|
|
20007
|
+
attachment: Flags.string({
|
|
20008
|
+
char: "a",
|
|
20009
|
+
description: "Attach a file to the reply. Repeat --attachment to attach multiple files.",
|
|
20010
|
+
multiple: true
|
|
20011
|
+
}),
|
|
19802
20012
|
wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
|
|
19803
20013
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
19804
20014
|
};
|
|
@@ -19820,14 +20030,16 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
19820
20030
|
apiBaseUrl2: flags["api-base-url-2"],
|
|
19821
20031
|
configDir: this.config.configDir
|
|
19822
20032
|
});
|
|
20033
|
+
const attachments = readAttachmentFiles(flags.attachment);
|
|
19823
20034
|
const result = await replyToEmail({
|
|
19824
20035
|
body: {
|
|
19825
20036
|
...bodies.body !== void 0 ? { body_text: bodies.body } : {},
|
|
19826
20037
|
...bodies.html !== void 0 ? { body_html: bodies.html } : {},
|
|
19827
20038
|
...flags.from !== void 0 ? { from: flags.from } : {},
|
|
20039
|
+
...attachments !== void 0 ? { attachments } : {},
|
|
19828
20040
|
...flags.wait !== void 0 ? { wait: flags.wait } : {}
|
|
19829
20041
|
},
|
|
19830
|
-
client: apiClient.
|
|
20042
|
+
client: apiClient._sendClient,
|
|
19831
20043
|
path: { id: flags.id },
|
|
19832
20044
|
responseStyle: "fields"
|
|
19833
20045
|
});
|
|
@@ -19852,39 +20064,6 @@ var ReplyCommand = class ReplyCommand extends Command {
|
|
|
19852
20064
|
}
|
|
19853
20065
|
};
|
|
19854
20066
|
//#endregion
|
|
19855
|
-
//#region src/oclif/attachments.ts
|
|
19856
|
-
function readAttachmentBytes(path, readFile) {
|
|
19857
|
-
try {
|
|
19858
|
-
return Buffer.from(readFile(path));
|
|
19859
|
-
} catch (error) {
|
|
19860
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
19861
|
-
throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
|
|
19862
|
-
}
|
|
19863
|
-
}
|
|
19864
|
-
function hasControlCharacter(value) {
|
|
19865
|
-
return Array.from(value).some((character) => {
|
|
19866
|
-
const code = character.charCodeAt(0);
|
|
19867
|
-
return code <= 31 || code >= 127 && code <= 159;
|
|
19868
|
-
});
|
|
19869
|
-
}
|
|
19870
|
-
function validateAttachmentFilename(path, filename) {
|
|
19871
|
-
if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
|
|
19872
|
-
if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
|
|
19873
|
-
}
|
|
19874
|
-
function readAttachmentFiles(paths, readFile = readFileSync) {
|
|
19875
|
-
if (!paths || paths.length === 0) return void 0;
|
|
19876
|
-
return paths.map((path) => {
|
|
19877
|
-
const filename = basename(path);
|
|
19878
|
-
validateAttachmentFilename(path, filename);
|
|
19879
|
-
const bytes = readAttachmentBytes(path, readFile);
|
|
19880
|
-
if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
|
|
19881
|
-
return {
|
|
19882
|
-
content_base64: bytes.toString("base64"),
|
|
19883
|
-
filename
|
|
19884
|
-
};
|
|
19885
|
-
});
|
|
19886
|
-
}
|
|
19887
|
-
//#endregion
|
|
19888
20067
|
//#region src/oclif/commands/send.ts
|
|
19889
20068
|
var SendCommand = class SendCommand extends Command {
|
|
19890
20069
|
static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
|
|
@@ -20659,6 +20838,7 @@ const CANONICAL_OPERATION_ALIASES = {
|
|
|
20659
20838
|
"domains:list": "domains:list-domains",
|
|
20660
20839
|
"domains:update": "domains:update-domain",
|
|
20661
20840
|
"domains:verify": "domains:verify-domain",
|
|
20841
|
+
"emails:conversation": "emails:get-conversation",
|
|
20662
20842
|
"emails:delete": "emails:delete-email",
|
|
20663
20843
|
"emails:discard-content": "emails:discard-email-content",
|
|
20664
20844
|
"emails:download-raw": "emails:download-raw-email",
|
|
@@ -20718,6 +20898,7 @@ const COMMANDS = {
|
|
|
20718
20898
|
send: SendCommand,
|
|
20719
20899
|
reply: ReplyCommand,
|
|
20720
20900
|
chat: ChatCommand,
|
|
20901
|
+
"chat:reply": ChatReplyCommand,
|
|
20721
20902
|
login: LoginCommand,
|
|
20722
20903
|
"login:browser": LoginBrowserCommand,
|
|
20723
20904
|
"login:confirm": LoginConfirmCommand,
|
|
@@ -20739,6 +20920,7 @@ const COMMANDS = {
|
|
|
20739
20920
|
"signup:confirm": SignupConfirmCommand,
|
|
20740
20921
|
"signup:interactive": SignupInteractiveCommand,
|
|
20741
20922
|
"signup:resend": SignupResendCommand,
|
|
20923
|
+
"signup:status": SignupStatusCommand,
|
|
20742
20924
|
logout: LogoutCommand,
|
|
20743
20925
|
whoami: WhoamiCommand,
|
|
20744
20926
|
doctor: DoctorCommand,
|
|
@@ -20747,6 +20929,7 @@ const COMMANDS = {
|
|
|
20747
20929
|
"emails:wait": EmailsWaitCommand,
|
|
20748
20930
|
"domains:zone-file": DomainsZoneFileCommand,
|
|
20749
20931
|
"domains:download-domain-zone-file": DomainsZoneFileCommand,
|
|
20932
|
+
"inbox:setup": InboxSetupCommand,
|
|
20750
20933
|
"inbox:status": InboxStatusCommand,
|
|
20751
20934
|
"inbox:get-inbox-status": InboxStatusCommand,
|
|
20752
20935
|
"functions:init": FunctionsInitCommand,
|