@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.
- package/dist/core/user-prompts-loader.d.ts +66 -0
- package/dist/core/user-prompts-loader.d.ts.map +1 -0
- package/dist/core/user-prompts-loader.js +319 -0
- package/dist/interfaces/rest-api.d.ts +8 -0
- package/dist/interfaces/rest-api.d.ts.map +1 -1
- package/dist/interfaces/rest-api.js +95 -0
- package/dist/tools/prompts.d.ts +21 -3
- package/dist/tools/prompts.d.ts.map +1 -1
- package/dist/tools/prompts.js +166 -26
- package/package.json +1 -1
- package/shared-prompts/prd-start.md +17 -3
|
@@ -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;
|
|
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
|
*/
|
package/dist/tools/prompts.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/tools/prompts.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
150
|
+
* Loads built-in prompts from the shared-prompts directory
|
|
85
151
|
*/
|
|
86
|
-
function
|
|
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
|
-
|
|
128
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
27
|
+
## Step 0: Check for PRD Argument
|
|
24
28
|
|
|
25
|
-
**
|
|
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
|
|