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