@jagilber-org/index-server 1.22.0 → 1.26.1
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 +87 -2
- package/CODE_OF_CONDUCT.md +2 -0
- package/CONTRIBUTING.md +32 -2
- package/README.md +83 -20
- package/SECURITY.md +17 -5
- package/dist/config/dashboardConfig.d.ts +3 -0
- package/dist/config/dashboardConfig.js +3 -0
- package/dist/config/defaultValues.d.ts +1 -1
- package/dist/config/defaultValues.js +1 -1
- package/dist/config/featureConfig.d.ts +2 -0
- package/dist/config/featureConfig.js +6 -1
- package/dist/config/runtimeConfig.d.ts +1 -1
- package/dist/config/runtimeConfig.js +8 -9
- package/dist/dashboard/client/admin.html +173 -54
- package/dist/dashboard/client/css/admin.css +151 -0
- package/dist/dashboard/client/js/admin.auth.js +25 -11
- package/dist/dashboard/client/js/admin.config.js +1 -1
- package/dist/dashboard/client/js/admin.feedback.js +328 -0
- package/dist/dashboard/client/js/admin.graph.js +120 -18
- package/dist/dashboard/client/js/admin.instructions.js +27 -13
- package/dist/dashboard/client/js/admin.logs.js +1 -5
- package/dist/dashboard/client/js/admin.maintenance.js +53 -8
- package/dist/dashboard/client/js/admin.messaging.js +1 -4
- package/dist/dashboard/client/js/admin.overview.js +5 -1
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/client/js/admin.utils.js +43 -1
- package/dist/dashboard/client/js/mermaid.min.js +813 -537
- package/dist/dashboard/export/DataExporter.js +2 -1
- package/dist/dashboard/server/AdminPanel.d.ts +3 -0
- package/dist/dashboard/server/AdminPanel.js +132 -35
- package/dist/dashboard/server/ApiRoutes.js +40 -9
- package/dist/dashboard/server/DashboardServer.js +1 -1
- package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
- package/dist/dashboard/server/FileMetricsStorage.js +52 -5
- package/dist/dashboard/server/HttpTransport.js +6 -0
- package/dist/dashboard/server/InstanceManager.js +7 -2
- package/dist/dashboard/server/KnowledgeStore.js +7 -2
- package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
- package/dist/dashboard/server/MetricsCollector.js +113 -17
- package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
- package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
- package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
- package/dist/dashboard/server/routes/admin.routes.js +35 -27
- package/dist/dashboard/server/routes/alerts.routes.js +4 -3
- package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
- package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
- package/dist/dashboard/server/routes/graph.routes.js +10 -13
- package/dist/dashboard/server/routes/index.d.ts +1 -0
- package/dist/dashboard/server/routes/index.js +74 -39
- package/dist/dashboard/server/routes/instances.routes.js +2 -1
- package/dist/dashboard/server/routes/instructions.routes.js +46 -27
- package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
- package/dist/dashboard/server/routes/logs.routes.js +5 -4
- package/dist/dashboard/server/routes/messaging.routes.js +15 -14
- package/dist/dashboard/server/routes/metrics.routes.js +14 -13
- package/dist/dashboard/server/routes/scripts.routes.js +6 -3
- package/dist/dashboard/server/routes/status.routes.js +25 -6
- package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
- package/dist/dashboard/server/routes/usage.routes.js +2 -1
- package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
- package/dist/dashboard/server/utils/escapeHtml.js +11 -0
- package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
- package/dist/dashboard/server/utils/pathContainment.js +15 -0
- package/dist/dashboard/server/wsInit.js +2 -2
- package/dist/lib/mcpStdioLogging.d.ts +165 -0
- package/dist/lib/mcpStdioLogging.js +287 -0
- package/dist/schemas/index.d.ts +37 -2
- package/dist/schemas/index.js +27 -3
- package/dist/server/backgroundServicesStartup.d.ts +7 -1
- package/dist/server/backgroundServicesStartup.js +25 -8
- package/dist/server/certInit.d.ts +97 -0
- package/dist/server/certInit.js +359 -0
- package/dist/server/certInit.types.d.ts +92 -0
- package/dist/server/certInit.types.js +34 -0
- package/dist/server/handshake/fallbackFrames.d.ts +31 -0
- package/dist/server/handshake/fallbackFrames.js +38 -0
- package/dist/server/handshake/initializeDetector.d.ts +31 -0
- package/dist/server/handshake/initializeDetector.js +88 -0
- package/dist/server/handshake/protocol.d.ts +15 -0
- package/dist/server/handshake/protocol.js +37 -0
- package/dist/server/handshake/readyEmitter.d.ts +6 -0
- package/dist/server/handshake/readyEmitter.js +88 -0
- package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
- package/dist/server/handshake/safetyFallbacks.js +134 -0
- package/dist/server/handshake/stdinSniffer.d.ts +1 -0
- package/dist/server/handshake/stdinSniffer.js +260 -0
- package/dist/server/handshake/tracing.d.ts +16 -0
- package/dist/server/handshake/tracing.js +95 -0
- package/dist/server/handshakeManager.d.ts +23 -23
- package/dist/server/handshakeManager.js +36 -466
- package/dist/server/index-server.d.ts +23 -0
- package/dist/server/index-server.js +194 -9
- package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
- package/dist/server/mcpReadOnlySurfaces.js +297 -0
- package/dist/server/sdkServer.js +69 -7
- package/dist/server/transport.d.ts +5 -6
- package/dist/server/transport.js +46 -64
- package/dist/server/transportFactory.d.ts +3 -9
- package/dist/server/transportFactory.js +18 -380
- package/dist/services/atomicFs.d.ts +3 -0
- package/dist/services/atomicFs.js +171 -13
- package/dist/services/auditLog.d.ts +17 -2
- package/dist/services/auditLog.js +75 -14
- package/dist/services/bootstrapGating.js +1 -1
- package/dist/services/categoryRules.d.ts +10 -0
- package/dist/services/categoryRules.js +17 -0
- package/dist/services/classificationService.js +7 -5
- package/dist/services/embeddingService.d.ts +27 -11
- package/dist/services/embeddingService.js +51 -14
- package/dist/services/feedbackStorage.d.ts +39 -0
- package/dist/services/feedbackStorage.js +88 -0
- package/dist/services/handlers/instructions.add.js +429 -317
- package/dist/services/handlers/instructions.groom.js +128 -31
- package/dist/services/handlers/instructions.import.js +56 -23
- package/dist/services/handlers/instructions.patch.js +43 -32
- package/dist/services/handlers/instructions.query.js +20 -29
- package/dist/services/handlers/instructions.shared.d.ts +54 -0
- package/dist/services/handlers/instructions.shared.js +126 -1
- package/dist/services/handlers.activation.js +83 -81
- package/dist/services/handlers.dashboardConfig.d.ts +2 -2
- package/dist/services/handlers.dashboardConfig.js +1 -2
- package/dist/services/handlers.diagnostics.js +75 -54
- package/dist/services/handlers.feedback.d.ts +4 -11
- package/dist/services/handlers.feedback.js +11 -333
- package/dist/services/handlers.gates.js +69 -37
- package/dist/services/handlers.graph.js +2 -2
- package/dist/services/handlers.help.js +2 -2
- package/dist/services/handlers.instructionSchema.js +4 -2
- package/dist/services/handlers.integrity.js +42 -22
- package/dist/services/handlers.messaging.js +1 -1
- package/dist/services/handlers.metrics.js +51 -6
- package/dist/services/handlers.prompt.js +10 -2
- package/dist/services/handlers.search.js +94 -44
- package/dist/services/handlers.trace.js +1 -1
- package/dist/services/handlers.usage.js +38 -7
- package/dist/services/indexContext.d.ts +21 -1
- package/dist/services/indexContext.js +267 -82
- package/dist/services/indexLoader.d.ts +1 -0
- package/dist/services/indexLoader.js +28 -8
- package/dist/services/instructionRecordValidation.d.ts +39 -0
- package/dist/services/instructionRecordValidation.js +388 -0
- package/dist/services/instructions.dispatcher.js +4 -4
- package/dist/services/loaderSchemaValidator.d.ts +15 -0
- package/dist/services/loaderSchemaValidator.js +69 -0
- package/dist/services/logger.js +11 -2
- package/dist/services/mcpLogBridge.d.ts +49 -0
- package/dist/services/mcpLogBridge.js +83 -0
- package/dist/services/ownershipService.js +18 -8
- package/dist/services/performanceBaseline.js +23 -22
- package/dist/services/promptReviewService.d.ts +3 -1
- package/dist/services/promptReviewService.js +41 -13
- package/dist/services/regexSafety.d.ts +6 -0
- package/dist/services/regexSafety.js +46 -0
- package/dist/services/seedBootstrap.js +4 -4
- package/dist/services/storage/factory.d.ts +14 -1
- package/dist/services/storage/factory.js +61 -1
- package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
- package/dist/services/storage/jsonEmbeddingStore.js +83 -0
- package/dist/services/storage/jsonFileStore.d.ts +3 -1
- package/dist/services/storage/jsonFileStore.js +8 -6
- package/dist/services/storage/migrationEngine.d.ts +13 -0
- package/dist/services/storage/migrationEngine.js +31 -0
- package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
- package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
- package/dist/services/storage/sqliteStore.d.ts +3 -1
- package/dist/services/storage/sqliteStore.js +2 -2
- package/dist/services/storage/types.d.ts +48 -1
- package/dist/services/toolRegistry.js +77 -67
- package/dist/services/toolRegistry.zod.js +89 -86
- package/dist/services/tracing.js +5 -4
- package/dist/utils/envUtils.d.ts +4 -0
- package/dist/utils/envUtils.js +7 -0
- package/dist/utils/memoryMonitor.js +11 -10
- package/package.json +11 -4
- package/schemas/instruction.schema.json +38 -1
- package/scripts/copy-dashboard-assets.mjs +1 -1
- package/scripts/dist/README.md +1 -1
- package/scripts/setup-wizard.mjs +781 -0
- package/server.json +1 -0
- package/dist/externalClientLib.d.ts +0 -1
- package/dist/externalClientLib.js +0 -2
- package/dist/portableClientWrapper.d.ts +0 -1
- package/dist/portableClientWrapper.js +0 -2
- package/dist/services/indexingService.d.ts +0 -1
- package/dist/services/indexingService.js +0 -2
|
@@ -12,6 +12,21 @@ export interface AuditEntry {
|
|
|
12
12
|
ids?: string[];
|
|
13
13
|
meta?: Record<string, unknown>;
|
|
14
14
|
}
|
|
15
|
+
export interface AuditReadResult {
|
|
16
|
+
entries: AuditEntry[];
|
|
17
|
+
parseErrors: number;
|
|
18
|
+
}
|
|
19
|
+
export interface AuditLogHealth {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
file: string | null;
|
|
22
|
+
writeFailures: number;
|
|
23
|
+
readFailures: number;
|
|
24
|
+
parseErrors: number;
|
|
25
|
+
degraded: boolean;
|
|
26
|
+
lastWriteError?: string;
|
|
27
|
+
lastReadError?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function getAuditLogHealth(): AuditLogHealth;
|
|
15
30
|
/**
|
|
16
31
|
* Append an entry to the audit log file. Silent no-op when logging is disabled.
|
|
17
32
|
* @param action - Tool or operation name being recorded
|
|
@@ -33,6 +48,6 @@ export declare function logHttpAudit(method: string, route: string, statusCode:
|
|
|
33
48
|
/**
|
|
34
49
|
* Read the most recent audit log entries from disk.
|
|
35
50
|
* @param limit - Maximum number of lines to return from the tail of the file (default 1000)
|
|
36
|
-
* @returns Parsed audit entries, or an empty
|
|
51
|
+
* @returns Parsed audit entries plus parse-error count, or an empty result if logging is disabled or the file is missing
|
|
37
52
|
*/
|
|
38
|
-
export declare function readAuditEntries(limit?: number):
|
|
53
|
+
export declare function readAuditEntries(limit?: number): AuditReadResult;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runWithCorrelation = runWithCorrelation;
|
|
7
7
|
exports.getCurrentCorrelationId = getCurrentCorrelationId;
|
|
8
8
|
exports.resetAuditLogCache = resetAuditLogCache;
|
|
9
|
+
exports.getAuditLogHealth = getAuditLogHealth;
|
|
9
10
|
exports.logAudit = logAudit;
|
|
10
11
|
exports.logToolAudit = logToolAudit;
|
|
11
12
|
exports.logHttpAudit = logHttpAudit;
|
|
@@ -14,6 +15,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
15
16
|
const async_hooks_1 = require("async_hooks");
|
|
16
17
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
18
|
+
const logger_1 = require("./logger");
|
|
17
19
|
const toolRegistry_1 = require("./toolRegistry");
|
|
18
20
|
// Append-only JSONL audit log for all server operations.
|
|
19
21
|
// Each line: { ts, kind, action, ids?, meta? }
|
|
@@ -33,6 +35,35 @@ function getCurrentCorrelationId() {
|
|
|
33
35
|
}
|
|
34
36
|
let cachedKey;
|
|
35
37
|
let cachedPath;
|
|
38
|
+
const auditLogState = {
|
|
39
|
+
writeFailures: 0,
|
|
40
|
+
readFailures: 0,
|
|
41
|
+
parseErrors: 0,
|
|
42
|
+
writeWarningActive: false,
|
|
43
|
+
};
|
|
44
|
+
function formatAuditError(error) {
|
|
45
|
+
return error instanceof Error ? error.message : String(error);
|
|
46
|
+
}
|
|
47
|
+
function markAuditWriteFailure(file, error) {
|
|
48
|
+
auditLogState.writeFailures += 1;
|
|
49
|
+
auditLogState.lastWriteError = formatAuditError(error);
|
|
50
|
+
if (!auditLogState.writeWarningActive) {
|
|
51
|
+
auditLogState.writeWarningActive = true;
|
|
52
|
+
(0, logger_1.logWarn)('[audit] Failed to persist audit log; entries may be dropped until persistence recovers', {
|
|
53
|
+
file,
|
|
54
|
+
error: auditLogState.lastWriteError,
|
|
55
|
+
writeFailures: auditLogState.writeFailures,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function markAuditWriteSuccess() {
|
|
60
|
+
auditLogState.lastWriteError = undefined;
|
|
61
|
+
auditLogState.writeWarningActive = false;
|
|
62
|
+
}
|
|
63
|
+
function markAuditReadFailure(error) {
|
|
64
|
+
auditLogState.readFailures += 1;
|
|
65
|
+
auditLogState.lastReadError = formatAuditError(error);
|
|
66
|
+
}
|
|
36
67
|
function resolveLogPath() {
|
|
37
68
|
const { auditLog } = (0, runtimeConfig_1.getRuntimeConfig)().instructions;
|
|
38
69
|
const key = auditLog.enabled && auditLog.file ? `on:${auditLog.file}` : 'off';
|
|
@@ -52,8 +83,10 @@ function resolveLogPath() {
|
|
|
52
83
|
fs_1.default.appendFileSync(file, '');
|
|
53
84
|
cachedPath = file;
|
|
54
85
|
}
|
|
55
|
-
catch {
|
|
56
|
-
cachedPath =
|
|
86
|
+
catch (error) {
|
|
87
|
+
cachedPath = undefined;
|
|
88
|
+
markAuditWriteFailure(file, error);
|
|
89
|
+
return null;
|
|
57
90
|
}
|
|
58
91
|
return cachedPath;
|
|
59
92
|
}
|
|
@@ -61,6 +94,25 @@ function resolveLogPath() {
|
|
|
61
94
|
function resetAuditLogCache() {
|
|
62
95
|
cachedKey = undefined;
|
|
63
96
|
cachedPath = undefined;
|
|
97
|
+
auditLogState.writeFailures = 0;
|
|
98
|
+
auditLogState.readFailures = 0;
|
|
99
|
+
auditLogState.parseErrors = 0;
|
|
100
|
+
auditLogState.lastWriteError = undefined;
|
|
101
|
+
auditLogState.lastReadError = undefined;
|
|
102
|
+
auditLogState.writeWarningActive = false;
|
|
103
|
+
}
|
|
104
|
+
function getAuditLogHealth() {
|
|
105
|
+
const { auditLog } = (0, runtimeConfig_1.getRuntimeConfig)().instructions;
|
|
106
|
+
return {
|
|
107
|
+
enabled: Boolean(auditLog.enabled && auditLog.file),
|
|
108
|
+
file: auditLog.file ?? null,
|
|
109
|
+
writeFailures: auditLogState.writeFailures,
|
|
110
|
+
readFailures: auditLogState.readFailures,
|
|
111
|
+
parseErrors: auditLogState.parseErrors,
|
|
112
|
+
degraded: auditLogState.writeFailures > 0 || auditLogState.readFailures > 0 || auditLogState.parseErrors > 0,
|
|
113
|
+
lastWriteError: auditLogState.lastWriteError,
|
|
114
|
+
lastReadError: auditLogState.lastReadError,
|
|
115
|
+
};
|
|
64
116
|
}
|
|
65
117
|
/**
|
|
66
118
|
* Append an entry to the audit log file. Silent no-op when logging is disabled.
|
|
@@ -72,11 +124,10 @@ function resetAuditLogCache() {
|
|
|
72
124
|
function logAudit(action, ids, meta, kind) {
|
|
73
125
|
const file = resolveLogPath();
|
|
74
126
|
if (!file)
|
|
75
|
-
return; // silent no-op when logging disabled
|
|
127
|
+
return; // silent no-op when logging is disabled
|
|
76
128
|
const entry = { ts: new Date().toISOString(), kind: kind ?? 'mutation', action };
|
|
77
|
-
if (ids)
|
|
129
|
+
if (ids)
|
|
78
130
|
entry.ids = Array.isArray(ids) ? ids : [ids];
|
|
79
|
-
}
|
|
80
131
|
// Auto-inject correlationId from async context if not already present in meta
|
|
81
132
|
const ctxCorr = getCurrentCorrelationId();
|
|
82
133
|
if (ctxCorr || meta) {
|
|
@@ -87,8 +138,12 @@ function logAudit(action, ids, meta, kind) {
|
|
|
87
138
|
}
|
|
88
139
|
try {
|
|
89
140
|
fs_1.default.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8'); // lgtm[js/http-to-file-access] — audit log path from config
|
|
141
|
+
markAuditWriteSuccess();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
cachedPath = undefined;
|
|
145
|
+
markAuditWriteFailure(file, error);
|
|
90
146
|
}
|
|
91
|
-
catch { /* swallow logging errors to avoid impacting primary operation path */ }
|
|
92
147
|
}
|
|
93
148
|
/**
|
|
94
149
|
* Log a tool invocation to the audit trail. Called from the registry wrapper
|
|
@@ -118,25 +173,31 @@ function logHttpAudit(method, route, statusCode, durationMs, clientIp, userAgent
|
|
|
118
173
|
/**
|
|
119
174
|
* Read the most recent audit log entries from disk.
|
|
120
175
|
* @param limit - Maximum number of lines to return from the tail of the file (default 1000)
|
|
121
|
-
* @returns Parsed audit entries, or an empty
|
|
176
|
+
* @returns Parsed audit entries plus parse-error count, or an empty result if logging is disabled or the file is missing
|
|
122
177
|
*/
|
|
123
178
|
function readAuditEntries(limit = 1000) {
|
|
124
179
|
const file = resolveLogPath();
|
|
125
180
|
if (!file || !fs_1.default.existsSync(file))
|
|
126
|
-
return [];
|
|
181
|
+
return { entries: [], parseErrors: 0 };
|
|
127
182
|
try {
|
|
128
183
|
const lines = fs_1.default.readFileSync(file, 'utf8').split(/\r?\n/).filter(l => l.trim());
|
|
129
184
|
const recent = lines.slice(-limit);
|
|
130
185
|
const parsed = [];
|
|
131
|
-
|
|
186
|
+
let parseErrors = 0;
|
|
187
|
+
for (const line of recent) {
|
|
132
188
|
try {
|
|
133
|
-
parsed.push(JSON.parse(
|
|
189
|
+
parsed.push(JSON.parse(line));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
parseErrors += 1;
|
|
134
193
|
}
|
|
135
|
-
catch { /* ignore */ }
|
|
136
194
|
}
|
|
137
|
-
|
|
195
|
+
auditLogState.parseErrors += parseErrors;
|
|
196
|
+
auditLogState.lastReadError = undefined;
|
|
197
|
+
return { entries: parsed, parseErrors };
|
|
138
198
|
}
|
|
139
|
-
catch {
|
|
140
|
-
|
|
199
|
+
catch (error) {
|
|
200
|
+
markAuditReadFailure(error);
|
|
201
|
+
return { entries: [], parseErrors: 0 };
|
|
141
202
|
}
|
|
142
203
|
}
|
|
@@ -23,7 +23,7 @@ const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
|
23
23
|
* States:
|
|
24
24
|
* - reference mode (INDEX_SERVER_REFERENCE_MODE=1): index is read-only, all mutation blocked permanently
|
|
25
25
|
* - new workspace: only bootstrap seed instructions present (000-bootstrapper / 001-lifecycle-bootstrap) → require confirmation
|
|
26
|
-
* - existing workspace: any non-bootstrap instruction present OR confirmation file exists -> mutation allowed (
|
|
26
|
+
* - existing workspace: any non-bootstrap instruction present OR confirmation file exists -> mutation allowed (unless explicitly forced read-only)
|
|
27
27
|
* - confirmed: confirmation file created after successful finalize → persists across restarts
|
|
28
28
|
*/
|
|
29
29
|
const BOOTSTRAP_IDS = new Set(['000-bootstrapper', '001-lifecycle-bootstrap']);
|
|
@@ -5,3 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const CATEGORY_RULES: [RegExp, string][];
|
|
7
7
|
export declare function deriveCategory(id: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Convert a CATEGORY_RULES display label (e.g. "Service Fabric", "VS Code",
|
|
10
|
+
* "AI/ML", ".NET", "Git/Repo") into a token that satisfies the disk schema's
|
|
11
|
+
* category pattern (^[a-z0-9][a-z0-9-_]{0,48}$ — lowercase, hyphen/underscore
|
|
12
|
+
* only, must start with [a-z0-9]).
|
|
13
|
+
*
|
|
14
|
+
* Used by groom remapCategories before writing primaryCategory to disk so the
|
|
15
|
+
* loader-symmetric validator does not silently reject the rewrite.
|
|
16
|
+
*/
|
|
17
|
+
export declare function slugifyCategory(label: string): string;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.CATEGORY_RULES = void 0;
|
|
9
9
|
exports.deriveCategory = deriveCategory;
|
|
10
|
+
exports.slugifyCategory = slugifyCategory;
|
|
10
11
|
exports.CATEGORY_RULES = [
|
|
11
12
|
[/azure|arm-template|apim|batch|vmss/, 'Azure'],
|
|
12
13
|
[/\bsf[-_]|service-fabric|servicefabric|collectsf|sfrp/, 'Service Fabric'],
|
|
@@ -35,3 +36,19 @@ function deriveCategory(id) {
|
|
|
35
36
|
}
|
|
36
37
|
return 'Other';
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Convert a CATEGORY_RULES display label (e.g. "Service Fabric", "VS Code",
|
|
41
|
+
* "AI/ML", ".NET", "Git/Repo") into a token that satisfies the disk schema's
|
|
42
|
+
* category pattern (^[a-z0-9][a-z0-9-_]{0,48}$ — lowercase, hyphen/underscore
|
|
43
|
+
* only, must start with [a-z0-9]).
|
|
44
|
+
*
|
|
45
|
+
* Used by groom remapCategories before writing primaryCategory to disk so the
|
|
46
|
+
* loader-symmetric validator does not silently reject the rewrite.
|
|
47
|
+
*/
|
|
48
|
+
function slugifyCategory(label) {
|
|
49
|
+
return label
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, '-') // collapse runs of non-alphanumerics
|
|
52
|
+
.replace(/^-+|-+$/g, '') // strip leading/trailing hyphens
|
|
53
|
+
.slice(0, 49); // schema allows up to 49 chars
|
|
54
|
+
}
|
|
@@ -22,7 +22,7 @@ class ClassificationService {
|
|
|
22
22
|
let userId = entry.userId;
|
|
23
23
|
const teamIds = entry.teamIds ? [...entry.teamIds] : [];
|
|
24
24
|
const otherCats = [];
|
|
25
|
-
for (const cRaw of entry.categories) {
|
|
25
|
+
for (const cRaw of entry.categories ?? []) {
|
|
26
26
|
const c = cRaw.toLowerCase();
|
|
27
27
|
if (c.startsWith('scope:workspace:')) {
|
|
28
28
|
if (!workspaceId)
|
|
@@ -61,8 +61,12 @@ class ClassificationService {
|
|
|
61
61
|
categories: Array.from(new Set(otherCats.map(c => c.toLowerCase()))).sort(),
|
|
62
62
|
updatedAt: entry.updatedAt || now,
|
|
63
63
|
createdAt: entry.createdAt || now,
|
|
64
|
-
//
|
|
65
|
-
|
|
64
|
+
// Always coerce to current SCHEMA_VERSION on normalization. The write path
|
|
65
|
+
// (writeEntry / writeEntryAsync) validates against the loader JSON schema
|
|
66
|
+
// which only accepts the current version. Preserving a legacy version here
|
|
67
|
+
// would cause silent write rejection. Migration of legacy fields runs
|
|
68
|
+
// separately via migrateInstructionRecord on the write path.
|
|
69
|
+
schemaVersion: schemaVersion_1.SCHEMA_VERSION,
|
|
66
70
|
// Compute hash from canonical (trimmed) body to ensure stability across innocuous whitespace differences
|
|
67
71
|
sourceHash: entry.sourceHash && entry.sourceHash.length === 64 ? entry.sourceHash : this.computeHash(trimmedBody),
|
|
68
72
|
riskScore: this.computeRisk(entry),
|
|
@@ -97,8 +101,6 @@ class ClassificationService {
|
|
|
97
101
|
issues.push('missing title');
|
|
98
102
|
if (!entry.body)
|
|
99
103
|
issues.push('missing body');
|
|
100
|
-
if (entry.requirement === 'deprecated' && !entry.deprecatedBy)
|
|
101
|
-
issues.push('deprecated requires deprecatedBy');
|
|
102
104
|
return issues;
|
|
103
105
|
}
|
|
104
106
|
/**
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - Uses @huggingface/transformers (optional dep) via dynamic ESM import
|
|
12
12
|
*/
|
|
13
13
|
import { InstructionEntry } from '../models/instruction';
|
|
14
|
+
import type { EmbeddingCacheData, IEmbeddingStore } from './storage/types';
|
|
15
|
+
export type { EmbeddingCacheData } from './storage/types';
|
|
14
16
|
/**
|
|
15
17
|
* Cosine similarity between two vectors.
|
|
16
18
|
* Pure math — no dependencies, no model needed.
|
|
@@ -20,16 +22,6 @@ export declare function cosineSimilarity(a: Float32Array, b: Float32Array): numb
|
|
|
20
22
|
* Check if cached embeddings are stale (index has changed since last embed).
|
|
21
23
|
*/
|
|
22
24
|
export declare function isStale(indexHash: string, embeddingHash: string): boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Persisted embedding cache format (backwards-compatible: entryHashes is optional).
|
|
25
|
-
*/
|
|
26
|
-
export interface EmbeddingCacheData {
|
|
27
|
-
indexHash: string;
|
|
28
|
-
modelName?: string;
|
|
29
|
-
/** Per-entry content hashes for incremental invalidation (required for v2+). */
|
|
30
|
-
entryHashes?: Record<string, string>;
|
|
31
|
-
embeddings: Record<string, number[]>;
|
|
32
|
-
}
|
|
33
25
|
/**
|
|
34
26
|
* Load cached embeddings from disk.
|
|
35
27
|
* Returns null if file doesn't exist or is invalid.
|
|
@@ -39,11 +31,35 @@ export declare function loadCachedEmbeddings(filePath: string): EmbeddingCacheDa
|
|
|
39
31
|
* Save embeddings to disk for cross-instance sharing.
|
|
40
32
|
*/
|
|
41
33
|
export declare function saveCachedEmbeddings(filePath: string, data: EmbeddingCacheData): void;
|
|
34
|
+
/**
|
|
35
|
+
* Check available ONNX Runtime execution providers and resolve the best device.
|
|
36
|
+
* Falls back to dml → cpu if the requested provider is not available.
|
|
37
|
+
*
|
|
38
|
+
* @returns The resolved device string ('cpu', 'cuda', or 'dml').
|
|
39
|
+
*/
|
|
40
|
+
/** ORT module shape accepted by resolveDevice for testability. */
|
|
41
|
+
export interface OrtModule {
|
|
42
|
+
listSupportedBackends?: () => Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
bundled: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export declare function resolveDevice(requested: string, ortModule?: OrtModule): Promise<string>;
|
|
42
48
|
/**
|
|
43
49
|
* Embed a single text string into a vector.
|
|
44
50
|
* Triggers lazy model loading on first call.
|
|
45
51
|
*/
|
|
46
52
|
export declare function embedText(text: string, modelName: string, cacheDir: string, device?: string, localOnly?: boolean): Promise<Float32Array>;
|
|
53
|
+
/**
|
|
54
|
+
* Check if the embedding model is ready for use.
|
|
55
|
+
* When localOnly is true, verifies model files exist in the cache directory.
|
|
56
|
+
*
|
|
57
|
+
* @returns Object with `ready` flag and optional remediation `message`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkModelReadiness(modelName: string, cacheDir: string, localOnly: boolean): {
|
|
60
|
+
ready: boolean;
|
|
61
|
+
message?: string;
|
|
62
|
+
};
|
|
47
63
|
/** Signature for the embed function (injectable for testing). */
|
|
48
64
|
export type EmbedFn = (text: string, modelName: string, cacheDir: string, device: string, localOnly: boolean) => Promise<Float32Array>;
|
|
49
65
|
/**
|
|
@@ -59,4 +75,4 @@ export type EmbedFn = (text: string, modelName: string, cacheDir: string, device
|
|
|
59
75
|
*
|
|
60
76
|
* @param embedFn - Injectable embed function (defaults to module embedText; override in tests).
|
|
61
77
|
*/
|
|
62
|
-
export declare function getInstructionEmbeddings(instructions: InstructionEntry[], indexHash: string, embeddingPath: string, modelName: string, cacheDir: string, device?: string, localOnly?: boolean, embedFn?: EmbedFn): Promise<Record<string, Float32Array>>;
|
|
78
|
+
export declare function getInstructionEmbeddings(instructions: InstructionEntry[], indexHash: string, embeddingPath: string, modelName: string, cacheDir: string, device?: string, localOnly?: boolean, embedFn?: EmbedFn, store?: IEmbeddingStore): Promise<Record<string, Float32Array>>;
|
|
@@ -19,7 +19,9 @@ exports.cosineSimilarity = cosineSimilarity;
|
|
|
19
19
|
exports.isStale = isStale;
|
|
20
20
|
exports.loadCachedEmbeddings = loadCachedEmbeddings;
|
|
21
21
|
exports.saveCachedEmbeddings = saveCachedEmbeddings;
|
|
22
|
+
exports.resolveDevice = resolveDevice;
|
|
22
23
|
exports.embedText = embedText;
|
|
24
|
+
exports.checkModelReadiness = checkModelReadiness;
|
|
23
25
|
exports.getInstructionEmbeddings = getInstructionEmbeddings;
|
|
24
26
|
const fs_1 = __importDefault(require("fs"));
|
|
25
27
|
const path_1 = __importDefault(require("path"));
|
|
@@ -86,7 +88,7 @@ function saveCachedEmbeddings(filePath, data) {
|
|
|
86
88
|
if (!fs_1.default.existsSync(dir)) {
|
|
87
89
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
88
90
|
}
|
|
89
|
-
fs_1.default.writeFileSync(filePath, JSON.stringify(data), 'utf-8');
|
|
91
|
+
fs_1.default.writeFileSync(filePath, JSON.stringify(data), 'utf-8'); // lgtm[js/http-to-file-access] — filePath is config-controlled embedding cache path
|
|
90
92
|
}
|
|
91
93
|
// Helper: dynamic ESM import (same pattern as sdkServer.ts)
|
|
92
94
|
const dynamicImport = (specifier) => (Function('m', 'return import(m);'))(specifier);
|
|
@@ -136,15 +138,11 @@ async function ensureModel(modelName, cacheDir, device = 'cpu', localOnly = fals
|
|
|
136
138
|
})();
|
|
137
139
|
await modelLoading;
|
|
138
140
|
}
|
|
139
|
-
|
|
140
|
-
* Check available ONNX Runtime execution providers and resolve the best device.
|
|
141
|
-
* Falls back to dml → cpu if the requested provider is not available.
|
|
142
|
-
*/
|
|
143
|
-
async function resolveDevice(requested) {
|
|
141
|
+
async function resolveDevice(requested, ortModule) {
|
|
144
142
|
if (requested === 'cpu')
|
|
145
143
|
return 'cpu';
|
|
146
144
|
try {
|
|
147
|
-
const ort = await dynamicImport('onnxruntime-node');
|
|
145
|
+
const ort = ortModule ?? await dynamicImport('onnxruntime-node');
|
|
148
146
|
if (typeof ort.listSupportedBackends === 'function') {
|
|
149
147
|
const backends = ort.listSupportedBackends();
|
|
150
148
|
const available = backends.map((b) => b.name);
|
|
@@ -160,11 +158,15 @@ async function resolveDevice(requested) {
|
|
|
160
158
|
(0, logger_1.logWarn)(`[embeddingService] ${requested} provider not available. Available: [${available.join(', ')}]. Falling back to cpu.`);
|
|
161
159
|
return 'cpu';
|
|
162
160
|
}
|
|
161
|
+
else {
|
|
162
|
+
(0, logger_1.logWarn)(`[embeddingService] onnxruntime-node does not expose listSupportedBackends(). Falling back to cpu.`);
|
|
163
|
+
return 'cpu';
|
|
164
|
+
}
|
|
163
165
|
}
|
|
164
166
|
catch {
|
|
165
|
-
(0, logger_1.logWarn)(`[embeddingService] Could not probe ONNX Runtime backends
|
|
167
|
+
(0, logger_1.logWarn)(`[embeddingService] Could not probe ONNX Runtime backends (onnxruntime-node not installed or import failed). Falling back to cpu.`);
|
|
166
168
|
}
|
|
167
|
-
return
|
|
169
|
+
return 'cpu';
|
|
168
170
|
}
|
|
169
171
|
/**
|
|
170
172
|
* Embed a single text string into a vector.
|
|
@@ -178,6 +180,35 @@ async function embedText(text, modelName, cacheDir, device = 'cpu', localOnly =
|
|
|
178
180
|
(0, logger_1.logInfo)(`[embeddingService] embedText completed in ${(performance.now() - start).toFixed(1)}ms (${text.substring(0, 60)}${text.length > 60 ? '...' : ''})`);
|
|
179
181
|
return vec;
|
|
180
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if the embedding model is ready for use.
|
|
185
|
+
* When localOnly is true, verifies model files exist in the cache directory.
|
|
186
|
+
*
|
|
187
|
+
* @returns Object with `ready` flag and optional remediation `message`.
|
|
188
|
+
*/
|
|
189
|
+
function checkModelReadiness(modelName, cacheDir, localOnly) {
|
|
190
|
+
if (!localOnly) {
|
|
191
|
+
return { ready: true }; // Model can be downloaded on demand
|
|
192
|
+
}
|
|
193
|
+
// HuggingFace transformers caches models as: models--<org>--<name>
|
|
194
|
+
const modelDirName = `models--${modelName.replace(/\//g, '--')}`;
|
|
195
|
+
const modelPath = path_1.default.join(cacheDir, modelDirName);
|
|
196
|
+
try {
|
|
197
|
+
if (fs_1.default.existsSync(modelPath) && fs_1.default.readdirSync(modelPath).length > 0) {
|
|
198
|
+
return { ready: true };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Directory doesn't exist or can't be read
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
ready: false,
|
|
206
|
+
message: `Embedding model '${modelName}' not found in cache (${cacheDir}). ` +
|
|
207
|
+
`LOCAL_ONLY is enabled, so the model cannot be downloaded automatically. ` +
|
|
208
|
+
`To fix: set INDEX_SERVER_SEMANTIC_LOCAL_ONLY=0 to allow download, ` +
|
|
209
|
+
`or manually place the model in the cache directory.`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
181
212
|
/**
|
|
182
213
|
* Get or compute embeddings for all index instructions.
|
|
183
214
|
* Uses disk cache when available and fresh.
|
|
@@ -191,9 +222,9 @@ async function embedText(text, modelName, cacheDir, device = 'cpu', localOnly =
|
|
|
191
222
|
*
|
|
192
223
|
* @param embedFn - Injectable embed function (defaults to module embedText; override in tests).
|
|
193
224
|
*/
|
|
194
|
-
async function getInstructionEmbeddings(instructions, indexHash, embeddingPath, modelName, cacheDir, device = 'cpu', localOnly = false, embedFn = embedText) {
|
|
225
|
+
async function getInstructionEmbeddings(instructions, indexHash, embeddingPath, modelName, cacheDir, device = 'cpu', localOnly = false, embedFn = embedText, store) {
|
|
195
226
|
// Full cache hit: same index hash and model — return immediately without locking.
|
|
196
|
-
const cached = loadCachedEmbeddings(embeddingPath);
|
|
227
|
+
const cached = store ? store.load() : loadCachedEmbeddings(embeddingPath);
|
|
197
228
|
if (cached && !isStale(indexHash, cached.indexHash) && cached.modelName === modelName) {
|
|
198
229
|
const entryCount = Object.keys(cached.embeddings).length;
|
|
199
230
|
(0, logger_1.logInfo)(`[embeddingService] Embedding cache HIT: ${entryCount} entries from ${embeddingPath} (model=${modelName})`);
|
|
@@ -230,9 +261,9 @@ async function getInstructionEmbeddings(instructions, indexHash, embeddingPath,
|
|
|
230
261
|
const entryHashes = { ...existingHashes };
|
|
231
262
|
for (const inst of toCompute) {
|
|
232
263
|
const text = `${inst.title} ${inst.semanticSummary || inst.body}`;
|
|
233
|
-
embeddings[inst.id] = Array.from(await embedFn(text, modelName, cacheDir, device, localOnly));
|
|
264
|
+
embeddings[inst.id] = Array.from(await embedFn(text, modelName, cacheDir, device, localOnly)); // lgtm[js/remote-property-injection] — id is schema-validated before reaching index
|
|
234
265
|
if (inst.sourceHash)
|
|
235
|
-
entryHashes[inst.id] = inst.sourceHash;
|
|
266
|
+
entryHashes[inst.id] = inst.sourceHash; // lgtm[js/remote-property-injection] — id is schema-validated before reaching index
|
|
236
267
|
}
|
|
237
268
|
// Prune deleted instructions from cache.
|
|
238
269
|
for (const id of Object.keys(embeddings)) {
|
|
@@ -243,7 +274,13 @@ async function getInstructionEmbeddings(instructions, indexHash, embeddingPath,
|
|
|
243
274
|
}
|
|
244
275
|
// Persist updated cache.
|
|
245
276
|
try {
|
|
246
|
-
|
|
277
|
+
const cacheData = { indexHash, modelName, entryHashes, embeddings };
|
|
278
|
+
if (store) {
|
|
279
|
+
store.save(cacheData);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
saveCachedEmbeddings(embeddingPath, cacheData);
|
|
283
|
+
}
|
|
247
284
|
(0, logger_1.logInfo)('[embeddingService] Embeddings cached to disk');
|
|
248
285
|
}
|
|
249
286
|
catch (err) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared feedback storage layer.
|
|
3
|
+
* Used by both the MCP feedback_submit handler and the future dashboard CRUD REST routes.
|
|
4
|
+
* All file I/O lives here — no duplication across consumers.
|
|
5
|
+
*/
|
|
6
|
+
export interface FeedbackEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
type: 'issue' | 'status' | 'security' | 'feature-request' | 'bug-report' | 'performance' | 'usability' | 'other';
|
|
10
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
11
|
+
title: string;
|
|
12
|
+
description: string;
|
|
13
|
+
context?: {
|
|
14
|
+
clientInfo?: {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
};
|
|
18
|
+
serverVersion?: string;
|
|
19
|
+
environment?: Record<string, string>;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
toolName?: string;
|
|
22
|
+
requestId?: string;
|
|
23
|
+
};
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
status: 'new' | 'acknowledged' | 'in-progress' | 'resolved' | 'closed';
|
|
27
|
+
}
|
|
28
|
+
export interface FeedbackStorage {
|
|
29
|
+
entries: FeedbackEntry[];
|
|
30
|
+
lastUpdated: string;
|
|
31
|
+
version: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function getMaxEntries(): number;
|
|
34
|
+
export declare function getFeedbackDir(): string;
|
|
35
|
+
export declare function getFeedbackFile(): string;
|
|
36
|
+
export declare function ensureFeedbackDir(): string;
|
|
37
|
+
export declare function loadFeedbackStorage(): FeedbackStorage;
|
|
38
|
+
export declare function saveFeedbackStorage(storage: FeedbackStorage): void;
|
|
39
|
+
export declare function generateFeedbackId(type: string, timestamp: string): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared feedback storage layer.
|
|
4
|
+
* Used by both the MCP feedback_submit handler and the future dashboard CRUD REST routes.
|
|
5
|
+
* All file I/O lives here — no duplication across consumers.
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.getMaxEntries = getMaxEntries;
|
|
12
|
+
exports.getFeedbackDir = getFeedbackDir;
|
|
13
|
+
exports.getFeedbackFile = getFeedbackFile;
|
|
14
|
+
exports.ensureFeedbackDir = ensureFeedbackDir;
|
|
15
|
+
exports.loadFeedbackStorage = loadFeedbackStorage;
|
|
16
|
+
exports.saveFeedbackStorage = saveFeedbackStorage;
|
|
17
|
+
exports.generateFeedbackId = generateFeedbackId;
|
|
18
|
+
const logger_1 = require("./logger");
|
|
19
|
+
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const crypto_1 = require("crypto");
|
|
23
|
+
function getMaxEntries() {
|
|
24
|
+
return (0, runtimeConfig_1.getRuntimeConfig)().feedback.maxEntries;
|
|
25
|
+
}
|
|
26
|
+
function getFeedbackDir() {
|
|
27
|
+
return (0, runtimeConfig_1.getRuntimeConfig)().feedback.dir;
|
|
28
|
+
}
|
|
29
|
+
function getFeedbackFile() {
|
|
30
|
+
return path_1.default.join(getFeedbackDir(), 'feedback-entries.json');
|
|
31
|
+
}
|
|
32
|
+
function ensureFeedbackDir() {
|
|
33
|
+
const dir = getFeedbackDir();
|
|
34
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
35
|
+
try {
|
|
36
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
(0, logger_1.logError)('[feedback] Failed to create feedback directory', { error: String(error), dir });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
function loadFeedbackStorage() {
|
|
45
|
+
const file = getFeedbackFile();
|
|
46
|
+
ensureFeedbackDir();
|
|
47
|
+
try {
|
|
48
|
+
if (fs_1.default.existsSync(file)) {
|
|
49
|
+
const content = fs_1.default.readFileSync(file, 'utf8');
|
|
50
|
+
const parsed = JSON.parse(content);
|
|
51
|
+
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
52
|
+
throw new Error('Invalid feedback storage format');
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
(0, logger_1.logWarn)('[feedback] Failed to load feedback storage, initializing empty', { error: String(error) });
|
|
59
|
+
}
|
|
60
|
+
return { entries: [], lastUpdated: new Date().toISOString(), version: '1.0.0' };
|
|
61
|
+
}
|
|
62
|
+
function saveFeedbackStorage(storage) {
|
|
63
|
+
const file = getFeedbackFile();
|
|
64
|
+
ensureFeedbackDir();
|
|
65
|
+
try {
|
|
66
|
+
const maxEntries = getMaxEntries();
|
|
67
|
+
const entries = storage.entries.length > maxEntries
|
|
68
|
+
? [...storage.entries]
|
|
69
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
70
|
+
.slice(0, maxEntries)
|
|
71
|
+
: [...storage.entries];
|
|
72
|
+
const content = JSON.stringify({
|
|
73
|
+
...storage,
|
|
74
|
+
entries,
|
|
75
|
+
lastUpdated: new Date().toISOString(),
|
|
76
|
+
}, null, 2);
|
|
77
|
+
fs_1.default.writeFileSync(file, content, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
(0, logger_1.logError)('[feedback] Failed to save feedback storage', error instanceof Error ? error : { error: String(error) });
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function generateFeedbackId(type, timestamp) {
|
|
85
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
86
|
+
hash.update(`${type}-${timestamp}-${Math.random()}`);
|
|
87
|
+
return hash.digest('hex').substring(0, 16);
|
|
88
|
+
}
|