@prompts-gpt/client 0.2.0 → 0.2.2
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/CHANGELOG.md +67 -1
- package/README.md +76 -16
- package/dist/cli.js +736 -132
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +66 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +474 -97
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
export const DEFAULT_PROMPTS_GPT_API_URL = "https://prompts-gpt.com";
|
|
@@ -10,17 +10,24 @@ export class PromptsGptApiError extends Error {
|
|
|
10
10
|
status;
|
|
11
11
|
code;
|
|
12
12
|
recovery;
|
|
13
|
+
requestId;
|
|
14
|
+
fieldErrors;
|
|
15
|
+
retryAfterMs;
|
|
13
16
|
constructor(message, options) {
|
|
14
17
|
super(message);
|
|
15
18
|
this.name = "PromptsGptApiError";
|
|
16
19
|
this.status = options?.status ?? 0;
|
|
17
20
|
this.code = options?.code ?? "UNKNOWN_ERROR";
|
|
18
21
|
this.recovery = options?.recovery ?? "Retry the request or create a fresh project token.";
|
|
22
|
+
this.requestId = options?.requestId ?? null;
|
|
23
|
+
this.fieldErrors = options?.fieldErrors;
|
|
24
|
+
this.retryAfterMs = options?.retryAfterMs ?? null;
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
28
|
+
const DEFAULT_GENERATE_TIMEOUT_MS = 60_000;
|
|
22
29
|
const MAX_RETRIES = 2;
|
|
23
|
-
const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
|
|
30
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 429, 502, 503, 504]);
|
|
24
31
|
export class PromptsGptClient {
|
|
25
32
|
apiUrl;
|
|
26
33
|
token;
|
|
@@ -28,27 +35,27 @@ export class PromptsGptClient {
|
|
|
28
35
|
timeoutMs;
|
|
29
36
|
constructor(options) {
|
|
30
37
|
this.apiUrl = safeNormalizeApiUrl(options.apiUrl);
|
|
31
|
-
this.token = options.token;
|
|
38
|
+
this.token = options.token?.trim() || null;
|
|
32
39
|
this.fetchImpl = options.fetch;
|
|
33
40
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
34
41
|
}
|
|
35
|
-
async getProject() {
|
|
36
|
-
const data = await this.request("/api/sdk/v1/project");
|
|
42
|
+
async getProject(options = {}) {
|
|
43
|
+
const data = await this.request("/api/sdk/v1/project", options);
|
|
37
44
|
return data.project;
|
|
38
45
|
}
|
|
39
|
-
async pullPrompts(query = {}) {
|
|
40
|
-
const
|
|
41
|
-
for (const [key, value] of Object.entries(query)) {
|
|
42
|
-
if (value === undefined || value === null || value === "")
|
|
43
|
-
continue;
|
|
44
|
-
params.set(key === "query" ? "q" : key, String(value));
|
|
45
|
-
}
|
|
46
|
-
const qs = params.toString();
|
|
46
|
+
async pullPrompts(query = {}, options = {}) {
|
|
47
|
+
const qs = serializePromptQuery(query);
|
|
47
48
|
const suffix = qs ? `?${qs}` : "";
|
|
48
|
-
const data = await this.request(`/api/sdk/v1/prompts${suffix}
|
|
49
|
+
const data = await this.request(`/api/sdk/v1/prompts${suffix}`, options);
|
|
50
|
+
if (!Array.isArray(data.prompts)) {
|
|
51
|
+
throw new PromptsGptApiError("Prompts-GPT returned an invalid prompts payload.", {
|
|
52
|
+
code: "INVALID_RESPONSE",
|
|
53
|
+
recovery: "Retry the request. If it keeps failing, verify the SDK API version.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
49
56
|
return data.prompts;
|
|
50
57
|
}
|
|
51
|
-
async generatePrompt(input) {
|
|
58
|
+
async generatePrompt(input, options = {}) {
|
|
52
59
|
const trimmedGoal = input.goal?.trim() ?? "";
|
|
53
60
|
if (trimmedGoal.length < 8) {
|
|
54
61
|
throw new PromptsGptApiError("Goal must be at least 8 characters.", { code: "VALIDATION_ERROR" });
|
|
@@ -60,9 +67,19 @@ export class PromptsGptClient {
|
|
|
60
67
|
if (trimmedContext.length > 1600) {
|
|
61
68
|
throw new PromptsGptApiError("Context must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
|
|
62
69
|
}
|
|
70
|
+
const trimmedConstraints = input.constraints?.trim() ?? "";
|
|
71
|
+
if (trimmedConstraints.length > 1600) {
|
|
72
|
+
throw new PromptsGptApiError("Constraints must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
|
|
73
|
+
}
|
|
74
|
+
const trimmedDesiredOutput = input.desiredOutput?.trim() ?? "";
|
|
75
|
+
if (trimmedDesiredOutput.length > 1600) {
|
|
76
|
+
throw new PromptsGptApiError("Desired output must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
|
|
77
|
+
}
|
|
63
78
|
const data = await this.request("/api/sdk/v1/prompts/generate", {
|
|
64
79
|
method: "POST",
|
|
65
80
|
body: input,
|
|
81
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_GENERATE_TIMEOUT_MS,
|
|
82
|
+
...options,
|
|
66
83
|
});
|
|
67
84
|
return data.prompt;
|
|
68
85
|
}
|
|
@@ -71,7 +88,7 @@ export class PromptsGptClient {
|
|
|
71
88
|
throw new PromptsGptApiError("Project token is missing.", {
|
|
72
89
|
status: 401,
|
|
73
90
|
code: "AUTH_ERROR",
|
|
74
|
-
recovery: "Run `prompts-gpt init --token <token>` or
|
|
91
|
+
recovery: "Run `prompts-gpt init --token <token>` or pass `--token`, `--token-stdin`, or `--token-prompt` to the CLI command.",
|
|
75
92
|
});
|
|
76
93
|
}
|
|
77
94
|
if (!this.token.startsWith("pgpt_")) {
|
|
@@ -87,58 +104,98 @@ export class PromptsGptClient {
|
|
|
87
104
|
});
|
|
88
105
|
}
|
|
89
106
|
const controller = new AbortController();
|
|
90
|
-
const
|
|
107
|
+
const releaseLinkedAbort = linkAbortSignal(options.signal, controller);
|
|
108
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? this.timeoutMs);
|
|
109
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
110
|
+
timeout.unref?.();
|
|
111
|
+
const method = options.method ?? "GET";
|
|
91
112
|
try {
|
|
113
|
+
if (options.signal?.aborted) {
|
|
114
|
+
throw new PromptsGptApiError(`Request to ${pathname} was aborted by the caller.`, {
|
|
115
|
+
code: "REQUEST_ABORTED",
|
|
116
|
+
recovery: "Start a new request when you are ready to continue.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
92
119
|
const url = new URL(pathname, this.apiUrl);
|
|
120
|
+
const headers = {
|
|
121
|
+
authorization: `Bearer ${this.token}`,
|
|
122
|
+
accept: "application/json",
|
|
123
|
+
"x-prompts-gpt-client": `@prompts-gpt/client/${getClientVersion()}`,
|
|
124
|
+
};
|
|
125
|
+
if (!options.omitUserAgent) {
|
|
126
|
+
headers["user-agent"] = `prompts-gpt-client/${getClientVersion()}`;
|
|
127
|
+
}
|
|
128
|
+
const outgoingRequestId = options.requestId?.trim() || generateRequestId();
|
|
129
|
+
headers["x-request-id"] = outgoingRequestId;
|
|
130
|
+
if (options.body) {
|
|
131
|
+
headers["content-type"] = "application/json";
|
|
132
|
+
}
|
|
93
133
|
const response = await fetchFn(url, {
|
|
94
|
-
method
|
|
95
|
-
headers
|
|
96
|
-
authorization: `Bearer ${this.token}`,
|
|
97
|
-
accept: "application/json",
|
|
98
|
-
"user-agent": `prompts-gpt-client/${getClientVersion()}`,
|
|
99
|
-
...(options.body ? { "content-type": "application/json" } : {}),
|
|
100
|
-
},
|
|
134
|
+
method,
|
|
135
|
+
headers,
|
|
101
136
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
102
137
|
signal: controller.signal,
|
|
103
138
|
});
|
|
139
|
+
const requestId = response.headers?.get?.("x-request-id") ?? outgoingRequestId;
|
|
140
|
+
const retryAfterMs = parseRetryAfterHeader(response.headers?.get?.("retry-after"));
|
|
104
141
|
const contentType = response.headers?.get?.("content-type") ?? "";
|
|
105
|
-
if (!contentType
|
|
142
|
+
if (!isJsonContentType(contentType) && response.status !== 204) {
|
|
106
143
|
throw new PromptsGptApiError(`Unexpected response content-type: ${contentType || "none"}`, {
|
|
107
144
|
status: response.status,
|
|
108
145
|
code: "INVALID_RESPONSE",
|
|
109
146
|
recovery: "Verify the API URL is correct and the server is accessible.",
|
|
147
|
+
requestId,
|
|
148
|
+
retryAfterMs,
|
|
110
149
|
});
|
|
111
150
|
}
|
|
112
|
-
const payload = await response
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
151
|
+
const payload = await parseJsonResponse(response);
|
|
152
|
+
const apiResponse = parseApiResponse(payload);
|
|
153
|
+
if (!response.ok || !apiResponse || !apiResponse.ok) {
|
|
154
|
+
if (!apiResponse) {
|
|
155
|
+
throw new PromptsGptApiError("Prompts-GPT returned an unexpected API envelope.", {
|
|
156
|
+
status: response.status,
|
|
157
|
+
code: "INVALID_RESPONSE",
|
|
158
|
+
recovery: "Retry the request. If it keeps failing, verify the SDK API version.",
|
|
159
|
+
requestId,
|
|
160
|
+
retryAfterMs,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (shouldRetryRequest({ method, status: response.status, retryCount, retryAfterMs })) {
|
|
164
|
+
await sleep(computeRetryDelayMs(retryCount, retryAfterMs));
|
|
119
165
|
return this.request(pathname, options, retryCount + 1);
|
|
120
166
|
}
|
|
121
|
-
const error =
|
|
167
|
+
const error = !apiResponse.ok ? apiResponse.error : null;
|
|
122
168
|
throw new PromptsGptApiError(error?.message ?? `Prompts-GPT request failed with ${response.status}.`, {
|
|
123
169
|
status: response.status,
|
|
124
170
|
code: error?.code,
|
|
125
171
|
recovery: error?.recovery,
|
|
172
|
+
requestId,
|
|
173
|
+
retryAfterMs,
|
|
174
|
+
fieldErrors: error?.fieldErrors,
|
|
126
175
|
});
|
|
127
176
|
}
|
|
128
|
-
return
|
|
177
|
+
return apiResponse.data;
|
|
129
178
|
}
|
|
130
179
|
catch (error) {
|
|
131
180
|
if (error instanceof PromptsGptApiError)
|
|
132
181
|
throw error;
|
|
182
|
+
if (!options.omitUserAgent && isUserAgentRuntimeError(error)) {
|
|
183
|
+
return this.request(pathname, { ...options, omitUserAgent: true }, retryCount);
|
|
184
|
+
}
|
|
133
185
|
if (error?.name === "AbortError") {
|
|
134
|
-
|
|
186
|
+
if (options.signal?.aborted) {
|
|
187
|
+
throw new PromptsGptApiError(`Request to ${pathname} was aborted by the caller.`, {
|
|
188
|
+
code: "REQUEST_ABORTED",
|
|
189
|
+
recovery: "Start a new request when you are ready to continue.",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
throw new PromptsGptApiError(`Request to ${pathname} timed out after ${timeoutMs}ms.`, {
|
|
135
193
|
code: "TIMEOUT",
|
|
136
194
|
recovery: "Check your network connection or increase the timeout.",
|
|
137
195
|
});
|
|
138
196
|
}
|
|
139
|
-
if (retryCount < MAX_RETRIES && isNetworkError(error)) {
|
|
140
|
-
|
|
141
|
-
await sleep(Math.min((retryCount + 1) * 1000 + jitter, 30_000));
|
|
197
|
+
if (retryCount < MAX_RETRIES && method === "GET" && isNetworkError(error)) {
|
|
198
|
+
await sleep(computeRetryDelayMs(retryCount));
|
|
142
199
|
return this.request(pathname, options, retryCount + 1);
|
|
143
200
|
}
|
|
144
201
|
throw new PromptsGptApiError(error?.message ?? "Network request failed.", {
|
|
@@ -147,6 +204,7 @@ export class PromptsGptClient {
|
|
|
147
204
|
});
|
|
148
205
|
}
|
|
149
206
|
finally {
|
|
207
|
+
releaseLinkedAbort();
|
|
150
208
|
clearTimeout(timeout);
|
|
151
209
|
}
|
|
152
210
|
}
|
|
@@ -154,12 +212,140 @@ export class PromptsGptClient {
|
|
|
154
212
|
function isNetworkError(error) {
|
|
155
213
|
if (!error)
|
|
156
214
|
return false;
|
|
215
|
+
const name = String(error.name ?? "").toLowerCase();
|
|
157
216
|
const message = String(error.message ?? "").toLowerCase();
|
|
158
|
-
return
|
|
217
|
+
return (name === "typeerror" ||
|
|
218
|
+
message.includes("fetch") ||
|
|
219
|
+
message.includes("network") ||
|
|
220
|
+
message.includes("econnrefused") ||
|
|
221
|
+
message.includes("enotfound") ||
|
|
222
|
+
message.includes("eai_again") ||
|
|
223
|
+
message.includes("ecancelled") ||
|
|
224
|
+
message.includes("socket"));
|
|
225
|
+
}
|
|
226
|
+
function isUserAgentRuntimeError(error) {
|
|
227
|
+
const message = String(error?.message ?? "").toLowerCase();
|
|
228
|
+
return message.includes("user-agent") && message.includes("not allowed");
|
|
229
|
+
}
|
|
230
|
+
function linkAbortSignal(signal, controller) {
|
|
231
|
+
if (!signal)
|
|
232
|
+
return () => undefined;
|
|
233
|
+
if (signal.aborted) {
|
|
234
|
+
controller.abort(signal.reason);
|
|
235
|
+
return () => undefined;
|
|
236
|
+
}
|
|
237
|
+
const abort = () => controller.abort(signal.reason);
|
|
238
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
239
|
+
return () => signal.removeEventListener("abort", abort);
|
|
240
|
+
}
|
|
241
|
+
function normalizeTimeoutMs(value) {
|
|
242
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
243
|
+
throw new PromptsGptApiError("Timeout must be a positive number of milliseconds.", {
|
|
244
|
+
code: "VALIDATION_ERROR",
|
|
245
|
+
recovery: "Provide a timeout greater than 0.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return Math.min(Math.trunc(value), 300_000);
|
|
249
|
+
}
|
|
250
|
+
function serializePromptQuery(query) {
|
|
251
|
+
const params = new URLSearchParams();
|
|
252
|
+
const promptQuery = typeof query.query === "string" && query.query.trim() ? query.query : query.q;
|
|
253
|
+
if (typeof promptQuery === "string" && promptQuery.trim()) {
|
|
254
|
+
params.set("q", promptQuery.trim());
|
|
255
|
+
}
|
|
256
|
+
for (const [key, value] of Object.entries({
|
|
257
|
+
category: query.category,
|
|
258
|
+
tool: query.tool,
|
|
259
|
+
outputType: query.outputType,
|
|
260
|
+
})) {
|
|
261
|
+
if (typeof value === "string" && value.trim()) {
|
|
262
|
+
params.set(key, value.trim());
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (query.limit !== undefined && query.limit !== null && query.limit !== "") {
|
|
266
|
+
const limit = typeof query.limit === "string" ? Number(query.limit) : query.limit;
|
|
267
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
268
|
+
throw new PromptsGptApiError("Prompt limit must be an integer between 1 and 100.", {
|
|
269
|
+
code: "VALIDATION_ERROR",
|
|
270
|
+
recovery: "Choose a limit between 1 and 100.",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
params.set("limit", String(limit));
|
|
274
|
+
}
|
|
275
|
+
return params.toString();
|
|
276
|
+
}
|
|
277
|
+
function isJsonContentType(contentType) {
|
|
278
|
+
const normalized = contentType.toLowerCase();
|
|
279
|
+
return normalized.includes("application/json") || normalized.includes("+json");
|
|
280
|
+
}
|
|
281
|
+
async function parseJsonResponse(response) {
|
|
282
|
+
if (response.status === 204)
|
|
283
|
+
return { ok: true, data: {} };
|
|
284
|
+
const rawBody = await response.text();
|
|
285
|
+
if (!rawBody.trim())
|
|
286
|
+
return null;
|
|
287
|
+
try {
|
|
288
|
+
return JSON.parse(rawBody);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
throw new PromptsGptApiError("Prompts-GPT returned malformed JSON.", {
|
|
292
|
+
status: response.status,
|
|
293
|
+
code: "INVALID_RESPONSE",
|
|
294
|
+
recovery: "Retry the request. If it keeps failing, verify the API URL and server health.",
|
|
295
|
+
requestId: response.headers?.get?.("x-request-id") ?? null,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function parseApiResponse(payload) {
|
|
300
|
+
if (!payload || typeof payload !== "object" || !("ok" in payload))
|
|
301
|
+
return null;
|
|
302
|
+
if (payload.ok === true && "data" in payload) {
|
|
303
|
+
return payload;
|
|
304
|
+
}
|
|
305
|
+
if (payload.ok === false && "error" in payload) {
|
|
306
|
+
return payload;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function shouldRetryRequest({ method, status, retryCount, retryAfterMs, }) {
|
|
311
|
+
if (retryCount >= MAX_RETRIES)
|
|
312
|
+
return false;
|
|
313
|
+
if (method !== "GET")
|
|
314
|
+
return false;
|
|
315
|
+
if (!RETRYABLE_STATUS_CODES.has(status))
|
|
316
|
+
return false;
|
|
317
|
+
if (retryAfterMs !== null && retryAfterMs > 300_000)
|
|
318
|
+
return false;
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
function computeRetryDelayMs(retryCount, retryAfterMs = null) {
|
|
322
|
+
const baseDelay = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : (retryCount + 1) * 1_000;
|
|
323
|
+
const jitter = Math.random() * 500;
|
|
324
|
+
return Math.min(baseDelay + jitter, 30_000);
|
|
325
|
+
}
|
|
326
|
+
function parseRetryAfterHeader(value) {
|
|
327
|
+
if (!value)
|
|
328
|
+
return null;
|
|
329
|
+
const trimmed = value.trim();
|
|
330
|
+
if (!trimmed)
|
|
331
|
+
return null;
|
|
332
|
+
const seconds = Number(trimmed);
|
|
333
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
334
|
+
const ms = seconds * 1_000;
|
|
335
|
+
return Math.min(ms, 600_000);
|
|
336
|
+
}
|
|
337
|
+
const retryAt = Date.parse(trimmed);
|
|
338
|
+
if (Number.isNaN(retryAt))
|
|
339
|
+
return null;
|
|
340
|
+
return Math.min(Math.max(retryAt - Date.now(), 0), 600_000);
|
|
159
341
|
}
|
|
160
342
|
function sleep(ms) {
|
|
161
343
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
344
|
}
|
|
345
|
+
function generateRequestId() {
|
|
346
|
+
const hex = Array.from({ length: 8 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, "0")).join("");
|
|
347
|
+
return `pgcli_${hex}`;
|
|
348
|
+
}
|
|
163
349
|
let cachedVersion = null;
|
|
164
350
|
function getClientVersion() {
|
|
165
351
|
if (cachedVersion)
|
|
@@ -174,16 +360,21 @@ function getClientVersion() {
|
|
|
174
360
|
return cachedVersion;
|
|
175
361
|
}
|
|
176
362
|
export async function saveLocalCredentials(input) {
|
|
177
|
-
|
|
363
|
+
const trimmedToken = input.token?.trim();
|
|
364
|
+
if (!trimmedToken)
|
|
178
365
|
throw new Error("Token is required.");
|
|
179
|
-
if (
|
|
366
|
+
if (trimmedToken.length > 256)
|
|
180
367
|
throw new Error("Token value is too long.");
|
|
368
|
+
if (!trimmedToken.startsWith("pgpt_"))
|
|
369
|
+
throw new Error("Token must start with the 'pgpt_' prefix.");
|
|
181
370
|
const cwd = input.cwd ?? process.cwd();
|
|
182
371
|
const outDir = path.resolve(cwd, DEFAULT_PROMPTS_GPT_OUT_DIR);
|
|
183
372
|
await mkdir(outDir, { recursive: true });
|
|
184
|
-
await writeFile(path.join(outDir, PROMPTS_GPT_CREDENTIALS_FILE), `${JSON.stringify({ token:
|
|
373
|
+
await writeFile(path.join(outDir, PROMPTS_GPT_CREDENTIALS_FILE), `${JSON.stringify({ token: trimmedToken, apiUrl: normalizeApiUrl(input.apiUrl ?? DEFAULT_PROMPTS_GPT_API_URL) }, null, 2)}\n`, { mode: 0o600 });
|
|
374
|
+
const credentialsPath = path.join(outDir, PROMPTS_GPT_CREDENTIALS_FILE);
|
|
375
|
+
await chmod(credentialsPath, 0o600).catch(() => undefined);
|
|
185
376
|
await ensureGitignoreEntry(cwd, `${DEFAULT_PROMPTS_GPT_OUT_DIR}/${PROMPTS_GPT_CREDENTIALS_FILE}`);
|
|
186
|
-
return { credentialsPath
|
|
377
|
+
return { credentialsPath };
|
|
187
378
|
}
|
|
188
379
|
export async function loadLocalCredentials(cwd = process.cwd()) {
|
|
189
380
|
const credentialsPath = path.resolve(cwd, DEFAULT_PROMPTS_GPT_OUT_DIR, PROMPTS_GPT_CREDENTIALS_FILE);
|
|
@@ -199,8 +390,9 @@ export async function loadLocalCredentials(cwd = process.cwd()) {
|
|
|
199
390
|
}
|
|
200
391
|
catch { }
|
|
201
392
|
}
|
|
393
|
+
const rawToken = typeof parsed.token === "string" ? parsed.token.trim() : null;
|
|
202
394
|
return {
|
|
203
|
-
token:
|
|
395
|
+
token: rawToken || null,
|
|
204
396
|
apiUrl,
|
|
205
397
|
};
|
|
206
398
|
}
|
|
@@ -210,7 +402,12 @@ export async function loadLocalCredentials(cwd = process.cwd()) {
|
|
|
210
402
|
}
|
|
211
403
|
export async function syncPrompts(prompts, options = {}) {
|
|
212
404
|
const markdown = await writePromptMarkdownFiles(prompts, options);
|
|
213
|
-
const agents = await writeAgentFiles(prompts,
|
|
405
|
+
const agents = await writeAgentFiles(prompts, {
|
|
406
|
+
cwd: options.cwd,
|
|
407
|
+
agent: options.agent,
|
|
408
|
+
agents: options.agents,
|
|
409
|
+
overwriteAgentFiles: options.overwrite,
|
|
410
|
+
});
|
|
214
411
|
const manifest = await writePromptManifest(prompts, { cwd: options.cwd, outDir: options.outDir });
|
|
215
412
|
return { markdown, agents, manifest };
|
|
216
413
|
}
|
|
@@ -218,18 +415,31 @@ export async function writePromptMarkdownFiles(prompts, options = {}) {
|
|
|
218
415
|
const cwd = options.cwd ?? process.cwd();
|
|
219
416
|
const outDir = assertSafeOutputDir(cwd, options.outDir ?? DEFAULT_PROMPTS_GPT_OUT_DIR);
|
|
220
417
|
const overwrite = Boolean(options.overwrite);
|
|
418
|
+
const normalizedPrompts = assertUniquePromptFileStems(prompts);
|
|
221
419
|
await mkdir(outDir, { recursive: true });
|
|
222
420
|
const written = [];
|
|
223
421
|
const skipped = [];
|
|
224
|
-
for (const prompt of
|
|
225
|
-
const filePath = path.join(outDir, `${
|
|
422
|
+
for (const { prompt, stem } of normalizedPrompts) {
|
|
423
|
+
const filePath = path.join(outDir, `${stem}.md`);
|
|
226
424
|
assertInside(filePath, outDir);
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
425
|
+
if (overwrite) {
|
|
426
|
+
await writeFile(filePath, formatPromptMarkdown(prompt));
|
|
427
|
+
written.push(filePath);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
try {
|
|
431
|
+
await writeFile(filePath, formatPromptMarkdown(prompt), { flag: "wx" });
|
|
432
|
+
written.push(filePath);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
if (err.code === "EEXIST") {
|
|
436
|
+
skipped.push(filePath);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
230
442
|
}
|
|
231
|
-
await writeFile(filePath, formatPromptMarkdown(prompt), { flag: overwrite ? "w" : "wx" });
|
|
232
|
-
written.push(filePath);
|
|
233
443
|
}
|
|
234
444
|
await writePromptIndex(prompts, { outDir });
|
|
235
445
|
return { outDir, written, skipped };
|
|
@@ -237,45 +447,72 @@ export async function writePromptMarkdownFiles(prompts, options = {}) {
|
|
|
237
447
|
export async function writeAgentFiles(prompts, options = {}) {
|
|
238
448
|
const cwd = options.cwd ?? process.cwd();
|
|
239
449
|
const targets = normalizeAgentTargets(options.agent ?? options.agents ?? "all");
|
|
240
|
-
const overwrite = Boolean(options.overwriteAgentFiles
|
|
450
|
+
const overwrite = Boolean(options.overwriteAgentFiles);
|
|
241
451
|
const written = [];
|
|
242
452
|
const skipped = [];
|
|
243
|
-
const
|
|
453
|
+
const normalizedPrompts = assertUniquePromptFileStems(prompts);
|
|
454
|
+
const allFiles = targets.flatMap((target) => buildAgentFiles(target, normalizedPrompts.filter(({ prompt }) => promptSupportsAgentTarget(prompt, target))).map((file) => ({ ...file, target })));
|
|
455
|
+
assertUniqueAgentFilePaths(allFiles);
|
|
244
456
|
const dirSet = new Set(allFiles.map((file) => path.dirname(assertSafeProjectFile(cwd, file.path))));
|
|
245
457
|
await Promise.all([...dirSet].map((dir) => mkdir(dir, { recursive: true })));
|
|
246
|
-
|
|
458
|
+
for (const file of allFiles) {
|
|
247
459
|
const filePath = assertSafeProjectFile(cwd, file.path);
|
|
248
460
|
if (file.managedBlock) {
|
|
249
|
-
|
|
461
|
+
let existing = "";
|
|
462
|
+
try {
|
|
463
|
+
existing = await readFile(filePath, "utf8");
|
|
464
|
+
}
|
|
465
|
+
catch { }
|
|
250
466
|
await writeFile(filePath, upsertManagedBlock(existing, file.content));
|
|
251
467
|
written.push(filePath);
|
|
252
|
-
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (overwrite) {
|
|
471
|
+
await writeFile(filePath, file.content);
|
|
472
|
+
written.push(filePath);
|
|
253
473
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
474
|
+
else {
|
|
475
|
+
try {
|
|
476
|
+
await writeFile(filePath, file.content, { flag: "wx" });
|
|
477
|
+
written.push(filePath);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
if (err.code === "EEXIST") {
|
|
481
|
+
skipped.push(filePath);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
257
487
|
}
|
|
258
|
-
|
|
259
|
-
written.push(filePath);
|
|
260
|
-
}));
|
|
488
|
+
}
|
|
261
489
|
return { written, skipped, targets };
|
|
262
490
|
}
|
|
263
491
|
export async function writePromptManifest(prompts, options = {}) {
|
|
264
492
|
const cwd = options.cwd ?? process.cwd();
|
|
265
493
|
const outDir = assertSafeOutputDir(cwd, options.outDir ?? DEFAULT_PROMPTS_GPT_OUT_DIR);
|
|
494
|
+
const normalizedPrompts = assertUniquePromptFileStems(prompts);
|
|
266
495
|
await mkdir(outDir, { recursive: true });
|
|
267
496
|
const manifestPath = path.join(outDir, PROMPTS_GPT_MANIFEST_FILE);
|
|
268
497
|
const payload = {
|
|
269
498
|
version: 1,
|
|
270
499
|
generatedAt: new Date().toISOString(),
|
|
271
|
-
count:
|
|
272
|
-
prompts:
|
|
273
|
-
slug:
|
|
500
|
+
count: normalizedPrompts.length,
|
|
501
|
+
prompts: normalizedPrompts.map(({ prompt, stem }) => ({
|
|
502
|
+
slug: stem,
|
|
274
503
|
title: prompt.title,
|
|
504
|
+
summary: prompt.summary ?? "",
|
|
275
505
|
source: prompt.source ?? "library",
|
|
276
506
|
category: prompt.category ?? "Prompt Library",
|
|
507
|
+
difficulty: prompt.difficulty ?? "Intermediate",
|
|
508
|
+
outputType: prompt.outputType ?? "Text",
|
|
277
509
|
supportedTools: prompt.supportedTools ?? [],
|
|
278
|
-
|
|
510
|
+
agentTargets: normalizePromptAgentTargets(prompt),
|
|
511
|
+
variables: prompt.variables ?? [],
|
|
512
|
+
tags: prompt.tags ?? [],
|
|
513
|
+
recommendedPath: prompt.recommendedPath ?? null,
|
|
514
|
+
file: `${stem}.md`,
|
|
515
|
+
files: buildDiscoverablePromptFiles(stem, prompt),
|
|
279
516
|
})),
|
|
280
517
|
};
|
|
281
518
|
await writeFile(manifestPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
@@ -302,11 +539,8 @@ export function formatPromptMarkdown(prompt) {
|
|
|
302
539
|
"",
|
|
303
540
|
prompt.promptText ?? "",
|
|
304
541
|
"",
|
|
305
|
-
prompt.variables?.length ? "## Variables"
|
|
306
|
-
...(prompt.
|
|
307
|
-
prompt.variables?.length ? "" : "",
|
|
308
|
-
prompt.usageNotes ? "## Usage Notes" : "",
|
|
309
|
-
prompt.usageNotes ?? "",
|
|
542
|
+
...(prompt.variables?.length ? ["## Variables", "", ...prompt.variables.map((variable) => `- \`${variable}\``), ""] : []),
|
|
543
|
+
...(prompt.usageNotes ? ["## Usage Notes", "", prompt.usageNotes] : []),
|
|
310
544
|
"",
|
|
311
545
|
].reduce((acc, line, index, list) => {
|
|
312
546
|
if (line === "" && index > 0 && list[index - 1] === "" && (index < 2 || list[index - 2] === ""))
|
|
@@ -323,10 +557,10 @@ function buildAgentFiles(target, prompts) {
|
|
|
323
557
|
content: [
|
|
324
558
|
"# Prompts-GPT Agent Instructions",
|
|
325
559
|
"",
|
|
326
|
-
"Prompts synced by `prompts-gpt sync` live in `.prompts-gpt/`.
|
|
560
|
+
"Prompts synced by `prompts-gpt sync` live in `.prompts-gpt/`. Start with [.prompts-gpt/manifest.json](.prompts-gpt/manifest.json), then open the prompt packs linked below before starting related work.",
|
|
327
561
|
"",
|
|
328
562
|
"## Available Prompt Packs",
|
|
329
|
-
...prompts.map((prompt) => `- ${prompt.title}
|
|
563
|
+
...prompts.map(({ prompt, stem }) => `- [${prompt.title}](.prompts-gpt/${stem}.md)`),
|
|
330
564
|
"",
|
|
331
565
|
"When a prompt pack is relevant, load it, adapt variables to the current task, and keep verification tied to the prompt's acceptance criteria.",
|
|
332
566
|
"",
|
|
@@ -334,12 +568,12 @@ function buildAgentFiles(target, prompts) {
|
|
|
334
568
|
}];
|
|
335
569
|
}
|
|
336
570
|
if (target === "cursor") {
|
|
337
|
-
return prompts.map((prompt) => ({
|
|
338
|
-
path: `.cursor/rules/prompts-gpt-${
|
|
571
|
+
return prompts.map(({ prompt, stem }) => ({
|
|
572
|
+
path: `.cursor/rules/prompts-gpt-${stem}.mdc`,
|
|
339
573
|
content: [
|
|
340
574
|
"---",
|
|
341
|
-
`description: ${prompt.summary || prompt.title}`,
|
|
342
|
-
"globs:",
|
|
575
|
+
`description: ${yamlScalar(prompt.summary || prompt.title)}`,
|
|
576
|
+
"globs: []",
|
|
343
577
|
"alwaysApply: false",
|
|
344
578
|
"---",
|
|
345
579
|
"",
|
|
@@ -360,9 +594,26 @@ function buildAgentFiles(target, prompts) {
|
|
|
360
594
|
content: [
|
|
361
595
|
"# Prompts-GPT Copilot Instructions",
|
|
362
596
|
"",
|
|
363
|
-
"Use
|
|
597
|
+
"Use [../.prompts-gpt/manifest.json](../.prompts-gpt/manifest.json) and the linked `.prompts-gpt/*.md` prompt packs as reusable repository context.",
|
|
598
|
+
"",
|
|
599
|
+
...prompts.map(({ prompt, stem }) => `- [${prompt.title}](../.prompts-gpt/${stem}.md)`),
|
|
600
|
+
"",
|
|
601
|
+
].join("\n"),
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
path: ".github/instructions/prompts-gpt.instructions.md",
|
|
605
|
+
content: [
|
|
606
|
+
"---",
|
|
607
|
+
'applyTo: "AGENTS.md,.prompts-gpt/**/*.md,.github/copilot-instructions.md,.github/prompts/**/*.prompt.md,.cursor/rules/**/*.mdc,.vscode/prompts-gpt.code-snippets"',
|
|
608
|
+
"---",
|
|
609
|
+
"",
|
|
610
|
+
"# Prompts-GPT managed artifacts",
|
|
611
|
+
"",
|
|
612
|
+
"These files are generated or refreshed by `prompts-gpt sync` and `prompts-gpt install-agents`.",
|
|
364
613
|
"",
|
|
365
|
-
|
|
614
|
+
"- Treat `.prompts-gpt/manifest.json` as the source of truth for discoverable prompt packs and generated agent files.",
|
|
615
|
+
"- Prefer updating the upstream prompt pack or rerunning sync instead of manually editing generated agent artifacts.",
|
|
616
|
+
"- Preserve the managed `prompts-gpt` blocks inside `AGENTS.md` and `.github/copilot-instructions.md`.",
|
|
366
617
|
"",
|
|
367
618
|
].join("\n"),
|
|
368
619
|
},
|
|
@@ -373,27 +624,123 @@ function buildAgentFiles(target, prompts) {
|
|
|
373
624
|
];
|
|
374
625
|
}
|
|
375
626
|
if (target === "copilot") {
|
|
376
|
-
return prompts.map((prompt) => ({
|
|
377
|
-
path: `.github/prompts/prompts-gpt-${
|
|
378
|
-
content:
|
|
627
|
+
return prompts.map(({ prompt, stem }) => ({
|
|
628
|
+
path: `.github/prompts/prompts-gpt-${stem}.prompt.md`,
|
|
629
|
+
content: formatCopilotPromptMarkdown(prompt, stem),
|
|
379
630
|
}));
|
|
380
631
|
}
|
|
381
632
|
return [];
|
|
382
633
|
}
|
|
383
634
|
function buildVsCodeSnippets(prompts) {
|
|
384
|
-
return prompts.reduce((snippets, prompt) => {
|
|
635
|
+
return prompts.reduce((snippets, { prompt, stem }) => {
|
|
385
636
|
snippets[`Prompts-GPT: ${prompt.title}`] = {
|
|
386
|
-
prefix: `pgpt-${
|
|
637
|
+
prefix: `pgpt-${stem}`,
|
|
387
638
|
description: prompt.summary || prompt.title,
|
|
388
639
|
body: String(prompt.promptText || "").split(/\r?\n/),
|
|
389
640
|
};
|
|
390
641
|
return snippets;
|
|
391
642
|
}, {});
|
|
392
643
|
}
|
|
644
|
+
function normalizePromptAgentTargets(prompt) {
|
|
645
|
+
const validSet = new Set(SUPPORTED_AGENT_TARGETS);
|
|
646
|
+
const declaredTargets = Array.isArray(prompt.agentTargets)
|
|
647
|
+
? prompt.agentTargets.map((target) => String(target).trim().toLowerCase()).filter(Boolean)
|
|
648
|
+
: [];
|
|
649
|
+
return [...new Set(declaredTargets.filter((target) => validSet.has(target)))];
|
|
650
|
+
}
|
|
651
|
+
function promptSupportsAgentTarget(prompt, target) {
|
|
652
|
+
const declaredTargets = normalizePromptAgentTargets(prompt);
|
|
653
|
+
return declaredTargets.length === 0 || declaredTargets.includes(target);
|
|
654
|
+
}
|
|
655
|
+
function assertUniquePromptFileStems(prompts) {
|
|
656
|
+
const byStem = new Map();
|
|
657
|
+
const normalizedPrompts = prompts.map((prompt) => {
|
|
658
|
+
const stem = safeSlug(prompt.slug || prompt.title);
|
|
659
|
+
const matches = byStem.get(stem) ?? [];
|
|
660
|
+
matches.push(prompt);
|
|
661
|
+
byStem.set(stem, matches);
|
|
662
|
+
return { prompt, stem };
|
|
663
|
+
});
|
|
664
|
+
const collisions = [...byStem.entries()].filter(([, items]) => items.length > 1);
|
|
665
|
+
if (collisions.length > 0) {
|
|
666
|
+
const details = collisions
|
|
667
|
+
.map(([stem, items]) => `${stem} <- ${items.map((item) => item.title || item.slug || "Untitled prompt").join(", ")}`)
|
|
668
|
+
.join("; ");
|
|
669
|
+
throw new Error(`Refusing to sync prompts with colliding file names after slug normalization: ${details}`);
|
|
670
|
+
}
|
|
671
|
+
return normalizedPrompts;
|
|
672
|
+
}
|
|
673
|
+
function assertUniqueAgentFilePaths(files) {
|
|
674
|
+
const pathCounts = new Map();
|
|
675
|
+
for (const file of files) {
|
|
676
|
+
const targets = pathCounts.get(file.path) ?? [];
|
|
677
|
+
targets.push(file.target);
|
|
678
|
+
pathCounts.set(file.path, targets);
|
|
679
|
+
}
|
|
680
|
+
const collisions = [...pathCounts.entries()].filter(([, targets]) => targets.length > 1 && !targets.every((target) => target === targets[0]));
|
|
681
|
+
if (collisions.length > 0) {
|
|
682
|
+
const details = collisions.map(([filePath, targets]) => `${filePath} <- ${targets.join(", ")}`).join("; ");
|
|
683
|
+
throw new Error(`Refusing to sync duplicate agent file paths: ${details}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function buildDiscoverablePromptFiles(stem, prompt) {
|
|
687
|
+
const agentTargets = new Set(normalizePromptAgentTargets(prompt));
|
|
688
|
+
const supports = (target) => agentTargets.size === 0 || agentTargets.has(target);
|
|
689
|
+
return {
|
|
690
|
+
markdown: `.prompts-gpt/${stem}.md`,
|
|
691
|
+
codexInstructions: supports("codex") ? "AGENTS.md" : null,
|
|
692
|
+
cursorRule: supports("cursor") ? `.cursor/rules/prompts-gpt-${stem}.mdc` : null,
|
|
693
|
+
vscodeInstructions: supports("vscode") ? ".github/copilot-instructions.md" : null,
|
|
694
|
+
copilotPathInstructions: supports("vscode") ? ".github/instructions/prompts-gpt.instructions.md" : null,
|
|
695
|
+
vscodeSnippets: supports("vscode") ? ".vscode/prompts-gpt.code-snippets" : null,
|
|
696
|
+
copilotPrompt: supports("copilot") ? `.github/prompts/prompts-gpt-${stem}.prompt.md` : null,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function formatCopilotPromptMarkdown(prompt, stem) {
|
|
700
|
+
const sections = [
|
|
701
|
+
"---",
|
|
702
|
+
"mode: agent",
|
|
703
|
+
`description: ${yamlScalar(prompt.summary || prompt.title)}`,
|
|
704
|
+
"---",
|
|
705
|
+
"",
|
|
706
|
+
`# ${prompt.title}`,
|
|
707
|
+
"",
|
|
708
|
+
`Use [../../.prompts-gpt/${stem}.md](../../.prompts-gpt/${stem}.md) as the canonical prompt-pack reference and verify generated work against [../../.prompts-gpt/manifest.json](../../.prompts-gpt/manifest.json).`,
|
|
709
|
+
"",
|
|
710
|
+
prompt.summary ?? "",
|
|
711
|
+
"",
|
|
712
|
+
"## Task",
|
|
713
|
+
"",
|
|
714
|
+
prompt.promptText ?? "",
|
|
715
|
+
];
|
|
716
|
+
if (prompt.variables?.length) {
|
|
717
|
+
sections.push("", "## Inputs", "");
|
|
718
|
+
for (const variable of prompt.variables) {
|
|
719
|
+
const sanitizedName = safeSlug(variable) || "value";
|
|
720
|
+
const sanitizedLabel = String(variable).replace(/[{}$]/g, "");
|
|
721
|
+
sections.push(`- ${sanitizedLabel}: \${input:${sanitizedName}:Provide ${sanitizedLabel}}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (prompt.usageNotes) {
|
|
725
|
+
sections.push("", "## Usage Notes", "", prompt.usageNotes);
|
|
726
|
+
}
|
|
727
|
+
sections.push("");
|
|
728
|
+
return sections
|
|
729
|
+
.reduce((acc, line, index, list) => {
|
|
730
|
+
if (line === "" && index > 0 && list[index - 1] === "" && (index < 2 || list[index - 2] === ""))
|
|
731
|
+
return acc;
|
|
732
|
+
acc.push(line);
|
|
733
|
+
return acc;
|
|
734
|
+
}, [])
|
|
735
|
+
.join("\n");
|
|
736
|
+
}
|
|
393
737
|
function upsertManagedBlock(existing, content) {
|
|
394
738
|
const start = "<!-- prompts-gpt:start -->";
|
|
395
739
|
const end = "<!-- prompts-gpt:end -->";
|
|
396
|
-
const
|
|
740
|
+
const sanitizedContent = content.trim()
|
|
741
|
+
.replace(/<!-- prompts-gpt:start -->/g, "")
|
|
742
|
+
.replace(/<!-- prompts-gpt:end -->/g, "");
|
|
743
|
+
const block = `${start}\n${sanitizedContent}\n${end}`;
|
|
397
744
|
const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`);
|
|
398
745
|
if (pattern.test(existing)) {
|
|
399
746
|
return `${existing.replace(pattern, block).trimEnd()}\n`;
|
|
@@ -404,6 +751,9 @@ function upsertManagedBlock(existing, content) {
|
|
|
404
751
|
function escapeRegExp(value) {
|
|
405
752
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
406
753
|
}
|
|
754
|
+
function escapeMarkdownLinkText(value) {
|
|
755
|
+
return value.replace(/[[\]]/g, "\\$&");
|
|
756
|
+
}
|
|
407
757
|
async function writePromptIndex(prompts, { outDir }) {
|
|
408
758
|
const indexPath = path.join(outDir, "README.md");
|
|
409
759
|
const content = [
|
|
@@ -412,7 +762,12 @@ async function writePromptIndex(prompts, { outDir }) {
|
|
|
412
762
|
"These prompts were synced by `prompts-gpt`. Re-run `prompts-gpt sync` to refresh Markdown and agent files.",
|
|
413
763
|
"",
|
|
414
764
|
"## Prompts",
|
|
415
|
-
...prompts.map((prompt) =>
|
|
765
|
+
...prompts.map((prompt) => {
|
|
766
|
+
const agentTargets = normalizePromptAgentTargets(prompt);
|
|
767
|
+
const suffix = agentTargets.length ? ` Targets: ${agentTargets.join(", ")}.` : "";
|
|
768
|
+
const escapedTitle = escapeMarkdownLinkText(prompt.title);
|
|
769
|
+
return `- [${escapedTitle}](./${safeSlug(prompt.slug || prompt.title)}.md) - ${prompt.summary ?? ""}${suffix}`;
|
|
770
|
+
}),
|
|
416
771
|
"",
|
|
417
772
|
].join("\n");
|
|
418
773
|
assertInside(indexPath, outDir);
|
|
@@ -421,8 +776,11 @@ async function writePromptIndex(prompts, { outDir }) {
|
|
|
421
776
|
function normalizeAgentTargets(value) {
|
|
422
777
|
const raw = Array.isArray(value) ? value.join(",") : String(value ?? "all");
|
|
423
778
|
const targets = raw.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
424
|
-
|
|
425
|
-
|
|
779
|
+
if (targets.includes("all"))
|
|
780
|
+
return [...SUPPORTED_AGENT_TARGETS];
|
|
781
|
+
const unique = [...new Set(targets)];
|
|
782
|
+
if (unique.length === 0)
|
|
783
|
+
return [...SUPPORTED_AGENT_TARGETS];
|
|
426
784
|
const validSet = new Set(SUPPORTED_AGENT_TARGETS);
|
|
427
785
|
const invalid = unique.filter((target) => !validSet.has(target));
|
|
428
786
|
if (invalid.length)
|
|
@@ -431,6 +789,9 @@ function normalizeAgentTargets(value) {
|
|
|
431
789
|
}
|
|
432
790
|
function normalizeApiUrl(value) {
|
|
433
791
|
const url = new URL(value);
|
|
792
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
793
|
+
throw new Error(`API URL must use https or http, got ${url.protocol}`);
|
|
794
|
+
}
|
|
434
795
|
url.pathname = "/";
|
|
435
796
|
url.search = "";
|
|
436
797
|
url.hash = "";
|
|
@@ -447,10 +808,14 @@ function safeNormalizeApiUrl(value) {
|
|
|
447
808
|
});
|
|
448
809
|
}
|
|
449
810
|
}
|
|
811
|
+
function isInsideDirectory(child, parent) {
|
|
812
|
+
const normalizedParent = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`;
|
|
813
|
+
return child.startsWith(normalizedParent) || child === parent;
|
|
814
|
+
}
|
|
450
815
|
function assertSafeOutputDir(cwd, outDir) {
|
|
451
816
|
const root = path.resolve(cwd);
|
|
452
817
|
const resolved = path.resolve(root, outDir);
|
|
453
|
-
if (!resolved
|
|
818
|
+
if (!isInsideDirectory(resolved, root) || resolved === root) {
|
|
454
819
|
throw new Error("Output directory must be a subdirectory of the current project.");
|
|
455
820
|
}
|
|
456
821
|
return resolved;
|
|
@@ -458,33 +823,45 @@ function assertSafeOutputDir(cwd, outDir) {
|
|
|
458
823
|
function assertSafeProjectFile(cwd, filePath) {
|
|
459
824
|
const root = path.resolve(cwd);
|
|
460
825
|
const resolved = path.resolve(root, filePath);
|
|
461
|
-
if (!resolved
|
|
826
|
+
if (!isInsideDirectory(resolved, root) || resolved === root) {
|
|
462
827
|
throw new Error("Agent file path must stay inside the current project.");
|
|
463
828
|
}
|
|
464
829
|
return resolved;
|
|
465
830
|
}
|
|
466
831
|
function assertInside(filePath, directory) {
|
|
467
|
-
if (!filePath
|
|
832
|
+
if (!isInsideDirectory(filePath, directory) || filePath === directory) {
|
|
468
833
|
throw new Error("Refusing to write outside the prompt output directory.");
|
|
469
834
|
}
|
|
470
835
|
}
|
|
471
836
|
function safeSlug(value) {
|
|
472
|
-
|
|
837
|
+
const raw = String(value ?? "");
|
|
838
|
+
if (!raw.trim())
|
|
839
|
+
return "prompt";
|
|
840
|
+
const slug = raw
|
|
473
841
|
.toLowerCase()
|
|
842
|
+
.normalize("NFKD")
|
|
843
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
474
844
|
.replace(/[^a-z0-9]+/g, "-")
|
|
475
845
|
.replace(/^-+|-+$/g, "")
|
|
476
|
-
.slice(0, 90)
|
|
846
|
+
.slice(0, 90);
|
|
847
|
+
return slug || "prompt";
|
|
477
848
|
}
|
|
478
849
|
function yamlScalar(value) {
|
|
479
|
-
|
|
850
|
+
const s = String(value ?? "");
|
|
851
|
+
if (s.includes("\n") || s.includes("\r")) {
|
|
852
|
+
return JSON.stringify(s.replace(/\r\n?/g, "\n"));
|
|
853
|
+
}
|
|
854
|
+
return JSON.stringify(s);
|
|
480
855
|
}
|
|
481
856
|
async function ensureGitignoreEntry(cwd, entry) {
|
|
482
857
|
const gitignorePath = path.resolve(cwd, ".gitignore");
|
|
483
858
|
const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, "utf8") : "";
|
|
859
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
484
860
|
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
485
861
|
if (lines.includes(entry))
|
|
486
862
|
return;
|
|
487
|
-
const
|
|
488
|
-
|
|
863
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith("\n") && !existing.endsWith("\r\n");
|
|
864
|
+
const prefix = needsLeadingNewline ? eol : "";
|
|
865
|
+
await writeFile(gitignorePath, `${existing}${prefix}${entry}${eol}`);
|
|
489
866
|
}
|
|
490
867
|
//# sourceMappingURL=index.js.map
|