@juspay/yama 1.1.0 → 1.2.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/CHANGELOG.md +34 -1
- package/README.md +152 -120
- package/dist/cli/index.js +201 -200
- package/dist/core/ContextGatherer.d.ts +10 -5
- package/dist/core/ContextGatherer.js +176 -161
- package/dist/core/Guardian.d.ts +1 -1
- package/dist/core/Guardian.js +126 -122
- package/dist/core/providers/BitbucketProvider.d.ts +3 -3
- package/dist/core/providers/BitbucketProvider.js +129 -121
- package/dist/features/CodeReviewer.d.ts +7 -3
- package/dist/features/CodeReviewer.js +314 -222
- package/dist/features/DescriptionEnhancer.d.ts +3 -3
- package/dist/features/DescriptionEnhancer.js +115 -94
- package/dist/index.d.ts +11 -11
- package/dist/index.js +10 -48
- package/dist/types/index.d.ts +27 -21
- package/dist/types/index.js +13 -18
- package/dist/utils/Cache.d.ts +6 -1
- package/dist/utils/Cache.js +78 -68
- package/dist/utils/ConfigManager.d.ts +5 -1
- package/dist/utils/ConfigManager.js +301 -253
- package/dist/utils/Logger.d.ts +2 -2
- package/dist/utils/Logger.js +69 -67
- package/dist/utils/MemoryBankManager.d.ts +73 -0
- package/dist/utils/MemoryBankManager.js +310 -0
- package/dist/utils/ProviderLimits.d.ts +58 -0
- package/dist/utils/ProviderLimits.js +143 -0
- package/package.json +7 -6
- package/yama.config.example.yaml +37 -21
package/dist/utils/Logger.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Enhanced Logger utility - Optimized from both pr-police.js and pr-describe.js
|
|
3
3
|
* Provides consistent logging across all Guardian operations
|
|
4
4
|
*/
|
|
5
|
-
import { Logger as ILogger, LogLevel, LoggerOptions } from
|
|
5
|
+
import { Logger as ILogger, LogLevel, LoggerOptions } from "../types/index.js";
|
|
6
6
|
export declare class Logger implements ILogger {
|
|
7
7
|
private options;
|
|
8
8
|
constructor(options?: Partial<LoggerOptions>);
|
|
@@ -16,7 +16,7 @@ export declare class Logger implements ILogger {
|
|
|
16
16
|
badge(): void;
|
|
17
17
|
phase(message: string): void;
|
|
18
18
|
success(message: string): void;
|
|
19
|
-
operation(operation: string, status:
|
|
19
|
+
operation(operation: string, status: "started" | "completed" | "failed"): void;
|
|
20
20
|
violation(severity: string, message: string, file?: string): void;
|
|
21
21
|
progress(current: number, total: number, operation: string): void;
|
|
22
22
|
private createProgressBar;
|
package/dist/utils/Logger.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Enhanced Logger utility - Optimized from both pr-police.js and pr-describe.js
|
|
4
3
|
* Provides consistent logging across all Guardian operations
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
-
};
|
|
9
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.logger = exports.Logger = void 0;
|
|
11
|
-
exports.createLogger = createLogger;
|
|
12
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
5
|
+
import chalk from "chalk";
|
|
13
6
|
const YAMA_BADGE = `
|
|
14
7
|
⚔️ ═══════════════════════════════════════════════════════════ ⚔️
|
|
15
8
|
██╗ ██╗ █████╗ ███╗ ███╗ █████╗
|
|
@@ -21,14 +14,15 @@ const YAMA_BADGE = `
|
|
|
21
14
|
⚔️ ═══════════════════════════════════════════════════════════ ⚔️
|
|
22
15
|
AI-Powered PR Automation • Enterprise Security • Code Quality Yama
|
|
23
16
|
`;
|
|
24
|
-
class Logger {
|
|
17
|
+
export class Logger {
|
|
18
|
+
options;
|
|
25
19
|
constructor(options = {}) {
|
|
26
20
|
this.options = {
|
|
27
|
-
level:
|
|
21
|
+
level: "info",
|
|
28
22
|
verbose: false,
|
|
29
|
-
format:
|
|
23
|
+
format: "simple",
|
|
30
24
|
colors: true,
|
|
31
|
-
...options
|
|
25
|
+
...options,
|
|
32
26
|
};
|
|
33
27
|
}
|
|
34
28
|
shouldLog(level) {
|
|
@@ -36,84 +30,93 @@ class Logger {
|
|
|
36
30
|
debug: 0,
|
|
37
31
|
info: 1,
|
|
38
32
|
warn: 2,
|
|
39
|
-
error: 3
|
|
33
|
+
error: 3,
|
|
40
34
|
};
|
|
41
35
|
return levels[level] >= levels[this.options.level];
|
|
42
36
|
}
|
|
43
37
|
formatMessage(level, message, ...args) {
|
|
44
38
|
const timestamp = new Date().toISOString();
|
|
45
|
-
const formattedArgs = args.length > 0
|
|
39
|
+
const formattedArgs = args.length > 0
|
|
40
|
+
? ` ${args
|
|
41
|
+
.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a))
|
|
42
|
+
.join(" ")}`
|
|
43
|
+
: "";
|
|
46
44
|
switch (this.options.format) {
|
|
47
|
-
case
|
|
45
|
+
case "json":
|
|
48
46
|
return JSON.stringify({
|
|
49
47
|
timestamp,
|
|
50
48
|
level: level.toUpperCase(),
|
|
51
49
|
message: message + formattedArgs,
|
|
52
|
-
args: args.length > 0 ? args : undefined
|
|
50
|
+
args: args.length > 0 ? args : undefined,
|
|
53
51
|
});
|
|
54
|
-
case
|
|
52
|
+
case "detailed":
|
|
55
53
|
return `[${timestamp}] [${level.toUpperCase().padEnd(5)}] ${message}${formattedArgs}`;
|
|
56
54
|
default: // simple
|
|
57
55
|
return `${message}${formattedArgs}`;
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
colorize(level, text) {
|
|
61
|
-
if (!this.options.colors)
|
|
59
|
+
if (!this.options.colors) {
|
|
62
60
|
return text;
|
|
61
|
+
}
|
|
63
62
|
switch (level) {
|
|
64
|
-
case
|
|
65
|
-
return
|
|
66
|
-
case
|
|
67
|
-
return
|
|
68
|
-
case
|
|
69
|
-
return
|
|
70
|
-
case
|
|
71
|
-
return
|
|
63
|
+
case "debug":
|
|
64
|
+
return chalk.gray(text);
|
|
65
|
+
case "info":
|
|
66
|
+
return chalk.blue(text);
|
|
67
|
+
case "warn":
|
|
68
|
+
return chalk.yellow(text);
|
|
69
|
+
case "error":
|
|
70
|
+
return chalk.red(text);
|
|
72
71
|
default:
|
|
73
72
|
return text;
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
debug(message, ...args) {
|
|
77
|
-
if (!this.shouldLog(
|
|
76
|
+
if (!this.shouldLog("debug") || !this.options.verbose) {
|
|
78
77
|
return;
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
}
|
|
79
|
+
const formatted = this.formatMessage("debug", `🔍 ${message}`, ...args);
|
|
80
|
+
console.log(this.colorize("debug", formatted));
|
|
81
81
|
}
|
|
82
82
|
info(message, ...args) {
|
|
83
|
-
if (!this.shouldLog(
|
|
83
|
+
if (!this.shouldLog("info")) {
|
|
84
84
|
return;
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
}
|
|
86
|
+
const formatted = this.formatMessage("info", `ℹ️ ${message}`, ...args);
|
|
87
|
+
console.log(this.colorize("info", formatted));
|
|
87
88
|
}
|
|
88
89
|
warn(message, ...args) {
|
|
89
|
-
if (!this.shouldLog(
|
|
90
|
+
if (!this.shouldLog("warn")) {
|
|
90
91
|
return;
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
}
|
|
93
|
+
const formatted = this.formatMessage("warn", `⚠️ ${message}`, ...args);
|
|
94
|
+
console.warn(this.colorize("warn", formatted));
|
|
93
95
|
}
|
|
94
96
|
error(message, ...args) {
|
|
95
|
-
if (!this.shouldLog(
|
|
97
|
+
if (!this.shouldLog("error")) {
|
|
96
98
|
return;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
}
|
|
100
|
+
const formatted = this.formatMessage("error", `❌ ${message}`, ...args);
|
|
101
|
+
console.error(this.colorize("error", formatted));
|
|
99
102
|
}
|
|
100
103
|
badge() {
|
|
101
|
-
console.log(
|
|
104
|
+
console.log(chalk.cyan(YAMA_BADGE));
|
|
102
105
|
}
|
|
103
106
|
phase(message) {
|
|
104
107
|
const formatted = `\n🔄 ${message}`;
|
|
105
|
-
console.log(this.options.colors ?
|
|
108
|
+
console.log(this.options.colors ? chalk.magenta(formatted) : formatted);
|
|
106
109
|
}
|
|
107
110
|
success(message) {
|
|
108
111
|
const formatted = `✅ ${message}`;
|
|
109
|
-
console.log(this.options.colors ?
|
|
112
|
+
console.log(this.options.colors ? chalk.green(formatted) : formatted);
|
|
110
113
|
}
|
|
111
114
|
operation(operation, status) {
|
|
112
|
-
const emoji = status ===
|
|
113
|
-
const color = status ===
|
|
115
|
+
const emoji = status === "started" ? "🚀" : status === "completed" ? "✅" : "❌";
|
|
116
|
+
const color = status === "started" ? "blue" : status === "completed" ? "green" : "red";
|
|
114
117
|
const message = `${emoji} ${operation.toUpperCase()}: ${status}`;
|
|
115
118
|
if (this.options.colors) {
|
|
116
|
-
console.log(
|
|
119
|
+
console.log(chalk[color](message));
|
|
117
120
|
}
|
|
118
121
|
else {
|
|
119
122
|
console.log(message);
|
|
@@ -121,21 +124,21 @@ class Logger {
|
|
|
121
124
|
}
|
|
122
125
|
violation(severity, message, file) {
|
|
123
126
|
const emoji = {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}[severity] ||
|
|
127
|
+
CRITICAL: "🚨",
|
|
128
|
+
MAJOR: "⚠️",
|
|
129
|
+
MINOR: "📝",
|
|
130
|
+
SUGGESTION: "💡",
|
|
131
|
+
}[severity] || "📋";
|
|
129
132
|
const color = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}[severity] ||
|
|
135
|
-
const location = file ? ` in ${file}` :
|
|
133
|
+
CRITICAL: "red",
|
|
134
|
+
MAJOR: "yellow",
|
|
135
|
+
MINOR: "blue",
|
|
136
|
+
SUGGESTION: "cyan",
|
|
137
|
+
}[severity] || "white";
|
|
138
|
+
const location = file ? ` in ${file}` : "";
|
|
136
139
|
const formatted = `${emoji} ${severity}: ${message}${location}`;
|
|
137
140
|
if (this.options.colors) {
|
|
138
|
-
console.log(
|
|
141
|
+
console.log(chalk[color](formatted));
|
|
139
142
|
}
|
|
140
143
|
else {
|
|
141
144
|
console.log(formatted);
|
|
@@ -149,7 +152,7 @@ class Logger {
|
|
|
149
152
|
process.stdout.write(`\r${message}`);
|
|
150
153
|
// Add newline when complete
|
|
151
154
|
if (current === total) {
|
|
152
|
-
process.stdout.write(
|
|
155
|
+
process.stdout.write("\n");
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
createProgressBar(percentage) {
|
|
@@ -157,23 +160,23 @@ class Logger {
|
|
|
157
160
|
const filled = Math.round((percentage / 100) * width);
|
|
158
161
|
const empty = width - filled;
|
|
159
162
|
if (this.options.colors) {
|
|
160
|
-
return
|
|
163
|
+
return chalk.green("█".repeat(filled)) + chalk.gray("░".repeat(empty));
|
|
161
164
|
}
|
|
162
165
|
else {
|
|
163
|
-
return
|
|
166
|
+
return "█".repeat(filled) + "░".repeat(empty);
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
// Method to create child logger with context
|
|
167
170
|
child(context) {
|
|
168
171
|
const childLogger = new Logger(this.options);
|
|
169
172
|
// Override methods to include context
|
|
170
|
-
const originalMethods = [
|
|
171
|
-
originalMethods.forEach(method => {
|
|
173
|
+
const originalMethods = ["debug", "info", "warn", "error"];
|
|
174
|
+
originalMethods.forEach((method) => {
|
|
172
175
|
const original = childLogger[method].bind(childLogger);
|
|
173
176
|
childLogger[method] = (message, ...args) => {
|
|
174
177
|
const contextStr = Object.entries(context)
|
|
175
178
|
.map(([k, v]) => `${k}=${v}`)
|
|
176
|
-
.join(
|
|
179
|
+
.join(" ");
|
|
177
180
|
original(`[${contextStr}] ${message}`, ...args);
|
|
178
181
|
};
|
|
179
182
|
});
|
|
@@ -192,17 +195,16 @@ class Logger {
|
|
|
192
195
|
return { ...this.options };
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
|
-
exports.Logger = Logger;
|
|
196
198
|
// Export singleton instance for convenience with environment-aware defaults
|
|
197
199
|
const loggerOptions = {};
|
|
198
200
|
// Check environment variables for debug mode
|
|
199
|
-
if (process.env.YAMA_DEBUG ===
|
|
200
|
-
loggerOptions.level =
|
|
201
|
+
if (process.env.YAMA_DEBUG === "true") {
|
|
202
|
+
loggerOptions.level = "debug";
|
|
201
203
|
loggerOptions.verbose = true;
|
|
202
204
|
}
|
|
203
|
-
|
|
205
|
+
export const logger = new Logger(loggerOptions);
|
|
204
206
|
// Export factory function
|
|
205
|
-
function createLogger(options) {
|
|
207
|
+
export function createLogger(options) {
|
|
206
208
|
return new Logger(options);
|
|
207
209
|
}
|
|
208
210
|
//# sourceMappingURL=Logger.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Bank Manager - Handles configurable memory bank operations
|
|
3
|
+
* Provides abstraction for memory bank file access with fallback support
|
|
4
|
+
*/
|
|
5
|
+
import { MemoryBankConfig, PRIdentifier } from "../types/index.js";
|
|
6
|
+
import { BitbucketProvider } from "../core/providers/BitbucketProvider.js";
|
|
7
|
+
export interface MemoryBankFile {
|
|
8
|
+
name: string;
|
|
9
|
+
content: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MemoryBankResult {
|
|
13
|
+
files: MemoryBankFile[];
|
|
14
|
+
resolvedPath: string;
|
|
15
|
+
filesProcessed: number;
|
|
16
|
+
fallbackUsed: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class MemoryBankManager {
|
|
19
|
+
private config;
|
|
20
|
+
private bitbucketProvider;
|
|
21
|
+
constructor(config: MemoryBankConfig, bitbucketProvider: BitbucketProvider);
|
|
22
|
+
/**
|
|
23
|
+
* Get memory bank files from the configured path with fallback support
|
|
24
|
+
*/
|
|
25
|
+
getMemoryBankFiles(identifier: PRIdentifier, forceRefresh?: boolean): Promise<MemoryBankResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Try to get files from a specific path
|
|
28
|
+
*/
|
|
29
|
+
private tryGetFilesFromPath;
|
|
30
|
+
/**
|
|
31
|
+
* Get the effective memory bank path (resolved after fallback logic)
|
|
32
|
+
*/
|
|
33
|
+
getEffectiveMemoryBankPath(identifier: PRIdentifier): Promise<string | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Check if memory bank exists at any configured path
|
|
36
|
+
*/
|
|
37
|
+
hasMemoryBank(identifier: PRIdentifier): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Get memory bank configuration
|
|
40
|
+
*/
|
|
41
|
+
getConfig(): MemoryBankConfig;
|
|
42
|
+
/**
|
|
43
|
+
* Update memory bank configuration
|
|
44
|
+
*/
|
|
45
|
+
updateConfig(newConfig: Partial<MemoryBankConfig>): void;
|
|
46
|
+
/**
|
|
47
|
+
* Validates that a path is safe for use as a relative path
|
|
48
|
+
* Protects against path traversal attacks including encoded variants
|
|
49
|
+
*/
|
|
50
|
+
private static isSafeRelativePath;
|
|
51
|
+
/**
|
|
52
|
+
* Validate memory bank configuration
|
|
53
|
+
*/
|
|
54
|
+
private validateConfig;
|
|
55
|
+
/**
|
|
56
|
+
* Clear memory bank cache for a specific repository
|
|
57
|
+
*/
|
|
58
|
+
clearCache(identifier: PRIdentifier): void;
|
|
59
|
+
/**
|
|
60
|
+
* Get memory bank statistics
|
|
61
|
+
*/
|
|
62
|
+
getStats(identifier: PRIdentifier): Promise<{
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
primaryPath: string;
|
|
65
|
+
fallbackPaths: string[];
|
|
66
|
+
hasMemoryBank: boolean;
|
|
67
|
+
resolvedPath: string | null;
|
|
68
|
+
fileCount: number;
|
|
69
|
+
cacheHits: number;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
export declare function createMemoryBankManager(config: MemoryBankConfig, bitbucketProvider: BitbucketProvider): MemoryBankManager;
|
|
73
|
+
//# sourceMappingURL=MemoryBankManager.d.ts.map
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Bank Manager - Handles configurable memory bank operations
|
|
3
|
+
* Provides abstraction for memory bank file access with fallback support
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigurationError } from "../types/index.js";
|
|
6
|
+
import { logger } from "./Logger.js";
|
|
7
|
+
import { cache, Cache } from "./Cache.js";
|
|
8
|
+
export class MemoryBankManager {
|
|
9
|
+
config;
|
|
10
|
+
bitbucketProvider;
|
|
11
|
+
constructor(config, bitbucketProvider) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.bitbucketProvider = bitbucketProvider;
|
|
14
|
+
this.validateConfig();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get memory bank files from the configured path with fallback support
|
|
18
|
+
*/
|
|
19
|
+
async getMemoryBankFiles(identifier, forceRefresh = false) {
|
|
20
|
+
if (!this.config.enabled) {
|
|
21
|
+
logger.debug("Memory bank is disabled in configuration");
|
|
22
|
+
return {
|
|
23
|
+
files: [],
|
|
24
|
+
resolvedPath: "",
|
|
25
|
+
filesProcessed: 0,
|
|
26
|
+
fallbackUsed: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const cacheKey = Cache.keys.memoryBankFiles(identifier.workspace, identifier.repository, identifier.branch || "main", this.config.path);
|
|
30
|
+
if (!forceRefresh && cache.has(cacheKey)) {
|
|
31
|
+
logger.debug("Using cached memory bank files");
|
|
32
|
+
return cache.get(cacheKey);
|
|
33
|
+
}
|
|
34
|
+
logger.debug(`Gathering memory bank files from configured paths...`);
|
|
35
|
+
// Try primary path first
|
|
36
|
+
const primaryResult = await this.tryGetFilesFromPath(identifier, this.config.path);
|
|
37
|
+
if (primaryResult.files.length > 0) {
|
|
38
|
+
const result = {
|
|
39
|
+
...primaryResult,
|
|
40
|
+
resolvedPath: this.config.path,
|
|
41
|
+
fallbackUsed: false,
|
|
42
|
+
};
|
|
43
|
+
// Cache the result
|
|
44
|
+
cache.set(cacheKey, result, 7200); // 2 hours
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
// Try fallback paths if primary path failed
|
|
48
|
+
if (this.config.fallbackPaths && this.config.fallbackPaths.length > 0) {
|
|
49
|
+
logger.debug(`Primary path '${this.config.path}' not found, trying fallback paths...`);
|
|
50
|
+
for (const fallbackPath of this.config.fallbackPaths) {
|
|
51
|
+
logger.debug(`Trying fallback path: ${fallbackPath}`);
|
|
52
|
+
const fallbackResult = await this.tryGetFilesFromPath(identifier, fallbackPath);
|
|
53
|
+
if (fallbackResult.files.length > 0) {
|
|
54
|
+
logger.info(`Memory bank found at fallback path: ${fallbackPath} (${fallbackResult.files.length} files)`);
|
|
55
|
+
const result = {
|
|
56
|
+
...fallbackResult,
|
|
57
|
+
resolvedPath: fallbackPath,
|
|
58
|
+
fallbackUsed: true,
|
|
59
|
+
};
|
|
60
|
+
// Cache the result
|
|
61
|
+
cache.set(cacheKey, result, 7200); // 2 hours
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// No memory bank found anywhere
|
|
67
|
+
logger.debug(`No memory bank found in primary path '${this.config.path}' or fallback paths`);
|
|
68
|
+
const emptyResult = {
|
|
69
|
+
files: [],
|
|
70
|
+
resolvedPath: "",
|
|
71
|
+
filesProcessed: 0,
|
|
72
|
+
fallbackUsed: false,
|
|
73
|
+
};
|
|
74
|
+
// Cache empty result for shorter time to allow for quick retry
|
|
75
|
+
cache.set(cacheKey, emptyResult, 1800); // 30 minutes
|
|
76
|
+
return emptyResult;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Try to get files from a specific path
|
|
80
|
+
*/
|
|
81
|
+
async tryGetFilesFromPath(identifier, path) {
|
|
82
|
+
try {
|
|
83
|
+
// Get directory listing
|
|
84
|
+
const directoryFiles = await this.bitbucketProvider.listDirectoryContent(identifier.workspace, identifier.repository, path, identifier.branch || "main");
|
|
85
|
+
if (!directoryFiles.length) {
|
|
86
|
+
logger.debug(`No files found in directory: ${path}`);
|
|
87
|
+
return { files: [], filesProcessed: 0 };
|
|
88
|
+
}
|
|
89
|
+
// Filter to only files (not directories)
|
|
90
|
+
const files = directoryFiles.filter((f) => f.type === "file");
|
|
91
|
+
logger.debug(`Found ${files.length} files in ${path}`);
|
|
92
|
+
// Get content of each file
|
|
93
|
+
const memoryBankFiles = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
try {
|
|
96
|
+
const content = await this.bitbucketProvider.getFileContent(identifier.workspace, identifier.repository, `${path}/${file.name}`, identifier.branch || "main");
|
|
97
|
+
memoryBankFiles.push({
|
|
98
|
+
name: file.name,
|
|
99
|
+
content,
|
|
100
|
+
path: `${path}/${file.name}`,
|
|
101
|
+
});
|
|
102
|
+
logger.debug(`✓ Loaded content for: ${file.name}`);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.debug(`Could not read file ${file.name}: ${error.message}`);
|
|
106
|
+
// Continue with other files even if one fails
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
files: memoryBankFiles,
|
|
111
|
+
filesProcessed: memoryBankFiles.length,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.debug(`Failed to access path '${path}': ${error.message}`);
|
|
116
|
+
return { files: [], filesProcessed: 0 };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get the effective memory bank path (resolved after fallback logic)
|
|
121
|
+
*/
|
|
122
|
+
async getEffectiveMemoryBankPath(identifier) {
|
|
123
|
+
const result = await this.getMemoryBankFiles(identifier);
|
|
124
|
+
return result.resolvedPath || null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if memory bank exists at any configured path
|
|
128
|
+
*/
|
|
129
|
+
async hasMemoryBank(identifier) {
|
|
130
|
+
const result = await this.getMemoryBankFiles(identifier);
|
|
131
|
+
return result.files.length > 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get memory bank configuration
|
|
135
|
+
*/
|
|
136
|
+
getConfig() {
|
|
137
|
+
return { ...this.config };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Update memory bank configuration
|
|
141
|
+
*/
|
|
142
|
+
updateConfig(newConfig) {
|
|
143
|
+
this.config = { ...this.config, ...newConfig };
|
|
144
|
+
this.validateConfig();
|
|
145
|
+
logger.debug("Memory bank configuration updated");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validates that a path is safe for use as a relative path
|
|
149
|
+
* Protects against path traversal attacks including encoded variants
|
|
150
|
+
*/
|
|
151
|
+
static isSafeRelativePath(path) {
|
|
152
|
+
if (!path || typeof path !== "string") {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
// Reject empty or whitespace-only paths
|
|
156
|
+
if (path.trim().length === 0) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
// Reject excessively long paths
|
|
160
|
+
if (path.length > 1000) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
// Reject absolute paths (Unix-style)
|
|
164
|
+
if (path.startsWith("/")) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
// Reject absolute paths (Windows-style)
|
|
168
|
+
if (/^[a-zA-Z]:/.test(path)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
// Reject UNC paths (Windows network paths)
|
|
172
|
+
if (path.startsWith("\\\\") || path.startsWith("//")) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// Decode URL-encoded characters to catch encoded traversal attempts
|
|
176
|
+
let decodedPath = path;
|
|
177
|
+
try {
|
|
178
|
+
// Multiple rounds of decoding to catch double-encoded attacks
|
|
179
|
+
for (let i = 0; i < 3; i++) {
|
|
180
|
+
const previousPath = decodedPath;
|
|
181
|
+
decodedPath = decodeURIComponent(decodedPath);
|
|
182
|
+
if (decodedPath === previousPath) {
|
|
183
|
+
break; // No more decoding needed
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// If decoding fails, treat as suspicious
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
// Normalize Unicode characters
|
|
192
|
+
decodedPath = decodedPath.normalize("NFC");
|
|
193
|
+
// Check for null bytes (can be used to bypass filters)
|
|
194
|
+
if (decodedPath.includes("\0") || decodedPath.includes("%00")) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Normalize path separators to forward slashes
|
|
198
|
+
const normalizedPath = decodedPath.replace(/\\/g, "/");
|
|
199
|
+
// Check for path traversal sequences after normalization
|
|
200
|
+
if (normalizedPath.includes("../") || normalizedPath.includes("/..")) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
// Check for path traversal at the beginning or end
|
|
204
|
+
if (normalizedPath.startsWith("..") || normalizedPath.endsWith("..")) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
// Check for hidden traversal patterns
|
|
208
|
+
if (normalizedPath.includes("./..") || normalizedPath.includes("../.")) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
// Split path into segments and validate each
|
|
212
|
+
const segments = normalizedPath.split("/").filter(segment => segment.length > 0);
|
|
213
|
+
for (const segment of segments) {
|
|
214
|
+
// Reject any segment that is exactly ".."
|
|
215
|
+
if (segment === "..") {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
// Reject segments that contain ".." anywhere
|
|
219
|
+
if (segment.includes("..")) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// Allow segments that start with a single dot (like .memory-bank, .config)
|
|
223
|
+
// but reject multiple dots or suspicious patterns
|
|
224
|
+
if (segment.startsWith(".") && segment !== ".") {
|
|
225
|
+
// Allow single dot followed by alphanumeric/dash/underscore
|
|
226
|
+
if (!/^\.[\w-]+$/.test(segment)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Reject segments with control characters
|
|
231
|
+
// Check for control characters (0x00-0x1F and 0x7F)
|
|
232
|
+
for (let i = 0; i < segment.length; i++) {
|
|
233
|
+
const charCode = segment.charCodeAt(i);
|
|
234
|
+
if ((charCode >= 0 && charCode <= 31) || charCode === 127) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Additional check: ensure the resolved path doesn't escape the base
|
|
240
|
+
// This is a final safety check using path resolution logic
|
|
241
|
+
const pathParts = segments.filter(part => part !== ".");
|
|
242
|
+
let depth = 0;
|
|
243
|
+
for (const part of pathParts) {
|
|
244
|
+
if (part === "..") {
|
|
245
|
+
depth--;
|
|
246
|
+
if (depth < 0) {
|
|
247
|
+
return false; // Would escape the base directory
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
depth++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Validate memory bank configuration
|
|
258
|
+
*/
|
|
259
|
+
validateConfig() {
|
|
260
|
+
if (!this.config.path) {
|
|
261
|
+
throw new ConfigurationError("Memory bank path must be specified when memory bank is enabled");
|
|
262
|
+
}
|
|
263
|
+
if (!MemoryBankManager.isSafeRelativePath(this.config.path)) {
|
|
264
|
+
throw new ConfigurationError(`Memory bank path is unsafe or contains path traversal: ${this.config.path}`);
|
|
265
|
+
}
|
|
266
|
+
// Validate fallback paths
|
|
267
|
+
if (this.config.fallbackPaths) {
|
|
268
|
+
for (const fallbackPath of this.config.fallbackPaths) {
|
|
269
|
+
if (!MemoryBankManager.isSafeRelativePath(fallbackPath)) {
|
|
270
|
+
throw new ConfigurationError(`Memory bank fallback path is unsafe or contains path traversal: ${fallbackPath}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
logger.debug("Memory bank configuration validated successfully");
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Clear memory bank cache for a specific repository
|
|
278
|
+
*/
|
|
279
|
+
clearCache(identifier) {
|
|
280
|
+
const patterns = [
|
|
281
|
+
`memory-bank:${identifier.workspace}:${identifier.repository}:*`,
|
|
282
|
+
`project-context:${identifier.workspace}:${identifier.repository}:*`,
|
|
283
|
+
];
|
|
284
|
+
patterns.forEach((pattern) => {
|
|
285
|
+
cache.invalidatePattern(pattern);
|
|
286
|
+
});
|
|
287
|
+
logger.debug(`Memory bank cache cleared for ${identifier.workspace}/${identifier.repository}`);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get memory bank statistics
|
|
291
|
+
*/
|
|
292
|
+
async getStats(identifier) {
|
|
293
|
+
const result = await this.getMemoryBankFiles(identifier);
|
|
294
|
+
const cacheStats = cache.stats();
|
|
295
|
+
return {
|
|
296
|
+
enabled: this.config.enabled,
|
|
297
|
+
primaryPath: this.config.path,
|
|
298
|
+
fallbackPaths: this.config.fallbackPaths || [],
|
|
299
|
+
hasMemoryBank: result.files.length > 0,
|
|
300
|
+
resolvedPath: result.resolvedPath || null,
|
|
301
|
+
fileCount: result.files.length,
|
|
302
|
+
cacheHits: cacheStats.hits,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Export factory function
|
|
307
|
+
export function createMemoryBankManager(config, bitbucketProvider) {
|
|
308
|
+
return new MemoryBankManager(config, bitbucketProvider);
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=MemoryBankManager.js.map
|