@vfarcic/dot-ai 0.174.0 → 0.175.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * User Prompts Loader
3
+ *
4
+ * Loads user-defined prompts from a git repository.
5
+ * Supports any git provider (GitHub, GitLab, Gitea, Forgejo, Bitbucket, etc.)
6
+ *
7
+ * Environment variables:
8
+ * - DOT_AI_USER_PROMPTS_REPO: Git repository URL (required to enable)
9
+ * - DOT_AI_USER_PROMPTS_BRANCH: Branch to use (default: main)
10
+ * - DOT_AI_USER_PROMPTS_PATH: Subdirectory within repo (default: root)
11
+ * - DOT_AI_GIT_TOKEN: Authentication token (optional)
12
+ * - DOT_AI_USER_PROMPTS_CACHE_TTL: Cache TTL in seconds (default: 86400 = 24h)
13
+ */
14
+ import { Logger } from './error-handling';
15
+ import { Prompt } from '../tools/prompts';
16
+ /**
17
+ * Configuration for user prompts repository
18
+ */
19
+ export interface UserPromptsConfig {
20
+ repoUrl: string;
21
+ branch: string;
22
+ subPath: string;
23
+ gitToken?: string;
24
+ cacheTtlSeconds: number;
25
+ }
26
+ /**
27
+ * Cache state for tracking repository freshness
28
+ */
29
+ interface CacheState {
30
+ lastPullTime: number;
31
+ localPath: string;
32
+ }
33
+ /**
34
+ * Read user prompts configuration from environment variables
35
+ * Returns null if DOT_AI_USER_PROMPTS_REPO is not set
36
+ */
37
+ export declare function getUserPromptsConfig(): UserPromptsConfig | null;
38
+ /**
39
+ * Get the cache directory for user prompts
40
+ * Tries project-relative tmp first, falls back to system temp
41
+ */
42
+ export declare function getCacheDirectory(): string;
43
+ /**
44
+ * Insert authentication token into git URL
45
+ * Works with any HTTPS git URL (GitHub, GitLab, Gitea, Bitbucket, etc.)
46
+ */
47
+ export declare function insertTokenInUrl(url: string, token: string): string;
48
+ /**
49
+ * Sanitize URL for logging (remove credentials)
50
+ */
51
+ export declare function sanitizeUrlForLogging(url: string): string;
52
+ /**
53
+ * Load user prompts from the configured git repository
54
+ * Returns empty array if not configured or on error
55
+ */
56
+ export declare function loadUserPrompts(logger: Logger, forceRefresh?: boolean): Promise<Prompt[]>;
57
+ /**
58
+ * Clear the cache state (useful for testing)
59
+ */
60
+ export declare function clearUserPromptsCache(): void;
61
+ /**
62
+ * Get current cache state (for testing/debugging)
63
+ */
64
+ export declare function getUserPromptsCacheState(): CacheState | null;
65
+ export {};
66
+ //# sourceMappingURL=user-prompts-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-prompts-loader.d.ts","sourceRoot":"","sources":["../../src/core/user-prompts-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,MAAM,EAAkB,MAAM,kBAAkB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,UAAU,UAAU;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,IAAI,CAkB/D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAqB1C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CASnE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUzD;AAmJD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC,CAmDnB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,UAAU,GAAG,IAAI,CAE5D"}
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ /**
3
+ * User Prompts Loader
4
+ *
5
+ * Loads user-defined prompts from a git repository.
6
+ * Supports any git provider (GitHub, GitLab, Gitea, Forgejo, Bitbucket, etc.)
7
+ *
8
+ * Environment variables:
9
+ * - DOT_AI_USER_PROMPTS_REPO: Git repository URL (required to enable)
10
+ * - DOT_AI_USER_PROMPTS_BRANCH: Branch to use (default: main)
11
+ * - DOT_AI_USER_PROMPTS_PATH: Subdirectory within repo (default: root)
12
+ * - DOT_AI_GIT_TOKEN: Authentication token (optional)
13
+ * - DOT_AI_USER_PROMPTS_CACHE_TTL: Cache TTL in seconds (default: 86400 = 24h)
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.getUserPromptsConfig = getUserPromptsConfig;
50
+ exports.getCacheDirectory = getCacheDirectory;
51
+ exports.insertTokenInUrl = insertTokenInUrl;
52
+ exports.sanitizeUrlForLogging = sanitizeUrlForLogging;
53
+ exports.loadUserPrompts = loadUserPrompts;
54
+ exports.clearUserPromptsCache = clearUserPromptsCache;
55
+ exports.getUserPromptsCacheState = getUserPromptsCacheState;
56
+ const fs = __importStar(require("fs"));
57
+ const path = __importStar(require("path"));
58
+ const os = __importStar(require("os"));
59
+ const platform_utils_1 = require("./platform-utils");
60
+ const prompts_1 = require("../tools/prompts");
61
+ // In-memory cache state (persists across requests within same process)
62
+ let cacheState = null;
63
+ /**
64
+ * Read user prompts configuration from environment variables
65
+ * Returns null if DOT_AI_USER_PROMPTS_REPO is not set
66
+ */
67
+ function getUserPromptsConfig() {
68
+ const repoUrl = process.env.DOT_AI_USER_PROMPTS_REPO;
69
+ if (!repoUrl) {
70
+ return null;
71
+ }
72
+ // Validate cache TTL - fallback to default if invalid or negative
73
+ const parsedTtl = parseInt(process.env.DOT_AI_USER_PROMPTS_CACHE_TTL || '86400', 10);
74
+ const cacheTtlSeconds = Number.isNaN(parsedTtl) || parsedTtl < 0 ? 86400 : parsedTtl;
75
+ return {
76
+ repoUrl,
77
+ branch: process.env.DOT_AI_USER_PROMPTS_BRANCH || 'main',
78
+ subPath: process.env.DOT_AI_USER_PROMPTS_PATH || '',
79
+ gitToken: process.env.DOT_AI_GIT_TOKEN,
80
+ cacheTtlSeconds,
81
+ };
82
+ }
83
+ /**
84
+ * Get the cache directory for user prompts
85
+ * Tries project-relative tmp first, falls back to system temp
86
+ */
87
+ function getCacheDirectory() {
88
+ // Try project-relative tmp directory first
89
+ const projectTmp = path.join(process.cwd(), 'tmp', 'user-prompts');
90
+ try {
91
+ // Ensure parent tmp directory exists
92
+ const parentTmp = path.join(process.cwd(), 'tmp');
93
+ if (!fs.existsSync(parentTmp)) {
94
+ fs.mkdirSync(parentTmp, { recursive: true });
95
+ }
96
+ // Test if we can write to it
97
+ const testFile = path.join(parentTmp, '.write-test');
98
+ fs.writeFileSync(testFile, 'test');
99
+ fs.unlinkSync(testFile);
100
+ return projectTmp;
101
+ }
102
+ catch {
103
+ // Fall back to system temp (works in Docker/K8s)
104
+ return path.join(os.tmpdir(), 'dot-ai-user-prompts');
105
+ }
106
+ }
107
+ /**
108
+ * Insert authentication token into git URL
109
+ * Works with any HTTPS git URL (GitHub, GitLab, Gitea, Bitbucket, etc.)
110
+ */
111
+ function insertTokenInUrl(url, token) {
112
+ try {
113
+ const parsed = new URL(url);
114
+ parsed.username = token;
115
+ return parsed.toString();
116
+ }
117
+ catch {
118
+ // If URL parsing fails, return original
119
+ return url;
120
+ }
121
+ }
122
+ /**
123
+ * Sanitize URL for logging (remove credentials)
124
+ */
125
+ function sanitizeUrlForLogging(url) {
126
+ try {
127
+ const parsed = new URL(url);
128
+ if (parsed.username)
129
+ parsed.username = '***';
130
+ if (parsed.password)
131
+ parsed.password = '***';
132
+ return parsed.toString();
133
+ }
134
+ catch {
135
+ // If URL parsing fails, do basic sanitization
136
+ return url.replace(/\/\/[^@]+@/, '//***@');
137
+ }
138
+ }
139
+ /**
140
+ * Validate git branch name to prevent command injection
141
+ * Allows alphanumeric characters, hyphens, underscores, slashes, and dots
142
+ */
143
+ function isValidGitBranch(branch) {
144
+ return /^[a-zA-Z0-9_.\-/]+$/.test(branch);
145
+ }
146
+ /**
147
+ * Clone the user prompts repository
148
+ */
149
+ async function cloneRepository(config, localPath, logger) {
150
+ // Validate branch name to prevent command injection
151
+ if (!isValidGitBranch(config.branch)) {
152
+ throw new Error(`Invalid branch name: ${config.branch}`);
153
+ }
154
+ const authUrl = config.gitToken
155
+ ? insertTokenInUrl(config.repoUrl, config.gitToken)
156
+ : config.repoUrl;
157
+ const sanitizedUrl = sanitizeUrlForLogging(config.repoUrl);
158
+ logger.info('Cloning user prompts repository', {
159
+ url: sanitizedUrl,
160
+ branch: config.branch,
161
+ localPath,
162
+ });
163
+ try {
164
+ // Ensure parent directory exists
165
+ const parentDir = path.dirname(localPath);
166
+ if (!fs.existsSync(parentDir)) {
167
+ fs.mkdirSync(parentDir, { recursive: true });
168
+ }
169
+ // Remove existing directory if it exists (clean clone)
170
+ if (fs.existsSync(localPath)) {
171
+ fs.rmSync(localPath, { recursive: true, force: true });
172
+ }
173
+ // Clone with shallow depth for faster operation
174
+ const cloneCommand = `git clone --depth 1 --branch ${config.branch} "${authUrl}" "${localPath}"`;
175
+ await (0, platform_utils_1.execAsync)(cloneCommand);
176
+ logger.info('Successfully cloned user prompts repository', {
177
+ url: sanitizedUrl,
178
+ branch: config.branch,
179
+ });
180
+ }
181
+ catch (error) {
182
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
183
+ // Sanitize error message in case it contains the token
184
+ const sanitizedError = config.gitToken
185
+ ? errorMessage.replaceAll(config.gitToken, '***')
186
+ : errorMessage;
187
+ logger.error('Failed to clone user prompts repository', new Error(sanitizedError), {
188
+ url: sanitizedUrl,
189
+ branch: config.branch,
190
+ });
191
+ throw new Error(`Failed to clone user prompts repository: ${sanitizedError}`);
192
+ }
193
+ }
194
+ /**
195
+ * Pull latest changes from the user prompts repository
196
+ */
197
+ async function pullRepository(config, localPath, logger) {
198
+ const sanitizedUrl = sanitizeUrlForLogging(config.repoUrl);
199
+ logger.debug('Pulling user prompts repository', {
200
+ url: sanitizedUrl,
201
+ localPath,
202
+ });
203
+ try {
204
+ // Set up credentials for pull if token is provided
205
+ if (config.gitToken) {
206
+ const authUrl = insertTokenInUrl(config.repoUrl, config.gitToken);
207
+ await (0, platform_utils_1.execAsync)(`git -C "${localPath}" remote set-url origin "${authUrl}"`);
208
+ }
209
+ await (0, platform_utils_1.execAsync)(`git -C "${localPath}" pull --ff-only`);
210
+ logger.debug('Successfully pulled user prompts repository', {
211
+ url: sanitizedUrl,
212
+ });
213
+ }
214
+ catch (error) {
215
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
216
+ const sanitizedError = config.gitToken
217
+ ? errorMessage.replaceAll(config.gitToken, '***')
218
+ : errorMessage;
219
+ logger.warn('Failed to pull user prompts repository, using cached version', {
220
+ url: sanitizedUrl,
221
+ error: sanitizedError,
222
+ });
223
+ // Don't throw - use cached version
224
+ }
225
+ }
226
+ /**
227
+ * Ensure the repository is cloned and up-to-date
228
+ * Returns the path to the prompts directory within the repository
229
+ */
230
+ async function ensureRepository(config, logger, forceRefresh = false) {
231
+ const localPath = getCacheDirectory();
232
+ const now = Date.now();
233
+ const ttlMs = config.cacheTtlSeconds * 1000;
234
+ // Check if we need to clone or pull
235
+ if (!cacheState || !fs.existsSync(cacheState.localPath)) {
236
+ // First time or cache directory was deleted - clone
237
+ await cloneRepository(config, localPath, logger);
238
+ cacheState = { lastPullTime: now, localPath };
239
+ }
240
+ else if (forceRefresh || now - cacheState.lastPullTime >= ttlMs) {
241
+ // Cache expired or force refresh - pull
242
+ await pullRepository(config, localPath, logger);
243
+ cacheState.lastPullTime = now;
244
+ }
245
+ else {
246
+ logger.debug('Using cached user prompts repository', {
247
+ localPath,
248
+ cacheAge: Math.round((now - cacheState.lastPullTime) / 1000),
249
+ ttl: config.cacheTtlSeconds,
250
+ });
251
+ }
252
+ // Return path to prompts directory (with optional subPath)
253
+ return config.subPath
254
+ ? path.join(localPath, config.subPath)
255
+ : localPath;
256
+ }
257
+ /**
258
+ * Load user prompts from the configured git repository
259
+ * Returns empty array if not configured or on error
260
+ */
261
+ async function loadUserPrompts(logger, forceRefresh = false) {
262
+ const config = getUserPromptsConfig();
263
+ if (!config) {
264
+ logger.debug('User prompts not configured (DOT_AI_USER_PROMPTS_REPO not set)');
265
+ return [];
266
+ }
267
+ try {
268
+ const promptsDir = await ensureRepository(config, logger, forceRefresh);
269
+ if (!fs.existsSync(promptsDir)) {
270
+ logger.warn('User prompts directory not found in repository', {
271
+ path: promptsDir,
272
+ subPath: config.subPath,
273
+ });
274
+ return [];
275
+ }
276
+ // Load all .md files from the prompts directory
277
+ const files = fs.readdirSync(promptsDir);
278
+ const promptFiles = files.filter(file => file.endsWith('.md'));
279
+ const prompts = [];
280
+ for (const file of promptFiles) {
281
+ try {
282
+ const filePath = path.join(promptsDir, file);
283
+ const prompt = (0, prompts_1.loadPromptFile)(filePath, 'user');
284
+ prompts.push(prompt);
285
+ logger.debug('Loaded user prompt', { name: prompt.name, file });
286
+ }
287
+ catch (error) {
288
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
289
+ logger.warn('Failed to load user prompt file, skipping', {
290
+ file,
291
+ error: errorMessage,
292
+ });
293
+ // Continue with other prompts
294
+ }
295
+ }
296
+ logger.info('Loaded user prompts from repository', {
297
+ total: prompts.length,
298
+ url: sanitizeUrlForLogging(config.repoUrl),
299
+ });
300
+ return prompts;
301
+ }
302
+ catch (error) {
303
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
304
+ logger.error('Failed to load user prompts, falling back to built-in only', new Error(errorMessage));
305
+ return [];
306
+ }
307
+ }
308
+ /**
309
+ * Clear the cache state (useful for testing)
310
+ */
311
+ function clearUserPromptsCache() {
312
+ cacheState = null;
313
+ }
314
+ /**
315
+ * Get current cache state (for testing/debugging)
316
+ */
317
+ function getUserPromptsCacheState() {
318
+ return cacheState ? { ...cacheState } : null;
319
+ }
@@ -101,6 +101,14 @@ export declare class RestApiRouter {
101
101
  * Handle resource sync requests from controller
102
102
  */
103
103
  private handleResourceSyncRequest;
104
+ /**
105
+ * Handle prompts list requests
106
+ */
107
+ private handlePromptsListRequest;
108
+ /**
109
+ * Handle prompt get requests
110
+ */
111
+ private handlePromptsGetRequest;
104
112
  /**
105
113
  * Set CORS headers
106
114
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC;;GAEG;AACH,oBAAY,UAAU;IACpB,EAAE,MAAM;IACR,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,kBAAkB,MAAM;IACxB,qBAAqB,MAAM;IAC3B,mBAAmB,MAAM;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,GAAG,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAa;gBAGjC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAoBrC;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAyFzF;;OAEG;IACH,OAAO,CAAC,YAAY;IAyCpB;;OAEG;YACW,mBAAmB;IA2CjC;;OAEG;YACW,mBAAmB;IAgGjC;;OAEG;YACW,iBAAiB;IA8B/B;;OAEG;YACW,yBAAyB;IAgEvC;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;YACW,gBAAgB;IAK9B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIvC;;OAEG;IACH,SAAS,IAAI,aAAa;CAG3B"}
1
+ {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAItC;;GAEG;AACH,oBAAY,UAAU;IACpB,EAAE,MAAM;IACR,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,kBAAkB,MAAM;IACxB,qBAAqB,MAAM;IAC3B,mBAAmB,MAAM;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,GAAG,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAa;gBAGjC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAoBrC;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA2GzF;;OAEG;IACH,OAAO,CAAC,YAAY;IAuDpB;;OAEG;YACW,mBAAmB;IA2CjC;;OAEG;YACW,mBAAmB;IAgGjC;;OAEG;YACW,iBAAiB;IA8B/B;;OAEG;YACW,yBAAyB;IAgEvC;;OAEG;YACW,wBAAwB;IA0CtC;;OAEG;YACW,uBAAuB;IAsDrC;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;YACW,gBAAgB;IAK9B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIvC;;OAEG;IACH,SAAS,IAAI,aAAa;CAG3B"}
@@ -10,6 +10,7 @@ exports.RestApiRouter = exports.HttpStatus = void 0;
10
10
  const node_url_1 = require("node:url");
11
11
  const openapi_generator_1 = require("./openapi-generator");
12
12
  const resource_sync_handler_1 = require("./resource-sync-handler");
13
+ const prompts_1 = require("../tools/prompts");
13
14
  /**
14
15
  * HTTP status codes for REST responses
15
16
  */
@@ -118,6 +119,25 @@ class RestApiRouter {
118
119
  await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown resources endpoint');
119
120
  }
120
121
  break;
122
+ case 'prompts':
123
+ if (req.method === 'GET') {
124
+ await this.handlePromptsListRequest(req, res, requestId);
125
+ }
126
+ else {
127
+ await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for prompts list');
128
+ }
129
+ break;
130
+ case 'prompt':
131
+ if (req.method === 'POST' && pathMatch.promptName) {
132
+ await this.handlePromptsGetRequest(req, res, requestId, pathMatch.promptName, body);
133
+ }
134
+ else if (req.method !== 'POST') {
135
+ await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for prompt get');
136
+ }
137
+ else {
138
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Prompt name is required');
139
+ }
140
+ break;
121
141
  default:
122
142
  await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown API endpoint');
123
143
  }
@@ -139,6 +159,8 @@ class RestApiRouter {
139
159
  // /api/v1/tools/{toolName} -> tool execution
140
160
  // /api/v1/openapi -> OpenAPI spec
141
161
  // /api/v1/resources/sync -> resource sync from controller
162
+ // /api/v1/prompts -> prompts list
163
+ // /api/v1/prompts/{promptName} -> prompt get
142
164
  const basePath = `${this.config.basePath}/${this.config.version}`;
143
165
  if (!pathname.startsWith(basePath)) {
144
166
  return null;
@@ -162,6 +184,16 @@ class RestApiRouter {
162
184
  if (cleanPath === 'resources/sync') {
163
185
  return { endpoint: 'resources', action: 'sync' };
164
186
  }
187
+ // Handle prompts endpoints
188
+ if (cleanPath === 'prompts') {
189
+ return { endpoint: 'prompts' };
190
+ }
191
+ if (cleanPath.startsWith('prompts/')) {
192
+ const promptName = cleanPath.substring(8); // Remove 'prompts/'
193
+ if (promptName) {
194
+ return { endpoint: 'prompt', promptName };
195
+ }
196
+ }
165
197
  return null;
166
198
  }
167
199
  /**
@@ -347,6 +379,69 @@ class RestApiRouter {
347
379
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SYNC_ERROR', 'Resource sync failed', { error: errorMessage });
348
380
  }
349
381
  }
382
+ /**
383
+ * Handle prompts list requests
384
+ */
385
+ async handlePromptsListRequest(req, res, requestId) {
386
+ try {
387
+ this.logger.info('Processing prompts list request', { requestId });
388
+ const result = await (0, prompts_1.handlePromptsListRequest)({}, this.logger, requestId);
389
+ const response = {
390
+ success: true,
391
+ data: result,
392
+ meta: {
393
+ timestamp: new Date().toISOString(),
394
+ requestId,
395
+ version: this.config.version
396
+ }
397
+ };
398
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
399
+ this.logger.info('Prompts list request completed', {
400
+ requestId,
401
+ promptCount: result.prompts?.length || 0
402
+ });
403
+ }
404
+ catch (error) {
405
+ this.logger.error('Prompts list request failed', error instanceof Error ? error : new Error(String(error)), {
406
+ requestId
407
+ });
408
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'PROMPTS_LIST_ERROR', 'Failed to list prompts');
409
+ }
410
+ }
411
+ /**
412
+ * Handle prompt get requests
413
+ */
414
+ async handlePromptsGetRequest(req, res, requestId, promptName, body) {
415
+ try {
416
+ this.logger.info('Processing prompt get request', { requestId, promptName });
417
+ const result = await (0, prompts_1.handlePromptsGetRequest)({ name: promptName, arguments: body?.arguments }, this.logger, requestId);
418
+ const response = {
419
+ success: true,
420
+ data: result,
421
+ meta: {
422
+ timestamp: new Date().toISOString(),
423
+ requestId,
424
+ version: this.config.version
425
+ }
426
+ };
427
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
428
+ this.logger.info('Prompt get request completed', {
429
+ requestId,
430
+ promptName
431
+ });
432
+ }
433
+ catch (error) {
434
+ const errorMessage = error instanceof Error ? error.message : String(error);
435
+ this.logger.error('Prompt get request failed', error instanceof Error ? error : new Error(String(error)), {
436
+ requestId,
437
+ promptName
438
+ });
439
+ // Check if it's a validation error (missing required arguments or prompt not found)
440
+ const isValidationError = errorMessage.includes('Missing required arguments') ||
441
+ errorMessage.includes('Prompt not found');
442
+ await this.sendErrorResponse(res, requestId, isValidationError ? HttpStatus.BAD_REQUEST : HttpStatus.INTERNAL_SERVER_ERROR, isValidationError ? 'VALIDATION_ERROR' : 'PROMPT_GET_ERROR', errorMessage);
443
+ }
444
+ }
350
445
  /**
351
446
  * Set CORS headers
352
447
  */
@@ -2,24 +2,42 @@
2
2
  * MCP Prompts Handler - Manages shared prompt library
3
3
  */
4
4
  import { Logger } from '../core/error-handling';
5
+ export interface PromptArgument {
6
+ name: string;
7
+ description?: string;
8
+ required?: boolean;
9
+ }
5
10
  export interface PromptMetadata {
6
11
  name: string;
7
12
  description: string;
8
13
  category: string;
14
+ arguments?: PromptArgument[];
9
15
  }
10
16
  export interface Prompt {
11
17
  name: string;
12
18
  description: string;
13
19
  content: string;
20
+ arguments?: PromptArgument[];
21
+ source: 'built-in' | 'user';
14
22
  }
15
23
  /**
16
24
  * Loads and parses a prompt file with YAML frontmatter
17
25
  */
18
- export declare function loadPromptFile(filePath: string): Prompt;
26
+ export declare function loadPromptFile(filePath: string, source?: 'built-in' | 'user'): Prompt;
27
+ /**
28
+ * Loads built-in prompts from the shared-prompts directory
29
+ */
30
+ export declare function loadBuiltInPrompts(logger: Logger, baseDir?: string): Prompt[];
31
+ /**
32
+ * Merge built-in and user prompts with collision detection
33
+ * Built-in prompts take precedence over user prompts with the same name
34
+ */
35
+ export declare function mergePrompts(builtInPrompts: Prompt[], userPrompts: Prompt[], logger: Logger): Prompt[];
19
36
  /**
20
- * Loads all prompts from the shared-prompts directory
37
+ * Loads all prompts (built-in + user) with collision detection
38
+ * This is the main entry point for loading prompts
21
39
  */
22
- export declare function loadAllPrompts(logger: Logger, baseDir?: string): Prompt[];
40
+ export declare function loadAllPrompts(logger: Logger, baseDir?: string, forceRefresh?: boolean): Promise<Prompt[]>;
23
41
  /**
24
42
  * Handle prompts/list MCP request
25
43
  */
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/tools/prompts.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAOhD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA4CvD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAoCzE;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,CAsCd;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,CAoEd"}
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/tools/prompts.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAOhD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC;CAC7B;AA8ED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,UAAU,GAAG,MAAmB,GAAG,MAAM,CAmCjG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAoC7E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,cAAc,EAAE,MAAM,EAAE,EACxB,WAAW,EAAE,MAAM,EAAE,EACrB,MAAM,EAAE,MAAM,GACb,MAAM,EAAE,CAgBV;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BnB;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,CA4Cd;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,CAiGd"}
@@ -37,16 +37,90 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.loadPromptFile = loadPromptFile;
40
+ exports.loadBuiltInPrompts = loadBuiltInPrompts;
41
+ exports.mergePrompts = mergePrompts;
40
42
  exports.loadAllPrompts = loadAllPrompts;
41
43
  exports.handlePromptsListRequest = handlePromptsListRequest;
42
44
  exports.handlePromptsGetRequest = handlePromptsGetRequest;
43
45
  const fs = __importStar(require("fs"));
44
46
  const path = __importStar(require("path"));
45
47
  const error_handling_1 = require("../core/error-handling");
48
+ /**
49
+ * Parses YAML frontmatter with support for nested arguments array
50
+ */
51
+ function parseYamlFrontmatter(yaml) {
52
+ const metadata = {};
53
+ const lines = yaml.split('\n');
54
+ let i = 0;
55
+ while (i < lines.length) {
56
+ const line = lines[i];
57
+ // Check for arguments array start
58
+ if (line.match(/^arguments:\s*$/)) {
59
+ const args = [];
60
+ i++;
61
+ // Parse array items (lines starting with " - ")
62
+ while (i < lines.length && lines[i].match(/^\s+-\s/)) {
63
+ const arg = { name: '' };
64
+ // First line of array item: " - name: value"
65
+ const firstLineMatch = lines[i].match(/^\s+-\s+(\w+):\s*(.*)$/);
66
+ if (firstLineMatch) {
67
+ const [, key, value] = firstLineMatch;
68
+ if (key === 'name') {
69
+ arg.name = value.trim().replace(/^["']|["']$/g, '');
70
+ }
71
+ else if (key === 'description') {
72
+ arg.description = value.trim().replace(/^["']|["']$/g, '');
73
+ }
74
+ else if (key === 'required') {
75
+ arg.required = value.trim().toLowerCase() === 'true';
76
+ }
77
+ }
78
+ i++;
79
+ // Continue parsing properties of this array item (lines starting with " ")
80
+ while (i < lines.length && lines[i].match(/^\s{4,}\w+:/)) {
81
+ const propMatch = lines[i].match(/^\s+(\w+):\s*(.*)$/);
82
+ if (propMatch) {
83
+ const [, key, value] = propMatch;
84
+ if (key === 'name') {
85
+ arg.name = value.trim().replace(/^["']|["']$/g, '');
86
+ }
87
+ else if (key === 'description') {
88
+ arg.description = value.trim().replace(/^["']|["']$/g, '');
89
+ }
90
+ else if (key === 'required') {
91
+ arg.required = value.trim().toLowerCase() === 'true';
92
+ }
93
+ }
94
+ i++;
95
+ }
96
+ if (arg.name) {
97
+ args.push(arg);
98
+ }
99
+ }
100
+ if (args.length > 0) {
101
+ metadata.arguments = args;
102
+ }
103
+ }
104
+ else {
105
+ // Simple key-value pair
106
+ const match = line.match(/^([^:]+):\s*(.+)$/);
107
+ if (match) {
108
+ const [, key, value] = match;
109
+ const cleanValue = value.trim().replace(/^["']|["']$/g, '');
110
+ const trimmedKey = key.trim();
111
+ if (trimmedKey !== 'arguments') {
112
+ metadata[trimmedKey] = cleanValue;
113
+ }
114
+ }
115
+ i++;
116
+ }
117
+ }
118
+ return metadata;
119
+ }
46
120
  /**
47
121
  * Loads and parses a prompt file with YAML frontmatter
48
122
  */
49
- function loadPromptFile(filePath) {
123
+ function loadPromptFile(filePath, source = 'built-in') {
50
124
  try {
51
125
  const content = fs.readFileSync(filePath, 'utf8');
52
126
  // Parse YAML frontmatter
@@ -55,18 +129,8 @@ function loadPromptFile(filePath) {
55
129
  throw new Error(`Invalid prompt file format: missing YAML frontmatter in ${filePath}`);
56
130
  }
57
131
  const [, frontmatterYaml, promptContent] = frontmatterMatch;
58
- // Simple YAML parsing for our specific format
59
- const metadata = {};
60
- const lines = frontmatterYaml.split('\n');
61
- for (const line of lines) {
62
- const match = line.match(/^([^:]+):\s*(.+)$/);
63
- if (match) {
64
- const [, key, value] = match;
65
- // Remove quotes if present
66
- const cleanValue = value.trim().replace(/^["']|["']$/g, '');
67
- metadata[key.trim()] = cleanValue;
68
- }
69
- }
132
+ // Parse YAML with support for arguments array
133
+ const metadata = parseYamlFrontmatter(frontmatterYaml);
70
134
  if (!metadata.name || !metadata.description || !metadata.category) {
71
135
  throw new Error(`Missing required metadata in ${filePath}: name, description, category`);
72
136
  }
@@ -74,6 +138,8 @@ function loadPromptFile(filePath) {
74
138
  name: metadata.name,
75
139
  description: metadata.description,
76
140
  content: promptContent.trim(),
141
+ arguments: metadata.arguments,
142
+ source,
77
143
  };
78
144
  }
79
145
  catch (error) {
@@ -81,9 +147,9 @@ function loadPromptFile(filePath) {
81
147
  }
82
148
  }
83
149
  /**
84
- * Loads all prompts from the shared-prompts directory
150
+ * Loads built-in prompts from the shared-prompts directory
85
151
  */
86
- function loadAllPrompts(logger, baseDir) {
152
+ function loadBuiltInPrompts(logger, baseDir) {
87
153
  try {
88
154
  const promptsDir = baseDir ?? path.join(__dirname, '..', '..', 'shared-prompts');
89
155
  if (!fs.existsSync(promptsDir)) {
@@ -96,15 +162,15 @@ function loadAllPrompts(logger, baseDir) {
96
162
  for (const file of promptFiles) {
97
163
  try {
98
164
  const filePath = path.join(promptsDir, file);
99
- const prompt = loadPromptFile(filePath);
165
+ const prompt = loadPromptFile(filePath, 'built-in');
100
166
  prompts.push(prompt);
101
- logger.debug('Loaded prompt', { name: prompt.name, file });
167
+ logger.debug('Loaded built-in prompt', { name: prompt.name, file });
102
168
  }
103
169
  catch (error) {
104
170
  logger.error(`Failed to load prompt file ${file}`, error);
105
171
  }
106
172
  }
107
- logger.info('Loaded prompts from shared library', {
173
+ logger.info('Loaded built-in prompts from shared library', {
108
174
  total: prompts.length,
109
175
  promptsDir,
110
176
  });
@@ -115,18 +181,71 @@ function loadAllPrompts(logger, baseDir) {
115
181
  return [];
116
182
  }
117
183
  }
184
+ /**
185
+ * Merge built-in and user prompts with collision detection
186
+ * Built-in prompts take precedence over user prompts with the same name
187
+ */
188
+ function mergePrompts(builtInPrompts, userPrompts, logger) {
189
+ const builtInNames = new Set(builtInPrompts.map(p => p.name));
190
+ const merged = [...builtInPrompts];
191
+ for (const userPrompt of userPrompts) {
192
+ if (builtInNames.has(userPrompt.name)) {
193
+ logger.warn('User prompt name collision with built-in prompt, skipping user prompt', {
194
+ name: userPrompt.name,
195
+ message: 'Built-in prompt takes precedence',
196
+ });
197
+ continue;
198
+ }
199
+ merged.push(userPrompt);
200
+ }
201
+ return merged;
202
+ }
203
+ /**
204
+ * Loads all prompts (built-in + user) with collision detection
205
+ * This is the main entry point for loading prompts
206
+ */
207
+ async function loadAllPrompts(logger, baseDir, forceRefresh = false) {
208
+ // Load built-in prompts (synchronous)
209
+ const builtInPrompts = loadBuiltInPrompts(logger, baseDir);
210
+ // Load user prompts from git repository (async, graceful failure)
211
+ let userPrompts = [];
212
+ try {
213
+ const { loadUserPrompts } = await Promise.resolve().then(() => __importStar(require('../core/user-prompts-loader.js')));
214
+ userPrompts = await loadUserPrompts(logger, forceRefresh);
215
+ }
216
+ catch (error) {
217
+ logger.debug('User prompts loader not available or failed', {
218
+ error: error instanceof Error ? error.message : 'Unknown error',
219
+ });
220
+ }
221
+ // Merge with collision detection
222
+ const allPrompts = mergePrompts(builtInPrompts, userPrompts, logger);
223
+ logger.info('Loaded all prompts', {
224
+ builtIn: builtInPrompts.length,
225
+ user: userPrompts.length,
226
+ total: allPrompts.length,
227
+ collisions: builtInPrompts.length + userPrompts.length - allPrompts.length,
228
+ });
229
+ return allPrompts;
230
+ }
118
231
  /**
119
232
  * Handle prompts/list MCP request
120
233
  */
121
234
  async function handlePromptsListRequest(args, logger, requestId) {
122
235
  try {
123
236
  logger.info('Processing prompts/list request', { requestId });
124
- const prompts = loadAllPrompts(logger, process.env.NODE_ENV === 'test' ? args?.baseDir : undefined);
125
- // Convert to MCP prompts/list response format
126
- const promptList = prompts.map(prompt => ({
127
- name: prompt.name,
128
- description: prompt.description,
129
- }));
237
+ const prompts = await loadAllPrompts(logger, process.env.NODE_ENV === 'test' ? args?.baseDir : undefined);
238
+ // Convert to MCP prompts/list response format (include arguments if present)
239
+ const promptList = prompts.map(prompt => {
240
+ const item = {
241
+ name: prompt.name,
242
+ description: prompt.description,
243
+ };
244
+ if (prompt.arguments && prompt.arguments.length > 0) {
245
+ item.arguments = prompt.arguments;
246
+ }
247
+ return item;
248
+ });
130
249
  logger.info('Prompts list generated', {
131
250
  requestId,
132
251
  promptCount: promptList.length,
@@ -157,7 +276,7 @@ async function handlePromptsGetRequest(args, logger, requestId) {
157
276
  if (!args.name) {
158
277
  throw new Error('Missing required parameter: name');
159
278
  }
160
- const prompts = loadAllPrompts(logger, process.env.NODE_ENV === 'test' ? args?.baseDir : undefined);
279
+ const prompts = await loadAllPrompts(logger, process.env.NODE_ENV === 'test' ? args?.baseDir : undefined);
161
280
  const prompt = prompts.find(p => p.name === args.name);
162
281
  if (!prompt) {
163
282
  throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, `Prompt not found: ${args.name}`, {
@@ -166,9 +285,30 @@ async function handlePromptsGetRequest(args, logger, requestId) {
166
285
  requestId,
167
286
  });
168
287
  }
288
+ // Validate required arguments if prompt has arguments defined
289
+ const providedArgs = args.arguments || {};
290
+ if (prompt.arguments && prompt.arguments.length > 0) {
291
+ const missingRequired = prompt.arguments
292
+ .filter(arg => arg.required && !providedArgs[arg.name])
293
+ .map(arg => arg.name);
294
+ if (missingRequired.length > 0) {
295
+ throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.MEDIUM, `Missing required arguments: ${missingRequired.join(', ')}`, {
296
+ operation: 'prompts_get',
297
+ component: 'PromptsHandler',
298
+ requestId,
299
+ input: { promptName: prompt.name, missingArguments: missingRequired },
300
+ });
301
+ }
302
+ }
303
+ // Substitute {{argumentName}} placeholders in content
304
+ let processedContent = prompt.content;
305
+ for (const [argName, argValue] of Object.entries(providedArgs)) {
306
+ processedContent = processedContent.replaceAll(`{{${argName}}}`, String(argValue));
307
+ }
169
308
  logger.info('Prompt found and returned', {
170
309
  requestId,
171
310
  promptName: prompt.name,
311
+ argumentsProvided: Object.keys(providedArgs).length,
172
312
  });
173
313
  // Convert to MCP prompts/get response format
174
314
  return {
@@ -178,7 +318,7 @@ async function handlePromptsGetRequest(args, logger, requestId) {
178
318
  role: 'user',
179
319
  content: {
180
320
  type: 'text',
181
- text: prompt.content,
321
+ text: processedContent,
182
322
  },
183
323
  },
184
324
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vfarcic/dot-ai",
3
- "version": "0.174.0",
3
+ "version": "0.175.0",
4
4
  "description": "AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance",
5
5
  "mcpName": "io.github.vfarcic/dot-ai",
6
6
  "main": "dist/index.js",
@@ -2,6 +2,10 @@
2
2
  name: prd-start
3
3
  description: Start working on a PRD implementation
4
4
  category: project-management
5
+ arguments:
6
+ - name: prdNumber
7
+ description: PRD number to start working on (e.g., 306)
8
+ required: false
5
9
  ---
6
10
 
7
11
  # PRD Start - Begin Implementation Work
@@ -20,9 +24,19 @@ You are helping initiate active implementation work on a specific Product Requir
20
24
  4. **Identify Starting Point** - Determine the best first implementation task
21
25
  5. **Begin Implementation** - Launch into actual development work
22
26
 
23
- ## Step 0: Context Awareness Check
27
+ ## Step 0: Check for PRD Argument
24
28
 
25
- **FIRST: Check if PRD context is already clear from recent conversation:**
29
+ **If `prdNumber` argument is provided ({{prdNumber}}):**
30
+ - Skip Step 0 context check and Step 1 auto-detection
31
+ - Use PRD #{{prdNumber}} directly
32
+ - Proceed to Step 2 (PRD Readiness Validation)
33
+
34
+ **If `prdNumber` argument is NOT provided:**
35
+ - Continue to context awareness check below
36
+
37
+ ## Step 0b: Context Awareness Check
38
+
39
+ **Check if PRD context is already clear from recent conversation:**
26
40
 
27
41
  **Skip detection/analysis if recent conversation shows:**
28
42
  - **Recent PRD work discussed** - "We just worked on PRD 29", "Just completed PRD update", etc.
@@ -31,7 +45,7 @@ You are helping initiate active implementation work on a specific Product Requir
31
45
  - **Clear work context** - Discussion of specific features, tasks, or requirements for a known PRD
32
46
 
33
47
  **If context is clear:**
34
- - Skip to Step 2 (PRD Readiness Validation) using the known PRD
48
+ - Skip to Step 2 (PRD Readiness Validation) using the known PRD
35
49
  - Use conversation history to understand current state and recent progress
36
50
  - Proceed directly with readiness validation based on known PRD status
37
51