@smallironman/mcp-memory-keeper 0.12.2-fork1
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 +542 -0
- package/LICENSE +21 -0
- package/README.md +1281 -0
- package/bin/mcp-memory-keeper +54 -0
- package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
- package/dist/__tests__/e2e/server-e2e.test.js +341 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +1054 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +411 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
- package/dist/__tests__/integration/git-integration.test.js +241 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +291 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +305 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4425 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +2017 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +780 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/token-limits.js +350 -0
- package/dist/utils/tool-profiles.js +242 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +85 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlagManager = void 0;
|
|
4
|
+
const uuid_1 = require("uuid");
|
|
5
|
+
class FeatureFlagManager {
|
|
6
|
+
db;
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this.initializeTables();
|
|
10
|
+
}
|
|
11
|
+
initializeTables() {
|
|
12
|
+
this.db.getDatabase().exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS feature_flags (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
key TEXT UNIQUE NOT NULL,
|
|
17
|
+
enabled BOOLEAN DEFAULT false,
|
|
18
|
+
description TEXT,
|
|
19
|
+
environments TEXT, -- JSON array
|
|
20
|
+
users TEXT, -- JSON array
|
|
21
|
+
percentage INTEGER DEFAULT 0,
|
|
22
|
+
enabled_from TIMESTAMP,
|
|
23
|
+
enabled_until TIMESTAMP,
|
|
24
|
+
category TEXT,
|
|
25
|
+
tags TEXT, -- JSON array
|
|
26
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
28
|
+
created_by TEXT,
|
|
29
|
+
last_modified_by TEXT
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS feature_flag_evaluations (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
flag_id TEXT NOT NULL,
|
|
35
|
+
flag_key TEXT NOT NULL,
|
|
36
|
+
enabled BOOLEAN NOT NULL,
|
|
37
|
+
reason TEXT NOT NULL,
|
|
38
|
+
context TEXT, -- JSON
|
|
39
|
+
evaluated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
40
|
+
FOREIGN KEY (flag_id) REFERENCES feature_flags(id)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS feature_flag_audit (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
flag_id TEXT NOT NULL,
|
|
46
|
+
flag_key TEXT NOT NULL,
|
|
47
|
+
action TEXT NOT NULL, -- 'created', 'updated', 'deleted', 'enabled', 'disabled'
|
|
48
|
+
old_value TEXT, -- JSON
|
|
49
|
+
new_value TEXT, -- JSON
|
|
50
|
+
user_id TEXT,
|
|
51
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_feature_flags_key ON feature_flags(key);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_feature_flags_enabled ON feature_flags(enabled);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_feature_flags_category ON feature_flags(category);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_evaluations_flag ON feature_flag_evaluations(flag_id);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_evaluations_time ON feature_flag_evaluations(evaluated_at);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_audit_flag ON feature_flag_audit(flag_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_audit_time ON feature_flag_audit(timestamp);
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
createFlag(flag) {
|
|
64
|
+
const id = (0, uuid_1.v4)();
|
|
65
|
+
const now = new Date().toISOString();
|
|
66
|
+
const flagWithDefaults = {
|
|
67
|
+
id,
|
|
68
|
+
createdAt: now,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
...flag,
|
|
71
|
+
enabled: flag.enabled ?? false,
|
|
72
|
+
percentage: flag.percentage ?? undefined,
|
|
73
|
+
};
|
|
74
|
+
this.db
|
|
75
|
+
.getDatabase()
|
|
76
|
+
.prepare(`
|
|
77
|
+
INSERT INTO feature_flags (
|
|
78
|
+
id, name, key, enabled, description, environments, users, percentage,
|
|
79
|
+
enabled_from, enabled_until, category, tags, created_at, updated_at,
|
|
80
|
+
created_by, last_modified_by
|
|
81
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
`)
|
|
83
|
+
.run(id, flag.name, flag.key, flag.enabled ? 1 : 0, flag.description, flag.environments ? JSON.stringify(flag.environments) : null, flag.users ? JSON.stringify(flag.users) : null, flag.percentage !== undefined ? flag.percentage : null, flag.enabledFrom, flag.enabledUntil, flag.category, flag.tags ? JSON.stringify(flag.tags) : null, now, now, flag.createdBy, flag.lastModifiedBy);
|
|
84
|
+
// Log creation
|
|
85
|
+
this.logAudit(id, flag.key, 'created', null, flagWithDefaults, flag.createdBy);
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
updateFlag(id, updates, userId) {
|
|
89
|
+
const existing = this.getFlag(id);
|
|
90
|
+
if (!existing) {
|
|
91
|
+
throw new Error(`Feature flag not found: ${id}`);
|
|
92
|
+
}
|
|
93
|
+
const updated = { ...existing, ...updates, id, updatedAt: new Date().toISOString() };
|
|
94
|
+
if (userId) {
|
|
95
|
+
updated.lastModifiedBy = userId;
|
|
96
|
+
}
|
|
97
|
+
this.db
|
|
98
|
+
.getDatabase()
|
|
99
|
+
.prepare(`
|
|
100
|
+
UPDATE feature_flags SET
|
|
101
|
+
name = ?, enabled = ?, description = ?, environments = ?, users = ?,
|
|
102
|
+
percentage = ?, enabled_from = ?, enabled_until = ?, category = ?,
|
|
103
|
+
tags = ?, updated_at = ?, last_modified_by = ?
|
|
104
|
+
WHERE id = ?
|
|
105
|
+
`)
|
|
106
|
+
.run(updated.name, updated.enabled ? 1 : 0, updated.description, updated.environments ? JSON.stringify(updated.environments) : null, updated.users ? JSON.stringify(updated.users) : null, updated.percentage !== undefined ? updated.percentage : null, updated.enabledFrom, updated.enabledUntil, updated.category, updated.tags ? JSON.stringify(updated.tags) : null, updated.updatedAt, updated.lastModifiedBy, id);
|
|
107
|
+
// Log update
|
|
108
|
+
this.logAudit(id, existing.key, 'updated', existing, updated, userId);
|
|
109
|
+
}
|
|
110
|
+
getFlag(id) {
|
|
111
|
+
const row = this.db
|
|
112
|
+
.getDatabase()
|
|
113
|
+
.prepare(`
|
|
114
|
+
SELECT * FROM feature_flags WHERE id = ?
|
|
115
|
+
`)
|
|
116
|
+
.get(id);
|
|
117
|
+
if (!row)
|
|
118
|
+
return null;
|
|
119
|
+
return this.rowToFlag(row);
|
|
120
|
+
}
|
|
121
|
+
getFlagByKey(key) {
|
|
122
|
+
const row = this.db
|
|
123
|
+
.getDatabase()
|
|
124
|
+
.prepare(`
|
|
125
|
+
SELECT * FROM feature_flags WHERE key = ?
|
|
126
|
+
`)
|
|
127
|
+
.get(key);
|
|
128
|
+
if (!row)
|
|
129
|
+
return null;
|
|
130
|
+
return this.rowToFlag(row);
|
|
131
|
+
}
|
|
132
|
+
listFlags(options = {}) {
|
|
133
|
+
let query = 'SELECT * FROM feature_flags WHERE 1=1';
|
|
134
|
+
const params = [];
|
|
135
|
+
if (options.category) {
|
|
136
|
+
query += ' AND category = ?';
|
|
137
|
+
params.push(options.category);
|
|
138
|
+
}
|
|
139
|
+
if (options.enabled !== undefined) {
|
|
140
|
+
query += ' AND enabled = ?';
|
|
141
|
+
params.push(options.enabled ? 1 : 0);
|
|
142
|
+
}
|
|
143
|
+
if (options.environment) {
|
|
144
|
+
query += ' AND (environments IS NULL OR environments LIKE ?)';
|
|
145
|
+
params.push(`%"${options.environment}"%`);
|
|
146
|
+
}
|
|
147
|
+
if (options.tag) {
|
|
148
|
+
query += ' AND tags LIKE ?';
|
|
149
|
+
params.push(`%"${options.tag}"%`);
|
|
150
|
+
}
|
|
151
|
+
query += ' ORDER BY updated_at DESC';
|
|
152
|
+
if (options.limit) {
|
|
153
|
+
query += ' LIMIT ?';
|
|
154
|
+
params.push(options.limit);
|
|
155
|
+
}
|
|
156
|
+
const rows = this.db
|
|
157
|
+
.getDatabase()
|
|
158
|
+
.prepare(query)
|
|
159
|
+
.all(...params);
|
|
160
|
+
return rows.map(row => this.rowToFlag(row));
|
|
161
|
+
}
|
|
162
|
+
deleteFlag(id, userId) {
|
|
163
|
+
const existing = this.getFlag(id);
|
|
164
|
+
if (!existing) {
|
|
165
|
+
throw new Error(`Feature flag not found: ${id}`);
|
|
166
|
+
}
|
|
167
|
+
// Delete related evaluation records first to avoid foreign key constraints
|
|
168
|
+
this.db
|
|
169
|
+
.getDatabase()
|
|
170
|
+
.prepare(`
|
|
171
|
+
DELETE FROM feature_flag_evaluations WHERE flag_id = ?
|
|
172
|
+
`)
|
|
173
|
+
.run(id);
|
|
174
|
+
// Now delete the flag (audit log can stay for historical purposes)
|
|
175
|
+
this.db
|
|
176
|
+
.getDatabase()
|
|
177
|
+
.prepare(`
|
|
178
|
+
DELETE FROM feature_flags WHERE id = ?
|
|
179
|
+
`)
|
|
180
|
+
.run(id);
|
|
181
|
+
// Log deletion after removing the flag
|
|
182
|
+
this.logAudit(id, existing.key, 'deleted', existing, null, userId);
|
|
183
|
+
}
|
|
184
|
+
evaluateFlag(key, context = {}) {
|
|
185
|
+
const flag = this.getFlagByKey(key);
|
|
186
|
+
const timestamp = context.timestamp || new Date().toISOString();
|
|
187
|
+
if (!flag) {
|
|
188
|
+
const evaluation = {
|
|
189
|
+
flag: {
|
|
190
|
+
id: '',
|
|
191
|
+
name: key,
|
|
192
|
+
key,
|
|
193
|
+
enabled: false,
|
|
194
|
+
createdAt: timestamp,
|
|
195
|
+
updatedAt: timestamp,
|
|
196
|
+
},
|
|
197
|
+
enabled: false,
|
|
198
|
+
reason: 'Flag not found',
|
|
199
|
+
context: { ...context, timestamp },
|
|
200
|
+
};
|
|
201
|
+
this.logEvaluation('', key, false, 'Flag not found', evaluation.context);
|
|
202
|
+
return evaluation;
|
|
203
|
+
}
|
|
204
|
+
let enabled = false;
|
|
205
|
+
let reason = 'Flag disabled';
|
|
206
|
+
// Check if flag is globally enabled
|
|
207
|
+
if (!flag.enabled) {
|
|
208
|
+
enabled = false;
|
|
209
|
+
reason = 'Flag globally disabled';
|
|
210
|
+
}
|
|
211
|
+
// Check date constraints
|
|
212
|
+
else if (flag.enabledFrom && new Date(timestamp) < new Date(flag.enabledFrom)) {
|
|
213
|
+
enabled = false;
|
|
214
|
+
reason = `Flag not yet active (starts ${flag.enabledFrom})`;
|
|
215
|
+
}
|
|
216
|
+
else if (flag.enabledUntil && new Date(timestamp) > new Date(flag.enabledUntil)) {
|
|
217
|
+
enabled = false;
|
|
218
|
+
reason = `Flag expired (ended ${flag.enabledUntil})`;
|
|
219
|
+
}
|
|
220
|
+
// Check environment constraints
|
|
221
|
+
else if (flag.environments && flag.environments.length > 0 && context.environment) {
|
|
222
|
+
if (flag.environments.includes(context.environment)) {
|
|
223
|
+
enabled = true;
|
|
224
|
+
reason = `Enabled for environment: ${context.environment}`;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
enabled = false;
|
|
228
|
+
reason = `Not enabled for environment: ${context.environment}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Check user constraints
|
|
232
|
+
else if (flag.users && flag.users.length > 0 && context.userId) {
|
|
233
|
+
if (flag.users.includes(context.userId)) {
|
|
234
|
+
enabled = true;
|
|
235
|
+
reason = `Enabled for user: ${context.userId}`;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
enabled = false;
|
|
239
|
+
reason = `Not enabled for user: ${context.userId}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Check percentage rollout
|
|
243
|
+
else if (flag.percentage !== undefined && flag.percentage !== null) {
|
|
244
|
+
if (flag.percentage === 0) {
|
|
245
|
+
enabled = false;
|
|
246
|
+
reason = `Disabled by percentage rollout (${flag.percentage}%)`;
|
|
247
|
+
}
|
|
248
|
+
else if (flag.percentage === 100) {
|
|
249
|
+
enabled = true;
|
|
250
|
+
reason = `Enabled by percentage rollout (${flag.percentage}%)`;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Use hash of key + userId/environment for consistent percentage evaluation
|
|
254
|
+
const hashInput = key + (context.userId || context.environment || 'anonymous');
|
|
255
|
+
const hash = this.simpleHash(hashInput);
|
|
256
|
+
const userPercentile = hash % 100;
|
|
257
|
+
if (userPercentile < flag.percentage) {
|
|
258
|
+
enabled = true;
|
|
259
|
+
reason = `Enabled by percentage rollout (${flag.percentage}%)`;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
enabled = false;
|
|
263
|
+
reason = `Disabled by percentage rollout (${flag.percentage}%, user at ${userPercentile}%)`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Default to flag enabled state
|
|
268
|
+
else {
|
|
269
|
+
enabled = flag.enabled;
|
|
270
|
+
reason = enabled ? 'Flag enabled' : 'Flag disabled';
|
|
271
|
+
}
|
|
272
|
+
const evaluation = {
|
|
273
|
+
flag,
|
|
274
|
+
enabled,
|
|
275
|
+
reason,
|
|
276
|
+
context: { ...context, timestamp },
|
|
277
|
+
};
|
|
278
|
+
// Log evaluation
|
|
279
|
+
this.logEvaluation(flag.id, key, enabled, reason, evaluation.context);
|
|
280
|
+
return evaluation;
|
|
281
|
+
}
|
|
282
|
+
isEnabled(key, context = {}) {
|
|
283
|
+
return this.evaluateFlag(key, context).enabled;
|
|
284
|
+
}
|
|
285
|
+
getStats() {
|
|
286
|
+
const flags = this.listFlags();
|
|
287
|
+
const enabledFlags = flags.filter(f => f.enabled);
|
|
288
|
+
// By category
|
|
289
|
+
const byCategory = {};
|
|
290
|
+
flags.forEach(flag => {
|
|
291
|
+
const category = flag.category || 'uncategorized';
|
|
292
|
+
if (!byCategory[category]) {
|
|
293
|
+
byCategory[category] = { count: 0, enabled: 0 };
|
|
294
|
+
}
|
|
295
|
+
byCategory[category].count++;
|
|
296
|
+
if (flag.enabled) {
|
|
297
|
+
byCategory[category].enabled++;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
// By environment
|
|
301
|
+
const byEnvironment = {};
|
|
302
|
+
flags.forEach(flag => {
|
|
303
|
+
const environments = flag.environments || ['default'];
|
|
304
|
+
environments.forEach(env => {
|
|
305
|
+
if (!byEnvironment[env]) {
|
|
306
|
+
byEnvironment[env] = { count: 0, enabled: 0 };
|
|
307
|
+
}
|
|
308
|
+
byEnvironment[env].count++;
|
|
309
|
+
if (flag.enabled) {
|
|
310
|
+
byEnvironment[env].enabled++;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
// Scheduled changes
|
|
315
|
+
const now = new Date();
|
|
316
|
+
const toEnable = flags
|
|
317
|
+
.filter(f => !f.enabled && f.enabledFrom && new Date(f.enabledFrom) > now)
|
|
318
|
+
.map(f => ({ flag: f.name, date: f.enabledFrom }));
|
|
319
|
+
const toDisable = flags
|
|
320
|
+
.filter(f => f.enabled && f.enabledUntil && new Date(f.enabledUntil) > now)
|
|
321
|
+
.map(f => ({ flag: f.name, date: f.enabledUntil }));
|
|
322
|
+
// Recent activity
|
|
323
|
+
const recentActivity = this.db
|
|
324
|
+
.getDatabase()
|
|
325
|
+
.prepare(`
|
|
326
|
+
SELECT flag_key, action, timestamp, user_id
|
|
327
|
+
FROM feature_flag_audit
|
|
328
|
+
ORDER BY timestamp DESC
|
|
329
|
+
LIMIT 10
|
|
330
|
+
`)
|
|
331
|
+
.all();
|
|
332
|
+
return {
|
|
333
|
+
totalFlags: flags.length,
|
|
334
|
+
enabledFlags: enabledFlags.length,
|
|
335
|
+
disabledFlags: flags.length - enabledFlags.length,
|
|
336
|
+
byCategory,
|
|
337
|
+
byEnvironment,
|
|
338
|
+
scheduledChanges: { toEnable, toDisable },
|
|
339
|
+
recentActivity: recentActivity.map(row => ({
|
|
340
|
+
flag: row.flag_key,
|
|
341
|
+
action: row.action,
|
|
342
|
+
timestamp: row.timestamp,
|
|
343
|
+
user: row.user_id,
|
|
344
|
+
})),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
getEvaluationHistory(flagKey, limit = 100) {
|
|
348
|
+
return this.db
|
|
349
|
+
.getDatabase()
|
|
350
|
+
.prepare(`
|
|
351
|
+
SELECT * FROM feature_flag_evaluations
|
|
352
|
+
WHERE flag_key = ?
|
|
353
|
+
ORDER BY evaluated_at DESC
|
|
354
|
+
LIMIT ?
|
|
355
|
+
`)
|
|
356
|
+
.all(flagKey, limit);
|
|
357
|
+
}
|
|
358
|
+
getAuditLog(flagId, limit = 50) {
|
|
359
|
+
const query = flagId
|
|
360
|
+
? 'SELECT * FROM feature_flag_audit WHERE flag_id = ? ORDER BY timestamp DESC LIMIT ?'
|
|
361
|
+
: 'SELECT * FROM feature_flag_audit ORDER BY timestamp DESC LIMIT ?';
|
|
362
|
+
const params = flagId ? [flagId, limit] : [limit];
|
|
363
|
+
return this.db
|
|
364
|
+
.getDatabase()
|
|
365
|
+
.prepare(query)
|
|
366
|
+
.all(...params);
|
|
367
|
+
}
|
|
368
|
+
// Bulk operations
|
|
369
|
+
enableFlag(key, userId) {
|
|
370
|
+
const flag = this.getFlagByKey(key);
|
|
371
|
+
if (!flag) {
|
|
372
|
+
throw new Error(`Feature flag not found: ${key}`);
|
|
373
|
+
}
|
|
374
|
+
this.updateFlag(flag.id, { enabled: true }, userId);
|
|
375
|
+
this.logAudit(flag.id, key, 'enabled', { enabled: false }, { enabled: true }, userId);
|
|
376
|
+
}
|
|
377
|
+
disableFlag(key, userId) {
|
|
378
|
+
const flag = this.getFlagByKey(key);
|
|
379
|
+
if (!flag) {
|
|
380
|
+
throw new Error(`Feature flag not found: ${key}`);
|
|
381
|
+
}
|
|
382
|
+
this.updateFlag(flag.id, { enabled: false }, userId);
|
|
383
|
+
this.logAudit(flag.id, key, 'disabled', { enabled: true }, { enabled: false }, userId);
|
|
384
|
+
}
|
|
385
|
+
// Utility methods
|
|
386
|
+
rowToFlag(row) {
|
|
387
|
+
return {
|
|
388
|
+
id: row.id,
|
|
389
|
+
name: row.name,
|
|
390
|
+
key: row.key,
|
|
391
|
+
enabled: Boolean(row.enabled),
|
|
392
|
+
description: row.description,
|
|
393
|
+
environments: row.environments ? JSON.parse(row.environments) : undefined,
|
|
394
|
+
users: row.users ? JSON.parse(row.users) : undefined,
|
|
395
|
+
percentage: row.percentage,
|
|
396
|
+
enabledFrom: row.enabled_from,
|
|
397
|
+
enabledUntil: row.enabled_until,
|
|
398
|
+
category: row.category,
|
|
399
|
+
tags: row.tags ? JSON.parse(row.tags) : undefined,
|
|
400
|
+
createdAt: row.created_at,
|
|
401
|
+
updatedAt: row.updated_at,
|
|
402
|
+
createdBy: row.created_by,
|
|
403
|
+
lastModifiedBy: row.last_modified_by,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
logEvaluation(flagId, flagKey, enabled, reason, context) {
|
|
407
|
+
// Only log if flag exists (flagId is not empty)
|
|
408
|
+
if (flagId) {
|
|
409
|
+
this.db
|
|
410
|
+
.getDatabase()
|
|
411
|
+
.prepare(`
|
|
412
|
+
INSERT INTO feature_flag_evaluations (id, flag_id, flag_key, enabled, reason, context)
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
414
|
+
`)
|
|
415
|
+
.run((0, uuid_1.v4)(), flagId, flagKey, enabled ? 1 : 0, reason, JSON.stringify(context));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
logAudit(flagId, flagKey, action, oldValue, newValue, userId) {
|
|
419
|
+
this.db
|
|
420
|
+
.getDatabase()
|
|
421
|
+
.prepare(`
|
|
422
|
+
INSERT INTO feature_flag_audit (id, flag_id, flag_key, action, old_value, new_value, user_id)
|
|
423
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
424
|
+
`)
|
|
425
|
+
.run((0, uuid_1.v4)(), flagId, flagKey, action, oldValue ? JSON.stringify(oldValue) : null, newValue ? JSON.stringify(newValue) : null, userId);
|
|
426
|
+
}
|
|
427
|
+
simpleHash(str) {
|
|
428
|
+
let hash = 0;
|
|
429
|
+
for (let i = 0; i < str.length; i++) {
|
|
430
|
+
const char = str.charCodeAt(i);
|
|
431
|
+
hash = (hash << 5) - hash + char;
|
|
432
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
433
|
+
}
|
|
434
|
+
return Math.abs(hash);
|
|
435
|
+
}
|
|
436
|
+
// Predefined feature flags for common features
|
|
437
|
+
static getDefaultFlags() {
|
|
438
|
+
return [
|
|
439
|
+
{
|
|
440
|
+
name: 'Enhanced Search',
|
|
441
|
+
key: 'enhanced_search',
|
|
442
|
+
enabled: true,
|
|
443
|
+
description: 'Enable enhanced search capabilities with filters',
|
|
444
|
+
category: 'search',
|
|
445
|
+
tags: ['core', 'stable'],
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: 'Beta Features',
|
|
449
|
+
key: 'beta_features',
|
|
450
|
+
enabled: false,
|
|
451
|
+
description: 'Enable experimental beta features',
|
|
452
|
+
category: 'experimental',
|
|
453
|
+
tags: ['beta', 'experimental'],
|
|
454
|
+
environments: ['development', 'staging'],
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'Advanced Analytics',
|
|
458
|
+
key: 'advanced_analytics',
|
|
459
|
+
enabled: false,
|
|
460
|
+
description: 'Enable detailed analytics and metrics',
|
|
461
|
+
category: 'analytics',
|
|
462
|
+
tags: ['analytics', 'metrics'],
|
|
463
|
+
percentage: 25, // 25% rollout
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: 'Auto Compression',
|
|
467
|
+
key: 'auto_compression',
|
|
468
|
+
enabled: true,
|
|
469
|
+
description: 'Automatically compress old context data',
|
|
470
|
+
category: 'performance',
|
|
471
|
+
tags: ['performance', 'storage'],
|
|
472
|
+
},
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
exports.FeatureFlagManager = FeatureFlagManager;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GitOperations = void 0;
|
|
37
|
+
const simple_git_1 = require("simple-git");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
class GitOperations {
|
|
41
|
+
git;
|
|
42
|
+
constructor(workingDirectory = process.cwd()) {
|
|
43
|
+
this.git = (0, simple_git_1.simpleGit)(workingDirectory);
|
|
44
|
+
}
|
|
45
|
+
async getGitInfo() {
|
|
46
|
+
try {
|
|
47
|
+
// Check if we're in a git repository
|
|
48
|
+
const isRepo = await this.isGitRepository();
|
|
49
|
+
if (!isRepo) {
|
|
50
|
+
return {
|
|
51
|
+
status: 'Not a git repository',
|
|
52
|
+
branch: 'none',
|
|
53
|
+
isGitRepo: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Get status and branch info
|
|
57
|
+
const [status, branch] = await Promise.all([this.git.status(), this.git.branch()]);
|
|
58
|
+
return {
|
|
59
|
+
status: JSON.stringify({
|
|
60
|
+
modified: status.modified,
|
|
61
|
+
created: status.created,
|
|
62
|
+
deleted: status.deleted,
|
|
63
|
+
staged: status.staged,
|
|
64
|
+
not_added: status.not_added, // untracked files
|
|
65
|
+
ahead: status.ahead,
|
|
66
|
+
behind: status.behind,
|
|
67
|
+
}),
|
|
68
|
+
branch: branch.current,
|
|
69
|
+
isGitRepo: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (_error) {
|
|
73
|
+
// Handle any git errors gracefully
|
|
74
|
+
return {
|
|
75
|
+
status: `Git error: ${_error instanceof Error ? _error.message : String(_error)}`,
|
|
76
|
+
branch: 'error',
|
|
77
|
+
isGitRepo: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async getCurrentBranch() {
|
|
82
|
+
try {
|
|
83
|
+
// First try using git command
|
|
84
|
+
const branch = await this.git.branch();
|
|
85
|
+
if (branch.current && branch.current.trim() !== '') {
|
|
86
|
+
return branch.current;
|
|
87
|
+
}
|
|
88
|
+
// Fallback to reading .git/HEAD
|
|
89
|
+
const gitHeadPath = path.join(process.cwd(), '.git', 'HEAD');
|
|
90
|
+
if (fs.existsSync(gitHeadPath)) {
|
|
91
|
+
const headContent = fs.readFileSync(gitHeadPath, 'utf8').trim();
|
|
92
|
+
if (headContent.startsWith('ref: refs/heads/')) {
|
|
93
|
+
return headContent.replace('ref: refs/heads/', '');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
catch (_error) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async safeCommit(message) {
|
|
103
|
+
try {
|
|
104
|
+
// Check if we're in a git repository
|
|
105
|
+
const isRepo = await this.isGitRepository();
|
|
106
|
+
if (!isRepo) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'Not a git repository',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Check if there are changes to commit
|
|
113
|
+
const status = await this.git.status();
|
|
114
|
+
if (status.files.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: 'No changes to commit',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Add all changes and commit
|
|
121
|
+
await this.git.add('.');
|
|
122
|
+
const commitResult = await this.git.commit(message);
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
commit: commitResult.commit,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (_error) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: _error instanceof Error ? _error.message : String(_error),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async isGitRepository() {
|
|
136
|
+
try {
|
|
137
|
+
await this.git.checkIsRepo();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.GitOperations = GitOperations;
|