@prompts-gpt/client 0.1.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 +95 -0
- package/README.md +244 -69
- package/dist/cli.js +742 -130
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +67 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +508 -102
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
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,56 +10,76 @@ 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;
|
|
27
34
|
fetchImpl;
|
|
28
35
|
timeoutMs;
|
|
29
|
-
constructor(options
|
|
30
|
-
this.apiUrl = safeNormalizeApiUrl(options.apiUrl
|
|
31
|
-
this.token = options.token
|
|
32
|
-
this.fetchImpl = options.fetch
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.apiUrl = safeNormalizeApiUrl(options.apiUrl);
|
|
38
|
+
this.token = options.token?.trim() || null;
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
async pullPrompts(query = {}, options = {}) {
|
|
47
|
+
const qs = serializePromptQuery(query);
|
|
48
|
+
const suffix = qs ? `?${qs}` : "";
|
|
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
|
+
});
|
|
45
55
|
}
|
|
46
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
47
|
-
const data = await this.request(`/api/sdk/v1/prompts${suffix}`);
|
|
48
56
|
return data.prompts;
|
|
49
57
|
}
|
|
50
|
-
async generatePrompt(input) {
|
|
51
|
-
|
|
58
|
+
async generatePrompt(input, options = {}) {
|
|
59
|
+
const trimmedGoal = input.goal?.trim() ?? "";
|
|
60
|
+
if (trimmedGoal.length < 8) {
|
|
52
61
|
throw new PromptsGptApiError("Goal must be at least 8 characters.", { code: "VALIDATION_ERROR" });
|
|
53
62
|
}
|
|
54
|
-
if (
|
|
63
|
+
if (trimmedGoal.length > 160) {
|
|
55
64
|
throw new PromptsGptApiError("Goal must be 160 characters or fewer.", { code: "VALIDATION_ERROR" });
|
|
56
65
|
}
|
|
57
|
-
|
|
66
|
+
const trimmedContext = input.context?.trim() ?? "";
|
|
67
|
+
if (trimmedContext.length > 1600) {
|
|
58
68
|
throw new PromptsGptApiError("Context must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
|
|
59
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
|
+
}
|
|
60
78
|
const data = await this.request("/api/sdk/v1/prompts/generate", {
|
|
61
79
|
method: "POST",
|
|
62
80
|
body: input,
|
|
81
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_GENERATE_TIMEOUT_MS,
|
|
82
|
+
...options,
|
|
63
83
|
});
|
|
64
84
|
return data.prompt;
|
|
65
85
|
}
|
|
@@ -68,7 +88,13 @@ export class PromptsGptClient {
|
|
|
68
88
|
throw new PromptsGptApiError("Project token is missing.", {
|
|
69
89
|
status: 401,
|
|
70
90
|
code: "AUTH_ERROR",
|
|
71
|
-
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.",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (!this.token.startsWith("pgpt_")) {
|
|
95
|
+
throw new PromptsGptApiError("Token must start with the 'pgpt_' prefix.", {
|
|
96
|
+
code: "VALIDATION_ERROR",
|
|
97
|
+
recovery: "Ensure the token value is copied correctly from the dashboard.",
|
|
72
98
|
});
|
|
73
99
|
}
|
|
74
100
|
const fetchFn = this.fetchImpl;
|
|
@@ -78,48 +104,98 @@ export class PromptsGptClient {
|
|
|
78
104
|
});
|
|
79
105
|
}
|
|
80
106
|
const controller = new AbortController();
|
|
81
|
-
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";
|
|
82
112
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
133
|
+
const response = await fetchFn(url, {
|
|
134
|
+
method,
|
|
135
|
+
headers,
|
|
90
136
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
91
137
|
signal: controller.signal,
|
|
92
138
|
});
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
139
|
+
const requestId = response.headers?.get?.("x-request-id") ?? outgoingRequestId;
|
|
140
|
+
const retryAfterMs = parseRetryAfterHeader(response.headers?.get?.("retry-after"));
|
|
141
|
+
const contentType = response.headers?.get?.("content-type") ?? "";
|
|
142
|
+
if (!isJsonContentType(contentType) && response.status !== 204) {
|
|
143
|
+
throw new PromptsGptApiError(`Unexpected response content-type: ${contentType || "none"}`, {
|
|
144
|
+
status: response.status,
|
|
145
|
+
code: "INVALID_RESPONSE",
|
|
146
|
+
recovery: "Verify the API URL is correct and the server is accessible.",
|
|
147
|
+
requestId,
|
|
148
|
+
retryAfterMs,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
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));
|
|
100
165
|
return this.request(pathname, options, retryCount + 1);
|
|
101
166
|
}
|
|
102
|
-
const error =
|
|
167
|
+
const error = !apiResponse.ok ? apiResponse.error : null;
|
|
103
168
|
throw new PromptsGptApiError(error?.message ?? `Prompts-GPT request failed with ${response.status}.`, {
|
|
104
169
|
status: response.status,
|
|
105
170
|
code: error?.code,
|
|
106
171
|
recovery: error?.recovery,
|
|
172
|
+
requestId,
|
|
173
|
+
retryAfterMs,
|
|
174
|
+
fieldErrors: error?.fieldErrors,
|
|
107
175
|
});
|
|
108
176
|
}
|
|
109
|
-
return
|
|
177
|
+
return apiResponse.data;
|
|
110
178
|
}
|
|
111
179
|
catch (error) {
|
|
112
180
|
if (error instanceof PromptsGptApiError)
|
|
113
181
|
throw error;
|
|
182
|
+
if (!options.omitUserAgent && isUserAgentRuntimeError(error)) {
|
|
183
|
+
return this.request(pathname, { ...options, omitUserAgent: true }, retryCount);
|
|
184
|
+
}
|
|
114
185
|
if (error?.name === "AbortError") {
|
|
115
|
-
|
|
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.`, {
|
|
116
193
|
code: "TIMEOUT",
|
|
117
194
|
recovery: "Check your network connection or increase the timeout.",
|
|
118
195
|
});
|
|
119
196
|
}
|
|
120
|
-
if (retryCount < MAX_RETRIES && isNetworkError(error)) {
|
|
121
|
-
|
|
122
|
-
await sleep((retryCount + 1) * 1000 + jitter);
|
|
197
|
+
if (retryCount < MAX_RETRIES && method === "GET" && isNetworkError(error)) {
|
|
198
|
+
await sleep(computeRetryDelayMs(retryCount));
|
|
123
199
|
return this.request(pathname, options, retryCount + 1);
|
|
124
200
|
}
|
|
125
201
|
throw new PromptsGptApiError(error?.message ?? "Network request failed.", {
|
|
@@ -128,6 +204,7 @@ export class PromptsGptClient {
|
|
|
128
204
|
});
|
|
129
205
|
}
|
|
130
206
|
finally {
|
|
207
|
+
releaseLinkedAbort();
|
|
131
208
|
clearTimeout(timeout);
|
|
132
209
|
}
|
|
133
210
|
}
|
|
@@ -135,12 +212,140 @@ export class PromptsGptClient {
|
|
|
135
212
|
function isNetworkError(error) {
|
|
136
213
|
if (!error)
|
|
137
214
|
return false;
|
|
215
|
+
const name = String(error.name ?? "").toLowerCase();
|
|
138
216
|
const message = String(error.message ?? "").toLowerCase();
|
|
139
|
-
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);
|
|
140
341
|
}
|
|
141
342
|
function sleep(ms) {
|
|
142
343
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
143
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
|
+
}
|
|
144
349
|
let cachedVersion = null;
|
|
145
350
|
function getClientVersion() {
|
|
146
351
|
if (cachedVersion)
|
|
@@ -155,14 +360,21 @@ function getClientVersion() {
|
|
|
155
360
|
return cachedVersion;
|
|
156
361
|
}
|
|
157
362
|
export async function saveLocalCredentials(input) {
|
|
158
|
-
|
|
363
|
+
const trimmedToken = input.token?.trim();
|
|
364
|
+
if (!trimmedToken)
|
|
159
365
|
throw new Error("Token is required.");
|
|
366
|
+
if (trimmedToken.length > 256)
|
|
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.");
|
|
160
370
|
const cwd = input.cwd ?? process.cwd();
|
|
161
371
|
const outDir = path.resolve(cwd, DEFAULT_PROMPTS_GPT_OUT_DIR);
|
|
162
372
|
await mkdir(outDir, { recursive: true });
|
|
163
|
-
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);
|
|
164
376
|
await ensureGitignoreEntry(cwd, `${DEFAULT_PROMPTS_GPT_OUT_DIR}/${PROMPTS_GPT_CREDENTIALS_FILE}`);
|
|
165
|
-
return { credentialsPath
|
|
377
|
+
return { credentialsPath };
|
|
166
378
|
}
|
|
167
379
|
export async function loadLocalCredentials(cwd = process.cwd()) {
|
|
168
380
|
const credentialsPath = path.resolve(cwd, DEFAULT_PROMPTS_GPT_OUT_DIR, PROMPTS_GPT_CREDENTIALS_FILE);
|
|
@@ -170,9 +382,18 @@ export async function loadLocalCredentials(cwd = process.cwd()) {
|
|
|
170
382
|
return null;
|
|
171
383
|
try {
|
|
172
384
|
const parsed = JSON.parse(await readFile(credentialsPath, "utf8"));
|
|
385
|
+
let apiUrl = DEFAULT_PROMPTS_GPT_API_URL;
|
|
386
|
+
if (typeof parsed.apiUrl === "string") {
|
|
387
|
+
try {
|
|
388
|
+
new URL(parsed.apiUrl);
|
|
389
|
+
apiUrl = parsed.apiUrl;
|
|
390
|
+
}
|
|
391
|
+
catch { }
|
|
392
|
+
}
|
|
393
|
+
const rawToken = typeof parsed.token === "string" ? parsed.token.trim() : null;
|
|
173
394
|
return {
|
|
174
|
-
token:
|
|
175
|
-
apiUrl
|
|
395
|
+
token: rawToken || null,
|
|
396
|
+
apiUrl,
|
|
176
397
|
};
|
|
177
398
|
}
|
|
178
399
|
catch {
|
|
@@ -181,7 +402,12 @@ export async function loadLocalCredentials(cwd = process.cwd()) {
|
|
|
181
402
|
}
|
|
182
403
|
export async function syncPrompts(prompts, options = {}) {
|
|
183
404
|
const markdown = await writePromptMarkdownFiles(prompts, options);
|
|
184
|
-
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
|
+
});
|
|
185
411
|
const manifest = await writePromptManifest(prompts, { cwd: options.cwd, outDir: options.outDir });
|
|
186
412
|
return { markdown, agents, manifest };
|
|
187
413
|
}
|
|
@@ -189,18 +415,31 @@ export async function writePromptMarkdownFiles(prompts, options = {}) {
|
|
|
189
415
|
const cwd = options.cwd ?? process.cwd();
|
|
190
416
|
const outDir = assertSafeOutputDir(cwd, options.outDir ?? DEFAULT_PROMPTS_GPT_OUT_DIR);
|
|
191
417
|
const overwrite = Boolean(options.overwrite);
|
|
418
|
+
const normalizedPrompts = assertUniquePromptFileStems(prompts);
|
|
192
419
|
await mkdir(outDir, { recursive: true });
|
|
193
420
|
const written = [];
|
|
194
421
|
const skipped = [];
|
|
195
|
-
for (const prompt of
|
|
196
|
-
const filePath = path.join(outDir, `${
|
|
422
|
+
for (const { prompt, stem } of normalizedPrompts) {
|
|
423
|
+
const filePath = path.join(outDir, `${stem}.md`);
|
|
197
424
|
assertInside(filePath, outDir);
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
442
|
}
|
|
202
|
-
await writeFile(filePath, formatPromptMarkdown(prompt), { flag: overwrite ? "w" : "wx" });
|
|
203
|
-
written.push(filePath);
|
|
204
443
|
}
|
|
205
444
|
await writePromptIndex(prompts, { outDir });
|
|
206
445
|
return { outDir, written, skipped };
|
|
@@ -208,45 +447,72 @@ export async function writePromptMarkdownFiles(prompts, options = {}) {
|
|
|
208
447
|
export async function writeAgentFiles(prompts, options = {}) {
|
|
209
448
|
const cwd = options.cwd ?? process.cwd();
|
|
210
449
|
const targets = normalizeAgentTargets(options.agent ?? options.agents ?? "all");
|
|
211
|
-
const overwrite = Boolean(options.overwriteAgentFiles
|
|
450
|
+
const overwrite = Boolean(options.overwriteAgentFiles);
|
|
212
451
|
const written = [];
|
|
213
452
|
const skipped = [];
|
|
214
|
-
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);
|
|
215
456
|
const dirSet = new Set(allFiles.map((file) => path.dirname(assertSafeProjectFile(cwd, file.path))));
|
|
216
457
|
await Promise.all([...dirSet].map((dir) => mkdir(dir, { recursive: true })));
|
|
217
|
-
|
|
458
|
+
for (const file of allFiles) {
|
|
218
459
|
const filePath = assertSafeProjectFile(cwd, file.path);
|
|
219
460
|
if (file.managedBlock) {
|
|
220
|
-
|
|
461
|
+
let existing = "";
|
|
462
|
+
try {
|
|
463
|
+
existing = await readFile(filePath, "utf8");
|
|
464
|
+
}
|
|
465
|
+
catch { }
|
|
221
466
|
await writeFile(filePath, upsertManagedBlock(existing, file.content));
|
|
222
467
|
written.push(filePath);
|
|
223
|
-
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (overwrite) {
|
|
471
|
+
await writeFile(filePath, file.content);
|
|
472
|
+
written.push(filePath);
|
|
224
473
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
487
|
}
|
|
229
|
-
|
|
230
|
-
written.push(filePath);
|
|
231
|
-
}));
|
|
488
|
+
}
|
|
232
489
|
return { written, skipped, targets };
|
|
233
490
|
}
|
|
234
491
|
export async function writePromptManifest(prompts, options = {}) {
|
|
235
492
|
const cwd = options.cwd ?? process.cwd();
|
|
236
493
|
const outDir = assertSafeOutputDir(cwd, options.outDir ?? DEFAULT_PROMPTS_GPT_OUT_DIR);
|
|
494
|
+
const normalizedPrompts = assertUniquePromptFileStems(prompts);
|
|
237
495
|
await mkdir(outDir, { recursive: true });
|
|
238
496
|
const manifestPath = path.join(outDir, PROMPTS_GPT_MANIFEST_FILE);
|
|
239
497
|
const payload = {
|
|
240
498
|
version: 1,
|
|
241
499
|
generatedAt: new Date().toISOString(),
|
|
242
|
-
count:
|
|
243
|
-
prompts:
|
|
244
|
-
slug:
|
|
500
|
+
count: normalizedPrompts.length,
|
|
501
|
+
prompts: normalizedPrompts.map(({ prompt, stem }) => ({
|
|
502
|
+
slug: stem,
|
|
245
503
|
title: prompt.title,
|
|
504
|
+
summary: prompt.summary ?? "",
|
|
246
505
|
source: prompt.source ?? "library",
|
|
247
506
|
category: prompt.category ?? "Prompt Library",
|
|
507
|
+
difficulty: prompt.difficulty ?? "Intermediate",
|
|
508
|
+
outputType: prompt.outputType ?? "Text",
|
|
248
509
|
supportedTools: prompt.supportedTools ?? [],
|
|
249
|
-
|
|
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),
|
|
250
516
|
})),
|
|
251
517
|
};
|
|
252
518
|
await writeFile(manifestPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
@@ -273,11 +539,8 @@ export function formatPromptMarkdown(prompt) {
|
|
|
273
539
|
"",
|
|
274
540
|
prompt.promptText ?? "",
|
|
275
541
|
"",
|
|
276
|
-
prompt.variables?.length ? "## Variables"
|
|
277
|
-
...(prompt.
|
|
278
|
-
prompt.variables?.length ? "" : "",
|
|
279
|
-
prompt.usageNotes ? "## Usage Notes" : "",
|
|
280
|
-
prompt.usageNotes ?? "",
|
|
542
|
+
...(prompt.variables?.length ? ["## Variables", "", ...prompt.variables.map((variable) => `- \`${variable}\``), ""] : []),
|
|
543
|
+
...(prompt.usageNotes ? ["## Usage Notes", "", prompt.usageNotes] : []),
|
|
281
544
|
"",
|
|
282
545
|
].reduce((acc, line, index, list) => {
|
|
283
546
|
if (line === "" && index > 0 && list[index - 1] === "" && (index < 2 || list[index - 2] === ""))
|
|
@@ -294,10 +557,10 @@ function buildAgentFiles(target, prompts) {
|
|
|
294
557
|
content: [
|
|
295
558
|
"# Prompts-GPT Agent Instructions",
|
|
296
559
|
"",
|
|
297
|
-
"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.",
|
|
298
561
|
"",
|
|
299
562
|
"## Available Prompt Packs",
|
|
300
|
-
...prompts.map((prompt) => `- ${prompt.title}
|
|
563
|
+
...prompts.map(({ prompt, stem }) => `- [${prompt.title}](.prompts-gpt/${stem}.md)`),
|
|
301
564
|
"",
|
|
302
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.",
|
|
303
566
|
"",
|
|
@@ -305,12 +568,12 @@ function buildAgentFiles(target, prompts) {
|
|
|
305
568
|
}];
|
|
306
569
|
}
|
|
307
570
|
if (target === "cursor") {
|
|
308
|
-
return prompts.map((prompt) => ({
|
|
309
|
-
path: `.cursor/rules/prompts-gpt-${
|
|
571
|
+
return prompts.map(({ prompt, stem }) => ({
|
|
572
|
+
path: `.cursor/rules/prompts-gpt-${stem}.mdc`,
|
|
310
573
|
content: [
|
|
311
574
|
"---",
|
|
312
|
-
`description: ${prompt.summary || prompt.title}`,
|
|
313
|
-
"globs:",
|
|
575
|
+
`description: ${yamlScalar(prompt.summary || prompt.title)}`,
|
|
576
|
+
"globs: []",
|
|
314
577
|
"alwaysApply: false",
|
|
315
578
|
"---",
|
|
316
579
|
"",
|
|
@@ -331,9 +594,26 @@ function buildAgentFiles(target, prompts) {
|
|
|
331
594
|
content: [
|
|
332
595
|
"# Prompts-GPT Copilot Instructions",
|
|
333
596
|
"",
|
|
334
|
-
"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`.",
|
|
335
613
|
"",
|
|
336
|
-
|
|
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`.",
|
|
337
617
|
"",
|
|
338
618
|
].join("\n"),
|
|
339
619
|
},
|
|
@@ -344,27 +624,123 @@ function buildAgentFiles(target, prompts) {
|
|
|
344
624
|
];
|
|
345
625
|
}
|
|
346
626
|
if (target === "copilot") {
|
|
347
|
-
return prompts.map((prompt) => ({
|
|
348
|
-
path: `.github/prompts/prompts-gpt-${
|
|
349
|
-
content:
|
|
627
|
+
return prompts.map(({ prompt, stem }) => ({
|
|
628
|
+
path: `.github/prompts/prompts-gpt-${stem}.prompt.md`,
|
|
629
|
+
content: formatCopilotPromptMarkdown(prompt, stem),
|
|
350
630
|
}));
|
|
351
631
|
}
|
|
352
632
|
return [];
|
|
353
633
|
}
|
|
354
634
|
function buildVsCodeSnippets(prompts) {
|
|
355
|
-
return prompts.reduce((snippets, prompt) => {
|
|
635
|
+
return prompts.reduce((snippets, { prompt, stem }) => {
|
|
356
636
|
snippets[`Prompts-GPT: ${prompt.title}`] = {
|
|
357
|
-
prefix: `pgpt-${
|
|
637
|
+
prefix: `pgpt-${stem}`,
|
|
358
638
|
description: prompt.summary || prompt.title,
|
|
359
639
|
body: String(prompt.promptText || "").split(/\r?\n/),
|
|
360
640
|
};
|
|
361
641
|
return snippets;
|
|
362
642
|
}, {});
|
|
363
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
|
+
}
|
|
364
737
|
function upsertManagedBlock(existing, content) {
|
|
365
738
|
const start = "<!-- prompts-gpt:start -->";
|
|
366
739
|
const end = "<!-- prompts-gpt:end -->";
|
|
367
|
-
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}`;
|
|
368
744
|
const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`);
|
|
369
745
|
if (pattern.test(existing)) {
|
|
370
746
|
return `${existing.replace(pattern, block).trimEnd()}\n`;
|
|
@@ -375,6 +751,9 @@ function upsertManagedBlock(existing, content) {
|
|
|
375
751
|
function escapeRegExp(value) {
|
|
376
752
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
377
753
|
}
|
|
754
|
+
function escapeMarkdownLinkText(value) {
|
|
755
|
+
return value.replace(/[[\]]/g, "\\$&");
|
|
756
|
+
}
|
|
378
757
|
async function writePromptIndex(prompts, { outDir }) {
|
|
379
758
|
const indexPath = path.join(outDir, "README.md");
|
|
380
759
|
const content = [
|
|
@@ -383,7 +762,12 @@ async function writePromptIndex(prompts, { outDir }) {
|
|
|
383
762
|
"These prompts were synced by `prompts-gpt`. Re-run `prompts-gpt sync` to refresh Markdown and agent files.",
|
|
384
763
|
"",
|
|
385
764
|
"## Prompts",
|
|
386
|
-
...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
|
+
}),
|
|
387
771
|
"",
|
|
388
772
|
].join("\n");
|
|
389
773
|
assertInside(indexPath, outDir);
|
|
@@ -392,8 +776,11 @@ async function writePromptIndex(prompts, { outDir }) {
|
|
|
392
776
|
function normalizeAgentTargets(value) {
|
|
393
777
|
const raw = Array.isArray(value) ? value.join(",") : String(value ?? "all");
|
|
394
778
|
const targets = raw.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
395
|
-
|
|
396
|
-
|
|
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];
|
|
397
784
|
const validSet = new Set(SUPPORTED_AGENT_TARGETS);
|
|
398
785
|
const invalid = unique.filter((target) => !validSet.has(target));
|
|
399
786
|
if (invalid.length)
|
|
@@ -402,6 +789,9 @@ function normalizeAgentTargets(value) {
|
|
|
402
789
|
}
|
|
403
790
|
function normalizeApiUrl(value) {
|
|
404
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
|
+
}
|
|
405
795
|
url.pathname = "/";
|
|
406
796
|
url.search = "";
|
|
407
797
|
url.hash = "";
|
|
@@ -418,44 +808,60 @@ function safeNormalizeApiUrl(value) {
|
|
|
418
808
|
});
|
|
419
809
|
}
|
|
420
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
|
+
}
|
|
421
815
|
function assertSafeOutputDir(cwd, outDir) {
|
|
422
816
|
const root = path.resolve(cwd);
|
|
423
817
|
const resolved = path.resolve(root, outDir);
|
|
424
|
-
if (resolved
|
|
425
|
-
throw new Error("Output directory must
|
|
818
|
+
if (!isInsideDirectory(resolved, root) || resolved === root) {
|
|
819
|
+
throw new Error("Output directory must be a subdirectory of the current project.");
|
|
426
820
|
}
|
|
427
821
|
return resolved;
|
|
428
822
|
}
|
|
429
823
|
function assertSafeProjectFile(cwd, filePath) {
|
|
430
824
|
const root = path.resolve(cwd);
|
|
431
825
|
const resolved = path.resolve(root, filePath);
|
|
432
|
-
if (!resolved
|
|
826
|
+
if (!isInsideDirectory(resolved, root) || resolved === root) {
|
|
433
827
|
throw new Error("Agent file path must stay inside the current project.");
|
|
434
828
|
}
|
|
435
829
|
return resolved;
|
|
436
830
|
}
|
|
437
831
|
function assertInside(filePath, directory) {
|
|
438
|
-
if (!filePath
|
|
832
|
+
if (!isInsideDirectory(filePath, directory) || filePath === directory) {
|
|
439
833
|
throw new Error("Refusing to write outside the prompt output directory.");
|
|
440
834
|
}
|
|
441
835
|
}
|
|
442
836
|
function safeSlug(value) {
|
|
443
|
-
|
|
837
|
+
const raw = String(value ?? "");
|
|
838
|
+
if (!raw.trim())
|
|
839
|
+
return "prompt";
|
|
840
|
+
const slug = raw
|
|
444
841
|
.toLowerCase()
|
|
842
|
+
.normalize("NFKD")
|
|
843
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
445
844
|
.replace(/[^a-z0-9]+/g, "-")
|
|
446
845
|
.replace(/^-+|-+$/g, "")
|
|
447
|
-
.slice(0, 90)
|
|
846
|
+
.slice(0, 90);
|
|
847
|
+
return slug || "prompt";
|
|
448
848
|
}
|
|
449
849
|
function yamlScalar(value) {
|
|
450
|
-
|
|
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);
|
|
451
855
|
}
|
|
452
856
|
async function ensureGitignoreEntry(cwd, entry) {
|
|
453
857
|
const gitignorePath = path.resolve(cwd, ".gitignore");
|
|
454
858
|
const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, "utf8") : "";
|
|
859
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
455
860
|
const lines = existing.split(/\r?\n/).map((line) => line.trim());
|
|
456
861
|
if (lines.includes(entry))
|
|
457
862
|
return;
|
|
458
|
-
const
|
|
459
|
-
|
|
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}`);
|
|
460
866
|
}
|
|
461
867
|
//# sourceMappingURL=index.js.map
|