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