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