prompts-gpt 0.2.8

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/dist/index.js ADDED
@@ -0,0 +1,1119 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { ensureGitignoreEntry } from "./runtime.js";
5
+ export { DEFAULT_RUN_ARTIFACTS_DIR, DEFAULT_RUN_CONFIG_PATH, ORCHESTRATION_AGENT_PROFILES, DEFAULT_MODELS, normalizeOrchestrationAgent, normalizeConcreteProvider, loadRunConfig, detectProviders, doctor, initRunConfig, runBatch, runPrompt, resolveRunProvider, resolveTimeoutSeconds, resolveDefaultPromptFile, assertPromptFitsLaunch, warnModelProviderMismatch, executeProviderCommandWithRetries, executeProviderCommand, captureWorktreeStatus, buildWorktreeDelta, buildProviderCommand, formatCombinedOutput, appendFileSafe, aggregateTokenUsage, validateRunConfig, discoverWorkspaceAssets, emptyTokenUsage, extractTokenUsageFromLog, hasTokenUsage, isCI, readTokenUsageFromLog, ensureGitignoreEntry, } from "./runtime.js";
6
+ export { sweepPrompt, acquireSweepLock, releaseSweepLock, parseStreamJsonToolCounts, streamJsonHasResult, extractIterationSummary, buildIterationPrompt, runPreFlight, writeSweepManifest, } from "./sweep.js";
7
+ export const DEFAULT_PROMPTS_GPT_API_URL = "https://prompts-gpt.com";
8
+ export const DEFAULT_PROMPTS_GPT_OUT_DIR = ".prompts-gpt";
9
+ export const PROMPTS_GPT_CREDENTIALS_FILE = ".credentials.json";
10
+ export const PROMPTS_GPT_MANIFEST_FILE = "manifest.json";
11
+ export const SUPPORTED_AGENT_TARGETS = [
12
+ "codex",
13
+ "claude-code",
14
+ "cursor",
15
+ "vscode",
16
+ "copilot",
17
+ "continue",
18
+ "gemini-cli",
19
+ "windsurf",
20
+ "cline",
21
+ "junie",
22
+ "amp",
23
+ ];
24
+ export class PromptsGptApiError extends Error {
25
+ status;
26
+ code;
27
+ recovery;
28
+ requestId;
29
+ fieldErrors;
30
+ retryAfterMs;
31
+ constructor(message, options) {
32
+ super(message);
33
+ this.name = "PromptsGptApiError";
34
+ this.status = options?.status ?? 0;
35
+ this.code = options?.code ?? "UNKNOWN_ERROR";
36
+ this.recovery = options?.recovery ?? "Retry the request or create a fresh project token.";
37
+ this.requestId = options?.requestId ?? null;
38
+ this.fieldErrors = options?.fieldErrors ? Object.freeze({ ...options.fieldErrors }) : undefined;
39
+ this.retryAfterMs = options?.retryAfterMs ?? null;
40
+ }
41
+ }
42
+ const DEFAULT_TIMEOUT_MS = 30_000;
43
+ const DEFAULT_GENERATE_TIMEOUT_MS = 60_000;
44
+ const MAX_RETRIES = 2;
45
+ const RETRYABLE_STATUS_CODES = new Set([408, 429, 502, 503, 504]);
46
+ export class PromptsGptClient {
47
+ apiUrl;
48
+ token;
49
+ fetchImpl;
50
+ timeoutMs;
51
+ accountId;
52
+ constructor(options) {
53
+ this.apiUrl = safeNormalizeApiUrl(options.apiUrl);
54
+ this.token = options.token?.trim() || null;
55
+ this.fetchImpl = options.fetch;
56
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
57
+ this.accountId = options.accountId?.trim() || null;
58
+ if (this.token && this.apiUrl.startsWith("http://") && !this.apiUrl.includes("localhost") && !this.apiUrl.includes("127.0.0.1")) {
59
+ throw new PromptsGptApiError("Refusing to send credentials over unencrypted HTTP. Use HTTPS or localhost.", { code: "INSECURE_TRANSPORT", recovery: "Change the API URL to use https://." });
60
+ }
61
+ }
62
+ async getProject(options = {}) {
63
+ const data = await this.request("/api/sdk/v1/project", options);
64
+ return data.project;
65
+ }
66
+ async pullPrompts(query = {}, options = {}) {
67
+ const qs = serializePromptQuery(query);
68
+ const suffix = qs ? `?${qs}` : "";
69
+ const data = await this.request(`/api/sdk/v1/prompts${suffix}`, options);
70
+ if (!Array.isArray(data.prompts)) {
71
+ throw new PromptsGptApiError("Prompts-GPT returned an invalid prompts payload.", {
72
+ code: "INVALID_RESPONSE",
73
+ recovery: "Retry the request. If it keeps failing, verify the SDK API version.",
74
+ });
75
+ }
76
+ return data.prompts;
77
+ }
78
+ async fetchModels(options = {}) {
79
+ const suffix = options.provider ? `?provider=${encodeURIComponent(options.provider)}` : "";
80
+ try {
81
+ const data = await this.request(`/api/sdk/v1/models${suffix}`, options);
82
+ return data.models ?? {};
83
+ }
84
+ catch (err) {
85
+ if (err instanceof PromptsGptApiError && err.status === 404) {
86
+ return {};
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+ async generatePrompt(input, options = {}) {
92
+ const trimmedGoal = input.goal?.trim() ?? "";
93
+ if (trimmedGoal.length < 8) {
94
+ throw new PromptsGptApiError("Goal must be at least 8 characters.", { code: "VALIDATION_ERROR" });
95
+ }
96
+ if (trimmedGoal.length > 160) {
97
+ throw new PromptsGptApiError("Goal must be 160 characters or fewer.", { code: "VALIDATION_ERROR" });
98
+ }
99
+ const trimmedContext = input.context?.trim() ?? "";
100
+ if (trimmedContext.length > 1600) {
101
+ throw new PromptsGptApiError("Context must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
102
+ }
103
+ const trimmedConstraints = input.constraints?.trim() ?? "";
104
+ if (trimmedConstraints.length > 1600) {
105
+ throw new PromptsGptApiError("Constraints must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
106
+ }
107
+ const trimmedDesiredOutput = input.desiredOutput?.trim() ?? "";
108
+ if (trimmedDesiredOutput.length > 1600) {
109
+ throw new PromptsGptApiError("Desired output must be 1600 characters or fewer.", { code: "VALIDATION_ERROR" });
110
+ }
111
+ const data = await this.request("/api/sdk/v1/prompts/generate", {
112
+ method: "POST",
113
+ body: input,
114
+ timeoutMs: options.timeoutMs ?? DEFAULT_GENERATE_TIMEOUT_MS,
115
+ ...options,
116
+ });
117
+ return data.prompt;
118
+ }
119
+ async request(pathname, options = {}, retryCount = 0) {
120
+ if (!this.token) {
121
+ throw new PromptsGptApiError("Project token is missing.", {
122
+ status: 401,
123
+ code: "AUTH_ERROR",
124
+ recovery: "Run `prompts-gpt init --token <token>` or pass `--token`, `--token-stdin`, or `--token-prompt` to the CLI command.",
125
+ });
126
+ }
127
+ if (!this.token.startsWith("pgpt_")) {
128
+ throw new PromptsGptApiError("Token must start with the 'pgpt_' prefix.", {
129
+ code: "VALIDATION_ERROR",
130
+ recovery: "Ensure the token value is copied correctly from the dashboard.",
131
+ });
132
+ }
133
+ const fetchFn = this.fetchImpl;
134
+ if (typeof fetchFn !== "function") {
135
+ throw new PromptsGptApiError("A fetch implementation is required. Use Node 18.18+ or pass fetch in the client options.", {
136
+ code: "MISSING_FETCH",
137
+ });
138
+ }
139
+ const controller = new AbortController();
140
+ const releaseLinkedAbort = linkAbortSignal(options.signal, controller);
141
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? this.timeoutMs);
142
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
143
+ timeout.unref?.();
144
+ const method = options.method ?? "GET";
145
+ try {
146
+ if (options.signal?.aborted) {
147
+ throw new PromptsGptApiError(`Request to ${pathname} was aborted by the caller.`, {
148
+ code: "REQUEST_ABORTED",
149
+ recovery: "Start a new request when you are ready to continue.",
150
+ });
151
+ }
152
+ const url = new URL(pathname, this.apiUrl);
153
+ const headers = {
154
+ authorization: `Bearer ${this.token}`,
155
+ accept: "application/json",
156
+ "x-prompts-gpt-client": `prompts-gpt/${getClientVersion()}`,
157
+ "x-prompts-gpt-build": getBuildFingerprint(),
158
+ };
159
+ if (this.accountId) {
160
+ headers["x-prompts-gpt-account"] = this.accountId;
161
+ }
162
+ if (!options.omitUserAgent) {
163
+ headers["user-agent"] = `prompts-gpt-client/${getClientVersion()} (${process.platform}; node/${process.version})`;
164
+ }
165
+ const outgoingRequestId = options.requestId?.trim() || generateRequestId();
166
+ headers["x-request-id"] = outgoingRequestId;
167
+ if (options.body) {
168
+ headers["content-type"] = "application/json";
169
+ }
170
+ const response = await fetchFn(url, {
171
+ method,
172
+ headers,
173
+ body: options.body ? JSON.stringify(options.body) : undefined,
174
+ signal: controller.signal,
175
+ });
176
+ const requestId = response.headers?.get?.("x-request-id") ?? outgoingRequestId;
177
+ const retryAfterMs = parseRetryAfterHeader(response.headers?.get?.("retry-after"));
178
+ const contentType = response.headers?.get?.("content-type") ?? "";
179
+ if (!isJsonContentType(contentType) && response.status !== 204) {
180
+ throw new PromptsGptApiError(`Unexpected response content-type: ${contentType || "none"}`, {
181
+ status: response.status,
182
+ code: "INVALID_RESPONSE",
183
+ recovery: "Verify the API URL is correct and the server is accessible.",
184
+ requestId,
185
+ retryAfterMs,
186
+ });
187
+ }
188
+ const payload = await parseJsonResponse(response);
189
+ const apiResponse = parseApiResponse(payload);
190
+ if (!response.ok || !apiResponse || !apiResponse.ok) {
191
+ if (!apiResponse) {
192
+ throw new PromptsGptApiError("Prompts-GPT returned an unexpected API envelope.", {
193
+ status: response.status,
194
+ code: "INVALID_RESPONSE",
195
+ recovery: "Retry the request. If it keeps failing, verify the SDK API version.",
196
+ requestId,
197
+ retryAfterMs,
198
+ });
199
+ }
200
+ if (shouldRetryRequest({ method, status: response.status, retryCount, retryAfterMs })) {
201
+ await sleep(computeRetryDelayMs(retryCount, retryAfterMs));
202
+ return this.request(pathname, options, retryCount + 1);
203
+ }
204
+ const error = !apiResponse.ok ? apiResponse.error : null;
205
+ throw new PromptsGptApiError(error?.message ?? `Prompts-GPT request failed with ${response.status}.`, {
206
+ status: response.status,
207
+ code: error?.code,
208
+ recovery: error?.recovery,
209
+ requestId,
210
+ retryAfterMs,
211
+ fieldErrors: error?.fieldErrors,
212
+ });
213
+ }
214
+ return apiResponse.data;
215
+ }
216
+ catch (error) {
217
+ if (error instanceof PromptsGptApiError)
218
+ throw error;
219
+ if (!options.omitUserAgent && isUserAgentRuntimeError(error)) {
220
+ return this.request(pathname, { ...options, omitUserAgent: true }, retryCount);
221
+ }
222
+ if (error?.name === "AbortError") {
223
+ if (options.signal?.aborted) {
224
+ throw new PromptsGptApiError(`Request to ${pathname} was aborted by the caller.`, {
225
+ code: "REQUEST_ABORTED",
226
+ recovery: "Start a new request when you are ready to continue.",
227
+ });
228
+ }
229
+ throw new PromptsGptApiError(`Request to ${pathname} timed out after ${timeoutMs}ms.`, {
230
+ code: "TIMEOUT",
231
+ recovery: "Check your network connection or increase the timeout.",
232
+ });
233
+ }
234
+ if (retryCount < MAX_RETRIES && method === "GET" && isNetworkError(error)) {
235
+ await sleep(computeRetryDelayMs(retryCount));
236
+ return this.request(pathname, options, retryCount + 1);
237
+ }
238
+ throw new PromptsGptApiError(error?.message ?? "Network request failed.", {
239
+ code: "NETWORK_ERROR",
240
+ recovery: "Check your network connection and retry.",
241
+ });
242
+ }
243
+ finally {
244
+ releaseLinkedAbort();
245
+ clearTimeout(timeout);
246
+ }
247
+ }
248
+ }
249
+ function isNetworkError(error) {
250
+ if (!error)
251
+ return false;
252
+ const name = String(error.name ?? "").toLowerCase();
253
+ const message = String(error.message ?? "").toLowerCase();
254
+ return (name === "typeerror" ||
255
+ message.includes("fetch") ||
256
+ message.includes("network") ||
257
+ message.includes("econnrefused") ||
258
+ message.includes("enotfound") ||
259
+ message.includes("eai_again") ||
260
+ message.includes("ecancelled") ||
261
+ message.includes("socket"));
262
+ }
263
+ function isUserAgentRuntimeError(error) {
264
+ const message = String(error?.message ?? "").toLowerCase();
265
+ return message.includes("user-agent") && message.includes("not allowed");
266
+ }
267
+ function linkAbortSignal(signal, controller) {
268
+ if (!signal)
269
+ return () => undefined;
270
+ if (signal.aborted) {
271
+ controller.abort(signal.reason);
272
+ return () => undefined;
273
+ }
274
+ const abort = () => controller.abort(signal.reason);
275
+ signal.addEventListener("abort", abort, { once: true });
276
+ return () => signal.removeEventListener("abort", abort);
277
+ }
278
+ function normalizeTimeoutMs(value) {
279
+ if (!Number.isFinite(value) || value <= 0) {
280
+ throw new PromptsGptApiError("Timeout must be a positive number of milliseconds.", {
281
+ code: "VALIDATION_ERROR",
282
+ recovery: "Provide a timeout greater than 0.",
283
+ });
284
+ }
285
+ const MAX_API_TIMEOUT_MS = 600_000;
286
+ const capped = Math.min(Math.trunc(value), MAX_API_TIMEOUT_MS);
287
+ return capped;
288
+ }
289
+ function serializePromptQuery(query) {
290
+ const params = new URLSearchParams();
291
+ const promptQuery = typeof query.query === "string" && query.query.trim() ? query.query : query.q;
292
+ if (typeof promptQuery === "string" && promptQuery.trim()) {
293
+ params.set("q", promptQuery.trim());
294
+ }
295
+ for (const [key, value] of Object.entries({
296
+ category: query.category,
297
+ tool: query.tool,
298
+ outputType: query.outputType,
299
+ })) {
300
+ if (typeof value === "string" && value.trim()) {
301
+ params.set(key, value.trim());
302
+ }
303
+ }
304
+ if (query.limit !== undefined && query.limit !== null && query.limit !== "") {
305
+ const limit = typeof query.limit === "string" ? Number(query.limit) : query.limit;
306
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
307
+ throw new PromptsGptApiError("Prompt limit must be an integer between 1 and 100.", {
308
+ code: "VALIDATION_ERROR",
309
+ recovery: "Choose a limit between 1 and 100.",
310
+ });
311
+ }
312
+ params.set("limit", String(limit));
313
+ }
314
+ return params.toString();
315
+ }
316
+ function isJsonContentType(contentType) {
317
+ const normalized = contentType.toLowerCase();
318
+ return normalized.includes("application/json") || normalized.includes("+json");
319
+ }
320
+ async function parseJsonResponse(response) {
321
+ if (response.status === 204)
322
+ return { ok: true, data: {} };
323
+ const rawBody = await response.text();
324
+ if (!rawBody.trim())
325
+ return null;
326
+ try {
327
+ return JSON.parse(rawBody);
328
+ }
329
+ catch {
330
+ throw new PromptsGptApiError("Prompts-GPT returned malformed JSON.", {
331
+ status: response.status,
332
+ code: "INVALID_RESPONSE",
333
+ recovery: "Retry the request. If it keeps failing, verify the API URL and server health.",
334
+ requestId: response.headers?.get?.("x-request-id") ?? null,
335
+ });
336
+ }
337
+ }
338
+ function parseApiResponse(payload) {
339
+ if (!payload || typeof payload !== "object" || !("ok" in payload))
340
+ return null;
341
+ if (payload.ok === true && "data" in payload) {
342
+ return payload;
343
+ }
344
+ if (payload.ok === false && "error" in payload) {
345
+ return payload;
346
+ }
347
+ return null;
348
+ }
349
+ function shouldRetryRequest({ method, status, retryCount, retryAfterMs, }) {
350
+ if (retryCount >= MAX_RETRIES)
351
+ return false;
352
+ if (!RETRYABLE_STATUS_CODES.has(status))
353
+ return false;
354
+ if (retryAfterMs !== null && retryAfterMs > 300_000)
355
+ return false;
356
+ if (method !== "GET")
357
+ return false;
358
+ return true;
359
+ }
360
+ function computeRetryDelayMs(retryCount, retryAfterMs = null) {
361
+ const baseDelay = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : (retryCount + 1) * 1_000;
362
+ const jitter = Math.random() * 500;
363
+ return Math.min(baseDelay + jitter, 30_000);
364
+ }
365
+ function parseRetryAfterHeader(value) {
366
+ if (!value)
367
+ return null;
368
+ const trimmed = value.trim();
369
+ if (!trimmed)
370
+ return null;
371
+ const seconds = Number(trimmed);
372
+ if (Number.isFinite(seconds) && seconds >= 0) {
373
+ const ms = seconds * 1_000;
374
+ return Math.min(ms, 600_000);
375
+ }
376
+ const retryAt = Date.parse(trimmed);
377
+ if (Number.isNaN(retryAt))
378
+ return null;
379
+ return Math.min(Math.max(retryAt - Date.now(), 0), 600_000);
380
+ }
381
+ function sleep(ms) {
382
+ return new Promise((resolve) => {
383
+ setTimeout(resolve, ms);
384
+ });
385
+ }
386
+ function generateRequestId() {
387
+ try {
388
+ const crypto = globalThis.crypto;
389
+ const bytes = new Uint8Array(8);
390
+ crypto.getRandomValues(bytes);
391
+ return `pgcli_${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
392
+ }
393
+ catch {
394
+ const hex = Array.from({ length: 8 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, "0")).join("");
395
+ return `pgcli_${hex}`;
396
+ }
397
+ }
398
+ const BUILD_TS = "dev";
399
+ const BUILD_ACCOUNT_ID = "unattributed";
400
+ let cachedBuildFingerprint = null;
401
+ function getBuildFingerprint() {
402
+ if (cachedBuildFingerprint)
403
+ return cachedBuildFingerprint;
404
+ const ts = BUILD_TS === "dev" ? "local" : BUILD_TS;
405
+ const acct = BUILD_ACCOUNT_ID === "unattributed" ? "local" : BUILD_ACCOUNT_ID;
406
+ cachedBuildFingerprint = `${getClientVersion()}/${ts}/${acct}`;
407
+ return cachedBuildFingerprint;
408
+ }
409
+ export function getAttribution() {
410
+ return {
411
+ version: getClientVersion(),
412
+ buildTs: BUILD_TS,
413
+ accountId: BUILD_ACCOUNT_ID,
414
+ };
415
+ }
416
+ let cachedVersion = null;
417
+ function getClientVersion() {
418
+ if (cachedVersion)
419
+ return cachedVersion;
420
+ try {
421
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
422
+ cachedVersion = pkg.version ?? "0.0.0";
423
+ }
424
+ catch {
425
+ cachedVersion = "0.0.0";
426
+ }
427
+ return cachedVersion;
428
+ }
429
+ export async function saveLocalCredentials(input) {
430
+ const trimmedToken = input.token?.trim();
431
+ if (!trimmedToken)
432
+ throw new Error("Token is required.");
433
+ if (trimmedToken.length > 256)
434
+ throw new Error("Token value is too long.");
435
+ if (!trimmedToken.startsWith("pgpt_"))
436
+ throw new Error("Token must start with the 'pgpt_' prefix.");
437
+ const cwd = input.cwd ?? process.cwd();
438
+ const outDir = path.resolve(cwd, DEFAULT_PROMPTS_GPT_OUT_DIR);
439
+ await mkdir(outDir, { recursive: true });
440
+ const credentialsPath = path.join(outDir, PROMPTS_GPT_CREDENTIALS_FILE);
441
+ const credContent = `${JSON.stringify({ token: trimmedToken, apiUrl: normalizeApiUrl(input.apiUrl ?? DEFAULT_PROMPTS_GPT_API_URL) }, null, 2)}\n`;
442
+ if (process.platform === "win32") {
443
+ await writeFile(credentialsPath, credContent);
444
+ }
445
+ else {
446
+ await writeFile(credentialsPath, credContent, { mode: 0o600 });
447
+ await chmod(credentialsPath, 0o600).catch(() => undefined);
448
+ }
449
+ await ensureGitignoreEntry(cwd, `${DEFAULT_PROMPTS_GPT_OUT_DIR}/${PROMPTS_GPT_CREDENTIALS_FILE}`);
450
+ return { credentialsPath };
451
+ }
452
+ export async function loadLocalCredentials(cwd = process.cwd()) {
453
+ const credentialsPath = path.resolve(cwd, DEFAULT_PROMPTS_GPT_OUT_DIR, PROMPTS_GPT_CREDENTIALS_FILE);
454
+ if (!existsSync(credentialsPath))
455
+ return null;
456
+ try {
457
+ const raw = await readFile(credentialsPath, "utf8");
458
+ const parsed = JSON.parse(raw);
459
+ if (!parsed || typeof parsed !== "object")
460
+ return null;
461
+ let apiUrl = DEFAULT_PROMPTS_GPT_API_URL;
462
+ if (typeof parsed.apiUrl === "string") {
463
+ try {
464
+ const url = new URL(parsed.apiUrl);
465
+ if (url.protocol === "https:" || url.protocol === "http:") {
466
+ apiUrl = parsed.apiUrl;
467
+ }
468
+ }
469
+ catch { /* invalid URL — use default */ }
470
+ }
471
+ const rawToken = typeof parsed.token === "string" ? parsed.token.trim() : null;
472
+ if (rawToken && !rawToken.startsWith("pgpt_")) {
473
+ return { token: null, apiUrl };
474
+ }
475
+ return {
476
+ token: rawToken || null,
477
+ apiUrl,
478
+ };
479
+ }
480
+ catch {
481
+ return null;
482
+ }
483
+ }
484
+ export async function syncPrompts(prompts, options = {}) {
485
+ const markdown = await writePromptMarkdownFiles(prompts, options);
486
+ const agents = await writeAgentFiles(prompts, {
487
+ cwd: options.cwd,
488
+ agent: options.agent,
489
+ agents: options.agents,
490
+ overwriteAgentFiles: options.overwrite,
491
+ });
492
+ const manifest = await writePromptManifest(prompts, { cwd: options.cwd, outDir: options.outDir });
493
+ return { markdown, agents, manifest };
494
+ }
495
+ export async function writePromptMarkdownFiles(prompts, options = {}) {
496
+ const cwd = options.cwd ?? process.cwd();
497
+ const outDir = assertSafeOutputDir(cwd, options.outDir ?? DEFAULT_PROMPTS_GPT_OUT_DIR);
498
+ const overwrite = Boolean(options.overwrite);
499
+ const normalizedPrompts = assertUniquePromptFileStems(prompts);
500
+ await mkdir(outDir, { recursive: true });
501
+ const written = [];
502
+ const skipped = [];
503
+ for (const { prompt, stem } of normalizedPrompts) {
504
+ if (/\.\.|[/\\]/.test(stem))
505
+ throw new Error(`Invalid prompt stem: ${stem}`);
506
+ const filePath = path.join(outDir, `${stem}.md`);
507
+ assertInside(filePath, outDir);
508
+ if (overwrite) {
509
+ await writeFile(filePath, formatPromptMarkdown(prompt));
510
+ written.push(filePath);
511
+ }
512
+ else {
513
+ try {
514
+ await writeFile(filePath, formatPromptMarkdown(prompt), { flag: "wx" });
515
+ written.push(filePath);
516
+ }
517
+ catch (err) {
518
+ if (err.code === "EEXIST") {
519
+ skipped.push(filePath);
520
+ }
521
+ else {
522
+ throw err;
523
+ }
524
+ }
525
+ }
526
+ }
527
+ await writePromptIndex(prompts, { outDir });
528
+ return { outDir, written, skipped };
529
+ }
530
+ export async function writeAgentFiles(prompts, options = {}) {
531
+ const cwd = options.cwd ?? process.cwd();
532
+ if (!existsSync(cwd)) {
533
+ throw new Error(`Working directory does not exist: ${cwd}`);
534
+ }
535
+ const targets = normalizeAgentTargets(options.agent ?? options.agents ?? "all");
536
+ const overwrite = Boolean(options.overwriteAgentFiles);
537
+ const written = [];
538
+ const skipped = [];
539
+ const normalizedPrompts = assertUniquePromptFileStems(prompts);
540
+ const allFiles = targets.flatMap((target) => buildAgentFiles(target, normalizedPrompts.filter(({ prompt }) => promptSupportsAgentTarget(prompt, target))).map((file) => ({ ...file, target })));
541
+ assertUniqueAgentFilePaths(allFiles);
542
+ const dirSet = new Set(allFiles.map((file) => path.dirname(assertSafeProjectFile(cwd, file.path))));
543
+ await Promise.all([...dirSet].map((dir) => mkdir(dir, { recursive: true })));
544
+ for (const file of allFiles) {
545
+ const filePath = assertSafeProjectFile(cwd, file.path);
546
+ if (file.managedBlock) {
547
+ let existing = "";
548
+ try {
549
+ existing = await readFile(filePath, "utf8");
550
+ }
551
+ catch { }
552
+ await writeFile(filePath, upsertManagedBlock(existing, file.content));
553
+ written.push(filePath);
554
+ continue;
555
+ }
556
+ if (overwrite) {
557
+ await writeFile(filePath, file.content);
558
+ written.push(filePath);
559
+ }
560
+ else {
561
+ try {
562
+ await writeFile(filePath, file.content, { flag: "wx" });
563
+ written.push(filePath);
564
+ }
565
+ catch (err) {
566
+ if (err.code === "EEXIST") {
567
+ skipped.push(filePath);
568
+ }
569
+ else {
570
+ throw err;
571
+ }
572
+ }
573
+ }
574
+ }
575
+ return { written, skipped, targets };
576
+ }
577
+ export async function writePromptManifest(prompts, options = {}) {
578
+ const cwd = options.cwd ?? process.cwd();
579
+ const outDir = assertSafeOutputDir(cwd, options.outDir ?? DEFAULT_PROMPTS_GPT_OUT_DIR);
580
+ const normalizedPrompts = assertUniquePromptFileStems(prompts);
581
+ await mkdir(outDir, { recursive: true });
582
+ const manifestPath = path.join(outDir, PROMPTS_GPT_MANIFEST_FILE);
583
+ const attribution = getAttribution();
584
+ const payload = {
585
+ version: 1,
586
+ generatedAt: new Date().toISOString(),
587
+ generatedBy: `prompts-gpt@${attribution.version}`,
588
+ accountId: attribution.accountId,
589
+ buildFingerprint: `${attribution.version}/${attribution.accountId}`,
590
+ count: normalizedPrompts.length,
591
+ prompts: normalizedPrompts.map(({ prompt, stem }) => ({
592
+ slug: stem,
593
+ title: prompt.title,
594
+ summary: prompt.summary ?? "",
595
+ source: prompt.source ?? "library",
596
+ category: prompt.category ?? "Prompt Library",
597
+ difficulty: prompt.difficulty ?? "Intermediate",
598
+ outputType: prompt.outputType ?? "Text",
599
+ supportedTools: prompt.supportedTools ?? [],
600
+ agentTargets: normalizePromptAgentTargets(prompt),
601
+ variables: prompt.variables ?? [],
602
+ tags: prompt.tags ?? [],
603
+ recommendedPath: prompt.recommendedPath ?? null,
604
+ file: `${stem}.md`,
605
+ files: buildDiscoverablePromptFiles(stem, prompt),
606
+ })),
607
+ };
608
+ await writeFile(manifestPath, `${JSON.stringify(payload, null, 2)}\n`);
609
+ return { manifestPath, manifest: payload };
610
+ }
611
+ export function formatPromptMarkdown(prompt) {
612
+ return [
613
+ "---",
614
+ `title: ${yamlScalar(prompt.title)}`,
615
+ `slug: ${yamlScalar(prompt.slug)}`,
616
+ `source: ${yamlScalar(prompt.source ?? "library")}`,
617
+ `category: ${yamlScalar(prompt.category ?? "Prompt Library")}`,
618
+ `difficulty: ${yamlScalar(prompt.difficulty ?? "Intermediate")}`,
619
+ `outputType: ${yamlScalar(prompt.outputType ?? "Text")}`,
620
+ `supportedTools: [${(prompt.supportedTools ?? []).map(yamlScalar).join(", ")}]`,
621
+ `tags: [${(prompt.tags ?? []).map(yamlScalar).join(", ")}]`,
622
+ "---",
623
+ "",
624
+ `# ${prompt.title}`,
625
+ "",
626
+ prompt.summary ?? "",
627
+ "",
628
+ "## Prompt",
629
+ "",
630
+ prompt.promptText ?? "",
631
+ "",
632
+ ...(prompt.variables?.length ? ["## Variables", "", ...prompt.variables.map((variable) => `- \`${variable}\``), ""] : []),
633
+ ...(prompt.usageNotes ? ["## Usage Notes", "", prompt.usageNotes] : []),
634
+ "",
635
+ ].reduce((acc, line, index, list) => {
636
+ if (line === "" && index > 0 && list[index - 1] === "" && (index < 2 || list[index - 2] === ""))
637
+ return acc;
638
+ acc.push(line);
639
+ return acc;
640
+ }, []).join("\n");
641
+ }
642
+ function buildAgentFiles(target, prompts) {
643
+ if (target === "codex") {
644
+ return [{
645
+ path: "AGENTS.md",
646
+ managedBlock: true,
647
+ content: [
648
+ "# Prompts-GPT Agent Instructions",
649
+ "",
650
+ "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.",
651
+ "",
652
+ "## Available Prompt Packs",
653
+ ...prompts.map(({ prompt, stem }) => `- [${prompt.title}](.prompts-gpt/${stem}.md)`),
654
+ "",
655
+ "When a prompt pack is relevant, load it, adapt variables to the current task, and keep verification tied to the prompt's acceptance criteria.",
656
+ "",
657
+ ].join("\n"),
658
+ }];
659
+ }
660
+ if (target === "claude-code") {
661
+ return [{
662
+ path: "CLAUDE.md",
663
+ managedBlock: true,
664
+ content: [
665
+ "# Prompts-GPT Claude Code Instructions",
666
+ "",
667
+ "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.",
668
+ "",
669
+ "## Available Prompt Packs",
670
+ ...prompts.map(({ prompt, stem }) => `- [${prompt.title}](.prompts-gpt/${stem}.md)`),
671
+ "",
672
+ "When a prompt pack is relevant, load it, adapt variables to the current task, and keep verification tied to the prompt's acceptance criteria.",
673
+ "",
674
+ ].join("\n"),
675
+ }];
676
+ }
677
+ if (target === "cursor") {
678
+ return prompts.flatMap(({ prompt, stem }) => ([
679
+ {
680
+ path: `.cursor/rules/prompts-gpt-${stem}.mdc`,
681
+ content: [
682
+ "---",
683
+ `description: ${yamlScalar(prompt.summary || prompt.title)}`,
684
+ "globs: []",
685
+ "alwaysApply: false",
686
+ "---",
687
+ "",
688
+ `# ${prompt.title}`,
689
+ "",
690
+ prompt.promptText,
691
+ "",
692
+ prompt.usageNotes ? `Usage notes: ${prompt.usageNotes}` : "",
693
+ "",
694
+ ].filter(Boolean).join("\n"),
695
+ },
696
+ {
697
+ path: `.cursor/commands/prompts-gpt-${stem}.md`,
698
+ content: formatCursorCommandMarkdown(prompt, stem),
699
+ },
700
+ ]));
701
+ }
702
+ if (target === "vscode") {
703
+ return [
704
+ {
705
+ path: ".github/copilot-instructions.md",
706
+ managedBlock: true,
707
+ content: [
708
+ "# Prompts-GPT Copilot Instructions",
709
+ "",
710
+ "Use [../.prompts-gpt/manifest.json](../.prompts-gpt/manifest.json) and the linked `.prompts-gpt/*.md` prompt packs as reusable repository context.",
711
+ "",
712
+ ...prompts.map(({ prompt, stem }) => `- [${prompt.title}](../.prompts-gpt/${stem}.md)`),
713
+ "",
714
+ ].join("\n"),
715
+ },
716
+ {
717
+ path: ".github/instructions/prompts-gpt.instructions.md",
718
+ content: [
719
+ "---",
720
+ 'applyTo: "AGENTS.md,.prompts-gpt/**/*.md,.github/copilot-instructions.md,.github/prompts/**/*.prompt.md,.cursor/rules/**/*.mdc,.cursor/commands/**/*.md,.vscode/prompts-gpt.code-snippets"',
721
+ "---",
722
+ "",
723
+ "# Prompts-GPT managed artifacts",
724
+ "",
725
+ "These files are generated or refreshed by `prompts-gpt sync`.",
726
+ "",
727
+ "- Treat `.prompts-gpt/manifest.json` as the source of truth for discoverable prompt packs and generated agent files.",
728
+ "- Prefer updating the upstream prompt pack or rerunning sync instead of manually editing generated agent artifacts.",
729
+ "- Preserve the managed `prompts-gpt` blocks inside `AGENTS.md` and `.github/copilot-instructions.md`.",
730
+ "",
731
+ ].join("\n"),
732
+ },
733
+ {
734
+ path: ".vscode/prompts-gpt.code-snippets",
735
+ content: JSON.stringify(buildVsCodeSnippets(prompts), null, 2) + "\n",
736
+ },
737
+ ];
738
+ }
739
+ if (target === "copilot") {
740
+ return prompts.map(({ prompt, stem }) => ({
741
+ path: `.github/prompts/prompts-gpt-${stem}.prompt.md`,
742
+ content: formatCopilotPromptMarkdown(prompt, stem),
743
+ }));
744
+ }
745
+ if (target === "continue") {
746
+ return prompts.map(({ prompt, stem }) => ({
747
+ path: `.continue/rules/prompts-gpt-${stem}.md`,
748
+ content: [
749
+ `# ${prompt.title}`,
750
+ "",
751
+ `[Canonical prompt pack](../../.prompts-gpt/${stem}.md)`,
752
+ "",
753
+ prompt.summary ?? "",
754
+ "",
755
+ prompt.promptText ?? "",
756
+ "",
757
+ prompt.usageNotes ? `Usage notes: ${prompt.usageNotes}` : "",
758
+ "",
759
+ ].filter(Boolean).join("\n"),
760
+ }));
761
+ }
762
+ if (target === "gemini-cli") {
763
+ return [{
764
+ path: "GEMINI.md",
765
+ managedBlock: true,
766
+ content: [
767
+ "# Prompts-GPT Gemini CLI Instructions",
768
+ "",
769
+ "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.",
770
+ "",
771
+ "## Available Prompt Packs",
772
+ ...prompts.map(({ prompt, stem }) => `- [${prompt.title}](.prompts-gpt/${stem}.md)`),
773
+ "",
774
+ "When a prompt pack is relevant, load it, adapt variables to the current task, and keep verification tied to the prompt's acceptance criteria.",
775
+ "",
776
+ ].join("\n"),
777
+ }];
778
+ }
779
+ if (target === "windsurf") {
780
+ return prompts.map(({ prompt, stem }) => ({
781
+ path: `.windsurf/rules/prompts-gpt-${stem}.md`,
782
+ content: [
783
+ `# ${prompt.title}`,
784
+ "",
785
+ `[Canonical prompt pack](../../.prompts-gpt/${stem}.md)`,
786
+ "",
787
+ prompt.summary ?? "",
788
+ "",
789
+ prompt.promptText ?? "",
790
+ "",
791
+ "Use this as a workspace rule for Cascade or Devin Local when the prompt pack matches the task.",
792
+ prompt.usageNotes ? `Usage notes: ${prompt.usageNotes}` : "",
793
+ "",
794
+ ].filter(Boolean).join("\n"),
795
+ }));
796
+ }
797
+ if (target === "cline") {
798
+ return prompts.map(({ prompt, stem }) => ({
799
+ path: `.clinerules/prompts-gpt-${stem}.md`,
800
+ content: [
801
+ `# ${prompt.title}`,
802
+ "",
803
+ `[Canonical prompt pack](../.prompts-gpt/${stem}.md)`,
804
+ "",
805
+ prompt.summary ?? "",
806
+ "",
807
+ prompt.promptText ?? "",
808
+ "",
809
+ "Enable this rule when the current Cline task matches the linked prompt pack.",
810
+ prompt.usageNotes ? `Usage notes: ${prompt.usageNotes}` : "",
811
+ "",
812
+ ].filter(Boolean).join("\n"),
813
+ }));
814
+ }
815
+ if (target === "junie") {
816
+ return [{
817
+ path: ".junie/guidelines.md",
818
+ managedBlock: true,
819
+ content: [
820
+ "# Prompts-GPT Junie Guidelines",
821
+ "",
822
+ "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.",
823
+ "",
824
+ "## Available Prompt Packs",
825
+ ...prompts.map(({ prompt, stem }) => `- [${prompt.title}](../.prompts-gpt/${stem}.md)`),
826
+ "",
827
+ "When a prompt pack is relevant, load it, adapt variables to the current task, and keep verification tied to the prompt's acceptance criteria.",
828
+ "",
829
+ ].join("\n"),
830
+ }];
831
+ }
832
+ if (target === "amp") {
833
+ return [{
834
+ path: "AGENT.md",
835
+ managedBlock: true,
836
+ content: [
837
+ "# Prompts-GPT Amp Instructions",
838
+ "",
839
+ "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.",
840
+ "",
841
+ "## Available Prompt Packs",
842
+ ...prompts.map(({ prompt, stem }) => `- [${prompt.title}](.prompts-gpt/${stem}.md)`),
843
+ "",
844
+ "When a prompt pack is relevant, load it, adapt variables to the current task, and keep verification tied to the prompt's acceptance criteria.",
845
+ "",
846
+ ].join("\n"),
847
+ }];
848
+ }
849
+ return [];
850
+ }
851
+ function buildVsCodeSnippets(prompts) {
852
+ return prompts.reduce((snippets, { prompt, stem }) => {
853
+ snippets[`Prompts-GPT: ${prompt.title}`] = {
854
+ prefix: `pgpt-${stem}`,
855
+ description: prompt.summary || prompt.title,
856
+ body: String(prompt.promptText || "").split(/\r?\n/),
857
+ };
858
+ return snippets;
859
+ }, {});
860
+ }
861
+ function normalizePromptAgentTargets(prompt) {
862
+ const validSet = new Set(SUPPORTED_AGENT_TARGETS);
863
+ const declaredTargets = Array.isArray(prompt.agentTargets)
864
+ ? prompt.agentTargets.map((target) => String(target).trim().toLowerCase()).filter(Boolean)
865
+ : [];
866
+ return [...new Set(declaredTargets.filter((target) => validSet.has(target)))];
867
+ }
868
+ function promptSupportsAgentTarget(prompt, target) {
869
+ const declaredTargets = normalizePromptAgentTargets(prompt);
870
+ return declaredTargets.length === 0 || declaredTargets.includes(target);
871
+ }
872
+ function assertUniquePromptFileStems(prompts) {
873
+ const byStem = new Map();
874
+ const normalizedPrompts = prompts.map((prompt) => {
875
+ const stem = safeSlug(prompt.slug || prompt.title);
876
+ const matches = byStem.get(stem) ?? [];
877
+ matches.push(prompt);
878
+ byStem.set(stem, matches);
879
+ return { prompt, stem };
880
+ });
881
+ const collisions = [...byStem.entries()].filter(([, items]) => items.length > 1);
882
+ if (collisions.length > 0) {
883
+ const details = collisions
884
+ .map(([stem, items]) => `${stem} <- ${items.map((item) => item.title || item.slug || "Untitled prompt").join(", ")}`)
885
+ .join("; ");
886
+ throw new Error(`Refusing to sync prompts with colliding file names after slug normalization: ${details}`);
887
+ }
888
+ return normalizedPrompts;
889
+ }
890
+ function assertUniqueAgentFilePaths(files) {
891
+ const pathCounts = new Map();
892
+ for (const file of files) {
893
+ const targets = pathCounts.get(file.path) ?? [];
894
+ targets.push(file.target);
895
+ pathCounts.set(file.path, targets);
896
+ }
897
+ const collisions = [...pathCounts.entries()].filter(([, targets]) => targets.length > 1 && !targets.every((target) => target === targets[0]));
898
+ if (collisions.length > 0) {
899
+ const details = collisions.map(([filePath, targets]) => `${filePath} <- ${targets.join(", ")}`).join("; ");
900
+ throw new Error(`Refusing to sync duplicate agent file paths: ${details}`);
901
+ }
902
+ }
903
+ function buildDiscoverablePromptFiles(stem, prompt) {
904
+ const agentTargets = new Set(normalizePromptAgentTargets(prompt));
905
+ const supports = (target) => agentTargets.size === 0 || agentTargets.has(target);
906
+ return {
907
+ markdown: `.prompts-gpt/${stem}.md`,
908
+ codexInstructions: supports("codex") ? "AGENTS.md" : null,
909
+ claudeCodeInstructions: supports("claude-code") ? "CLAUDE.md" : null,
910
+ cursorRule: supports("cursor") ? `.cursor/rules/prompts-gpt-${stem}.mdc` : null,
911
+ cursorCommand: supports("cursor") ? `.cursor/commands/prompts-gpt-${stem}.md` : null,
912
+ vscodeInstructions: supports("vscode") ? ".github/copilot-instructions.md" : null,
913
+ copilotPathInstructions: supports("vscode") ? ".github/instructions/prompts-gpt.instructions.md" : null,
914
+ vscodeSnippets: supports("vscode") ? ".vscode/prompts-gpt.code-snippets" : null,
915
+ copilotPrompt: supports("copilot") ? `.github/prompts/prompts-gpt-${stem}.prompt.md` : null,
916
+ continueRule: supports("continue") ? `.continue/rules/prompts-gpt-${stem}.md` : null,
917
+ geminiInstructions: supports("gemini-cli") ? "GEMINI.md" : null,
918
+ windsurfRule: supports("windsurf") ? `.windsurf/rules/prompts-gpt-${stem}.md` : null,
919
+ clineRule: supports("cline") ? `.clinerules/prompts-gpt-${stem}.md` : null,
920
+ junieGuidelines: supports("junie") ? ".junie/guidelines.md" : null,
921
+ ampInstructions: supports("amp") ? "AGENT.md" : null,
922
+ };
923
+ }
924
+ function formatCursorCommandMarkdown(prompt, stem) {
925
+ const lines = [
926
+ `# ${prompt.title}`,
927
+ "",
928
+ `[Canonical prompt pack](../../.prompts-gpt/${stem}.md)`,
929
+ "",
930
+ prompt.summary ?? "",
931
+ "",
932
+ "## Task",
933
+ "",
934
+ prompt.promptText ?? "",
935
+ ];
936
+ if (prompt.variables?.length) {
937
+ lines.push("", "## Inputs", "");
938
+ for (const variable of prompt.variables) {
939
+ lines.push(`- ${String(variable).replace(/[{}$]/g, "")}`);
940
+ }
941
+ }
942
+ if (prompt.usageNotes) {
943
+ lines.push("", "## Usage Notes", "", prompt.usageNotes);
944
+ }
945
+ lines.push("", "Verify the output against `.prompts-gpt/manifest.json` and the linked canonical prompt pack.", "");
946
+ return lines
947
+ .reduce((acc, line, index, list) => {
948
+ if (line === "" && index > 0 && list[index - 1] === "" && (index < 2 || list[index - 2] === ""))
949
+ return acc;
950
+ acc.push(line);
951
+ return acc;
952
+ }, [])
953
+ .join("\n");
954
+ }
955
+ function formatCopilotPromptMarkdown(prompt, stem) {
956
+ const sections = [
957
+ "---",
958
+ "mode: agent",
959
+ `description: ${yamlScalar(prompt.summary || prompt.title)}`,
960
+ "---",
961
+ "",
962
+ `# ${prompt.title}`,
963
+ "",
964
+ `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).`,
965
+ "",
966
+ prompt.summary ?? "",
967
+ "",
968
+ "## Task",
969
+ "",
970
+ prompt.promptText ?? "",
971
+ ];
972
+ if (prompt.variables?.length) {
973
+ sections.push("", "## Inputs", "");
974
+ for (const variable of prompt.variables) {
975
+ const sanitizedName = safeSlug(variable) || "value";
976
+ const sanitizedLabel = String(variable).replace(/[{}$]/g, "");
977
+ sections.push(`- ${sanitizedLabel}: \${input:${sanitizedName}:Provide ${sanitizedLabel}}`);
978
+ }
979
+ }
980
+ if (prompt.usageNotes) {
981
+ sections.push("", "## Usage Notes", "", prompt.usageNotes);
982
+ }
983
+ sections.push("");
984
+ return sections
985
+ .reduce((acc, line, index, list) => {
986
+ if (line === "" && index > 0 && list[index - 1] === "" && (index < 2 || list[index - 2] === ""))
987
+ return acc;
988
+ acc.push(line);
989
+ return acc;
990
+ }, [])
991
+ .join("\n");
992
+ }
993
+ function upsertManagedBlock(existing, content) {
994
+ const start = "<!-- prompts-gpt:start -->";
995
+ const end = "<!-- prompts-gpt:end -->";
996
+ const sanitizedContent = content.trim()
997
+ .replace(/<!-- prompts-gpt:start -->/g, "")
998
+ .replace(/<!-- prompts-gpt:end -->/g, "");
999
+ const block = `${start}\n${sanitizedContent}\n${end}`;
1000
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`);
1001
+ if (pattern.test(existing)) {
1002
+ return `${existing.replace(pattern, block).trimEnd()}\n`;
1003
+ }
1004
+ const prefix = existing.trimEnd();
1005
+ return `${prefix ? `${prefix}\n\n` : ""}${block}\n`;
1006
+ }
1007
+ function escapeRegExp(value) {
1008
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1009
+ }
1010
+ function escapeMarkdownLinkText(value) {
1011
+ return value.replace(/[[\]]/g, "\\$&");
1012
+ }
1013
+ async function writePromptIndex(prompts, { outDir }) {
1014
+ const indexPath = path.resolve(outDir, "README.md");
1015
+ assertInside(indexPath, path.resolve(outDir));
1016
+ const managedContent = [
1017
+ "# Prompts-GPT Prompt Packs",
1018
+ "",
1019
+ "These prompts were synced by `prompts-gpt`. Re-run `prompts-gpt sync` to refresh Markdown and agent files.",
1020
+ "",
1021
+ "## Prompts",
1022
+ ...prompts.map((prompt) => {
1023
+ const agentTargets = normalizePromptAgentTargets(prompt);
1024
+ const suffix = agentTargets.length ? ` Targets: ${agentTargets.join(", ")}.` : "";
1025
+ const escapedTitle = escapeMarkdownLinkText(prompt.title);
1026
+ return `- [${escapedTitle}](./${safeSlug(prompt.slug || prompt.title)}.md) - ${prompt.summary ?? ""}${suffix}`;
1027
+ }),
1028
+ "",
1029
+ ].join("\n");
1030
+ let existing = "";
1031
+ try {
1032
+ existing = await readFile(indexPath, "utf8");
1033
+ }
1034
+ catch { }
1035
+ await writeFile(indexPath, upsertManagedBlock(existing, managedContent));
1036
+ }
1037
+ function normalizeAgentTargets(value) {
1038
+ const raw = Array.isArray(value) ? value.join(",") : String(value ?? "all");
1039
+ const targets = raw.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
1040
+ if (targets.includes("all"))
1041
+ return [...SUPPORTED_AGENT_TARGETS];
1042
+ const unique = [...new Set(targets)];
1043
+ if (unique.length === 0)
1044
+ return [...SUPPORTED_AGENT_TARGETS];
1045
+ const validSet = new Set(SUPPORTED_AGENT_TARGETS);
1046
+ const invalid = unique.filter((target) => !validSet.has(target));
1047
+ if (invalid.length)
1048
+ throw new Error(`Unsupported agent target: ${invalid.join(", ")}. Use ${SUPPORTED_AGENT_TARGETS.join(", ")}, or all.`);
1049
+ return unique;
1050
+ }
1051
+ function normalizeApiUrl(value) {
1052
+ const url = new URL(value);
1053
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
1054
+ throw new Error(`API URL must use https or http, got ${url.protocol}`);
1055
+ }
1056
+ url.pathname = "/";
1057
+ url.search = "";
1058
+ url.hash = "";
1059
+ return url.toString().replace(/\/+$/, "");
1060
+ }
1061
+ function safeNormalizeApiUrl(value) {
1062
+ try {
1063
+ return normalizeApiUrl(value);
1064
+ }
1065
+ catch {
1066
+ throw new PromptsGptApiError(`Invalid API URL: ${value}`, {
1067
+ code: "INVALID_URL",
1068
+ recovery: `Provide a valid URL like ${DEFAULT_PROMPTS_GPT_API_URL}.`,
1069
+ });
1070
+ }
1071
+ }
1072
+ function isInsideDirectory(child, parent) {
1073
+ const normalizedParent = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`;
1074
+ return child.startsWith(normalizedParent) || child === parent;
1075
+ }
1076
+ function assertSafeOutputDir(cwd, outDir) {
1077
+ const root = path.resolve(cwd);
1078
+ const resolved = path.resolve(root, outDir);
1079
+ if (!isInsideDirectory(resolved, root) || resolved === root) {
1080
+ throw new Error("Output directory must be a subdirectory of the current project.");
1081
+ }
1082
+ return resolved;
1083
+ }
1084
+ function assertSafeProjectFile(cwd, filePath) {
1085
+ const root = path.resolve(cwd);
1086
+ const resolved = path.resolve(root, filePath);
1087
+ if (!isInsideDirectory(resolved, root) || resolved === root) {
1088
+ throw new Error("Agent file path must stay inside the current project.");
1089
+ }
1090
+ return resolved;
1091
+ }
1092
+ function assertInside(filePath, directory) {
1093
+ if (!isInsideDirectory(filePath, directory) || filePath === directory) {
1094
+ throw new Error("Refusing to write outside the prompt output directory.");
1095
+ }
1096
+ }
1097
+ function safeSlug(value) {
1098
+ const raw = String(value ?? "").trim();
1099
+ if (!raw)
1100
+ return "prompt";
1101
+ const slug = raw
1102
+ .toLowerCase()
1103
+ .normalize("NFKD")
1104
+ .replace(/[\u0300-\u036f]/g, "")
1105
+ .replace(/[^a-z0-9]+/g, "-")
1106
+ .replace(/^-+|-+$/g, "")
1107
+ .slice(0, 90);
1108
+ if (!slug || slug === "-")
1109
+ return "prompt";
1110
+ return slug;
1111
+ }
1112
+ function yamlScalar(value) {
1113
+ const s = String(value ?? "");
1114
+ if (s.includes("\n") || s.includes("\r")) {
1115
+ return JSON.stringify(s.replace(/\r\n?/g, "\n"));
1116
+ }
1117
+ return JSON.stringify(s);
1118
+ }
1119
+ //# sourceMappingURL=index.js.map