@jagilber-org/index-server 1.27.0 → 1.28.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 +63 -1
- package/CONTRIBUTING.md +3 -3
- package/dist/dashboard/client/admin.html +58 -28
- package/dist/dashboard/client/css/admin.css +54 -0
- package/dist/dashboard/client/js/admin.config.js +3 -6
- package/dist/dashboard/client/js/admin.embeddings.js +63 -4
- package/dist/dashboard/client/js/admin.events.js +256 -0
- package/dist/dashboard/client/js/admin.feedback.js +1 -1
- package/dist/dashboard/client/js/admin.instructions.js +1 -1
- package/dist/dashboard/client/js/admin.maintenance.js +75 -32
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/security/SecurityMonitor.js +2 -2
- package/dist/dashboard/server/AdminPanel.js +83 -6
- package/dist/dashboard/server/AdminPanelConfig.d.ts +11 -0
- package/dist/dashboard/server/AdminPanelConfig.js +47 -17
- package/dist/dashboard/server/AdminPanelState.js +5 -1
- package/dist/dashboard/server/ApiRoutes.js +2 -1
- package/dist/dashboard/server/DashboardServer.js +13 -0
- package/dist/dashboard/server/MetricsCollector.js +3 -2
- package/dist/dashboard/server/WebSocketManager.js +2 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +1 -1
- package/dist/dashboard/server/routes/admin.routes.js +143 -17
- package/dist/dashboard/server/routes/api.usage.routes.js +5 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +91 -1
- package/dist/dashboard/server/routes/instructions.routes.js +142 -12
- package/dist/dashboard/server/routes/scripts.routes.js +1 -1
- package/dist/dashboard/server/routes/sqlite.routes.js +74 -0
- package/dist/models/instruction.d.ts +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/server/sdkServer.js +12 -4
- package/dist/services/auditLog.d.ts +1 -1
- package/dist/services/auditLog.js +1 -1
- package/dist/services/embeddingService.d.ts +2 -0
- package/dist/services/embeddingService.js +16 -4
- package/dist/services/embeddingTrigger.d.ts +33 -0
- package/dist/services/embeddingTrigger.js +86 -0
- package/dist/services/eventBuffer.d.ts +45 -0
- package/dist/services/eventBuffer.js +110 -0
- package/dist/services/feedbackStorage.js +1 -1
- package/dist/services/handlers/instructions.add.js +36 -3
- package/dist/services/handlers/instructions.import.js +71 -13
- package/dist/services/handlers.dashboardConfig.js +81 -0
- package/dist/services/handlers.feedback.js +1 -1
- package/dist/services/handlers.instructionSchema.js +4 -4
- package/dist/services/handlers.search.js +3 -3
- package/dist/services/indexContext.d.ts +18 -0
- package/dist/services/indexContext.js +133 -24
- package/dist/services/instructionRecordValidation.d.ts +3 -0
- package/dist/services/instructionRecordValidation.js +64 -4
- package/dist/services/logger.js +9 -0
- package/dist/services/manifestManager.js +11 -1
- package/dist/services/seedBootstrap.js +7 -3
- package/dist/services/storage/factory.d.ts +2 -0
- package/dist/services/storage/factory.js +12 -1
- package/dist/services/toolRegistry.js +8 -8
- package/dist/services/toolRegistry.zod.js +3 -3
- package/dist/services/tracing.js +3 -1
- package/dist/versioning/schemaVersion.d.ts +1 -1
- package/dist/versioning/schemaVersion.js +47 -2
- package/package.json +54 -40
- package/schemas/index-server.code-schema.json +1 -1
- package/schemas/instruction.schema.json +3 -3
- package/schemas/json-schema/instruction-content-type.schema.json +1 -1
- package/schemas/json-schema/instruction-instruction-entry.schema.json +1 -1
- package/scripts/README.md +48 -0
- package/scripts/{generate-certs.mjs → build/generate-certs.mjs} +1 -1
- package/scripts/{setup-wizard.mjs → build/setup-wizard.mjs} +1 -1
- package/scripts/{setup-hooks.cjs → hooks/setup-hooks.cjs} +3 -3
- package/server.json +3 -3
- /package/scripts/{copy-dashboard-assets.mjs → build/copy-dashboard-assets.mjs} +0 -0
|
@@ -16,16 +16,125 @@ const registry_js_1 = require("../../../server/registry.js");
|
|
|
16
16
|
const indexContext_js_1 = require("../../../services/indexContext.js");
|
|
17
17
|
const adminAuth_js_1 = require("./adminAuth.js");
|
|
18
18
|
const handlers_search_js_1 = require("../../../services/handlers.search.js");
|
|
19
|
+
const schemaVersion_js_1 = require("../../../versioning/schemaVersion.js");
|
|
20
|
+
const runtimeConfig_js_1 = require("../../../config/runtimeConfig.js");
|
|
19
21
|
const instructionRecordValidation_js_1 = require("../../../services/instructionRecordValidation.js");
|
|
20
22
|
const logger_js_1 = require("../../../services/logger.js");
|
|
21
23
|
const pathContainment_js_1 = require("../utils/pathContainment.js");
|
|
22
|
-
/**
|
|
24
|
+
/** Validate an instruction name with a defense-in-depth path-containment guard. */
|
|
23
25
|
function safeName(name) {
|
|
24
|
-
const sanitized = String(name)
|
|
26
|
+
const sanitized = String(name);
|
|
27
|
+
const validationErrors = (0, instructionRecordValidation_js_1.validateInstructionIdSurface)(sanitized);
|
|
28
|
+
if (validationErrors.length) {
|
|
29
|
+
throw new instructionRecordValidation_js_1.InstructionValidationError(validationErrors);
|
|
30
|
+
}
|
|
25
31
|
const base = (0, indexContext_js_1.getInstructionsDir)();
|
|
26
32
|
(0, pathContainment_js_1.validatePathContainment)(node_path_1.default.resolve(base, `${sanitized}.json`), base);
|
|
27
33
|
return sanitized;
|
|
28
34
|
}
|
|
35
|
+
function requireInstructionContentObject(content) {
|
|
36
|
+
if (!content || typeof content !== 'object' || Array.isArray(content)) {
|
|
37
|
+
throw new instructionRecordValidation_js_1.InstructionValidationError(['/content: must be an object']);
|
|
38
|
+
}
|
|
39
|
+
return content;
|
|
40
|
+
}
|
|
41
|
+
function assertValidDashboardInstructionEnums(content) {
|
|
42
|
+
const validationErrors = (0, instructionRecordValidation_js_1.validateInstructionInputEnumMembership)(content);
|
|
43
|
+
if (validationErrors.length) {
|
|
44
|
+
throw new instructionRecordValidation_js_1.InstructionValidationError(validationErrors);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function assertValidDashboardInstructionShape(content) {
|
|
48
|
+
const validationErrors = [];
|
|
49
|
+
for (const [key, value] of Object.entries(content)) {
|
|
50
|
+
if (value === null)
|
|
51
|
+
validationErrors.push(`/${key}: null is not allowed`);
|
|
52
|
+
}
|
|
53
|
+
if (content.title !== undefined && typeof content.title !== 'string') {
|
|
54
|
+
validationErrors.push(`/title: must be a string, received ${typeof content.title}`);
|
|
55
|
+
}
|
|
56
|
+
if (content.body !== undefined && typeof content.body !== 'string') {
|
|
57
|
+
validationErrors.push(`/body: must be a string, received ${typeof content.body}`);
|
|
58
|
+
}
|
|
59
|
+
if (content.audience !== undefined && typeof content.audience !== 'string') {
|
|
60
|
+
validationErrors.push(`/audience: must be a string, received ${typeof content.audience}`);
|
|
61
|
+
}
|
|
62
|
+
if (content.requirement !== undefined && typeof content.requirement !== 'string') {
|
|
63
|
+
validationErrors.push(`/requirement: must be a string, received ${typeof content.requirement}`);
|
|
64
|
+
}
|
|
65
|
+
if (content.contentType !== undefined && typeof content.contentType !== 'string') {
|
|
66
|
+
validationErrors.push(`/contentType: must be a string, received ${typeof content.contentType}`);
|
|
67
|
+
}
|
|
68
|
+
if (content.priority !== undefined && typeof content.priority !== 'number') {
|
|
69
|
+
validationErrors.push(`/priority: must be a number, received ${typeof content.priority}`);
|
|
70
|
+
}
|
|
71
|
+
else if (typeof content.priority === 'number' && (!Number.isInteger(content.priority) || content.priority < 1 || content.priority > 100)) {
|
|
72
|
+
validationErrors.push('/priority: must be an integer from 1 to 100');
|
|
73
|
+
}
|
|
74
|
+
if (content.categories !== undefined && !Array.isArray(content.categories)) {
|
|
75
|
+
validationErrors.push(`/categories: must be an array of strings, received ${typeof content.categories}`);
|
|
76
|
+
}
|
|
77
|
+
else if (Array.isArray(content.categories)) {
|
|
78
|
+
for (const [index, category] of content.categories.entries()) {
|
|
79
|
+
if (typeof category !== 'string') {
|
|
80
|
+
validationErrors.push(`/categories/${index}: must be a string, received ${typeof category}`);
|
|
81
|
+
}
|
|
82
|
+
else if (!/^[a-z0-9][a-z0-9-_]{0,48}$/.test(category.toLowerCase())) {
|
|
83
|
+
validationErrors.push(`/categories/${index}: must match /^[a-z0-9][a-z0-9-_]{0,48}$/`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (validationErrors.length) {
|
|
88
|
+
throw new instructionRecordValidation_js_1.InstructionValidationError(validationErrors);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function assertDashboardBodyWithinLimit(content) {
|
|
92
|
+
if (typeof content.body !== 'string')
|
|
93
|
+
return;
|
|
94
|
+
const bodyLength = content.body.trim().length;
|
|
95
|
+
const { bodyWarnLength } = (0, runtimeConfig_js_1.getRuntimeConfig)().index;
|
|
96
|
+
if (bodyLength > bodyWarnLength) {
|
|
97
|
+
throw new instructionRecordValidation_js_1.InstructionValidationError([`/body: exceeds the ${bodyWarnLength}-character limit (${bodyLength} chars)`]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function validationErrorResponse(res, error) {
|
|
101
|
+
return res.status(400).json({ success: false, error: 'invalid_instruction', validationErrors: error.validationErrors });
|
|
102
|
+
}
|
|
103
|
+
function dashboardAudience(value) {
|
|
104
|
+
switch (value) {
|
|
105
|
+
case 'individual':
|
|
106
|
+
case 'group':
|
|
107
|
+
case 'all':
|
|
108
|
+
return value;
|
|
109
|
+
default:
|
|
110
|
+
return 'all';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function dashboardRequirement(value) {
|
|
114
|
+
switch (value) {
|
|
115
|
+
case 'mandatory':
|
|
116
|
+
case 'critical':
|
|
117
|
+
case 'recommended':
|
|
118
|
+
case 'optional':
|
|
119
|
+
case 'deprecated':
|
|
120
|
+
return value;
|
|
121
|
+
default:
|
|
122
|
+
return 'optional';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function dashboardContentType(value) {
|
|
126
|
+
switch (value) {
|
|
127
|
+
case 'template':
|
|
128
|
+
case 'workflow':
|
|
129
|
+
case 'reference':
|
|
130
|
+
case 'example':
|
|
131
|
+
case 'agent':
|
|
132
|
+
case 'instruction':
|
|
133
|
+
return value;
|
|
134
|
+
default:
|
|
135
|
+
return 'instruction';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
29
138
|
function createInstructionsRoutes() {
|
|
30
139
|
const router = (0, express_1.Router)();
|
|
31
140
|
const buildSnippet = (parts, query) => {
|
|
@@ -176,6 +285,9 @@ function createInstructionsRoutes() {
|
|
|
176
285
|
res.json({ success: true, content: entry, timestamp: Date.now() });
|
|
177
286
|
}
|
|
178
287
|
catch (error) {
|
|
288
|
+
if ((0, instructionRecordValidation_js_1.isInstructionValidationError)(error)) {
|
|
289
|
+
return validationErrorResponse(res, error);
|
|
290
|
+
}
|
|
179
291
|
(0, logger_js_1.logError)('[API] Failed to load instruction:', error);
|
|
180
292
|
res.status(500).json({ success: false, error: 'Failed to load instruction' });
|
|
181
293
|
}
|
|
@@ -193,20 +305,28 @@ function createInstructionsRoutes() {
|
|
|
193
305
|
const st = res.locals.indexState;
|
|
194
306
|
if (st.byId.has(id))
|
|
195
307
|
return res.status(409).json({ success: false, error: 'Instruction already exists' });
|
|
196
|
-
const contentObj =
|
|
308
|
+
const contentObj = requireInstructionContentObject(content);
|
|
309
|
+
assertValidDashboardInstructionEnums(contentObj);
|
|
310
|
+
assertValidDashboardInstructionShape(contentObj);
|
|
311
|
+
assertDashboardBodyWithinLimit(contentObj);
|
|
197
312
|
const entry = {
|
|
198
313
|
...contentObj,
|
|
199
314
|
id,
|
|
200
|
-
title: contentObj.title
|
|
201
|
-
body: contentObj.body
|
|
202
|
-
categories: Array.isArray(contentObj.categories)
|
|
315
|
+
title: typeof contentObj.title === 'string' ? contentObj.title : id,
|
|
316
|
+
body: typeof contentObj.body === 'string' ? contentObj.body : '',
|
|
317
|
+
categories: Array.isArray(contentObj.categories)
|
|
318
|
+
? contentObj.categories.filter((category) => typeof category === 'string')
|
|
319
|
+
: [],
|
|
203
320
|
priority: typeof contentObj.priority === 'number' ? contentObj.priority : 50,
|
|
204
|
-
audience: contentObj.audience
|
|
205
|
-
requirement: contentObj.requirement
|
|
321
|
+
audience: dashboardAudience(contentObj.audience),
|
|
322
|
+
requirement: dashboardRequirement(contentObj.requirement),
|
|
323
|
+
contentType: dashboardContentType(contentObj.contentType),
|
|
324
|
+
sourceHash: typeof contentObj.sourceHash === 'string' ? contentObj.sourceHash : '',
|
|
325
|
+
schemaVersion: typeof contentObj.schemaVersion === 'string' ? contentObj.schemaVersion : schemaVersion_js_1.SCHEMA_VERSION,
|
|
206
326
|
createdAt: new Date().toISOString(),
|
|
207
327
|
updatedAt: new Date().toISOString(),
|
|
208
328
|
};
|
|
209
|
-
await (0, indexContext_js_1.writeEntryAsync)(entry); // lgtm[js/http-to-file-access] — writes to config-controlled instructions directory
|
|
329
|
+
await (0, indexContext_js_1.writeEntryAsync)(entry, { createOnly: true }); // lgtm[js/http-to-file-access] — writes to config-controlled instructions directory
|
|
210
330
|
(0, indexContext_js_1.touchIndexVersion)();
|
|
211
331
|
(0, indexContext_js_1.invalidate)();
|
|
212
332
|
const reloaded = await (0, indexContext_js_1.ensureLoadedAsync)();
|
|
@@ -219,7 +339,10 @@ function createInstructionsRoutes() {
|
|
|
219
339
|
}
|
|
220
340
|
catch (error) {
|
|
221
341
|
if ((0, instructionRecordValidation_js_1.isInstructionValidationError)(error)) {
|
|
222
|
-
return res
|
|
342
|
+
return validationErrorResponse(res, error);
|
|
343
|
+
}
|
|
344
|
+
if ((0, indexContext_js_1.isDuplicateInstructionWriteError)(error)) {
|
|
345
|
+
return res.status(409).json({ success: false, error: 'Instruction already exists' });
|
|
223
346
|
}
|
|
224
347
|
(0, logger_js_1.logError)('[API] Failed to create instruction:', error);
|
|
225
348
|
res.status(500).json({ success: false, error: 'Failed to create instruction' });
|
|
@@ -234,13 +357,17 @@ function createInstructionsRoutes() {
|
|
|
234
357
|
const id = safeName(req.params.name);
|
|
235
358
|
if (!content)
|
|
236
359
|
return res.status(400).json({ success: false, error: 'Missing content' });
|
|
360
|
+
const contentObj = requireInstructionContentObject(content);
|
|
361
|
+
assertValidDashboardInstructionEnums(contentObj);
|
|
362
|
+
assertValidDashboardInstructionShape(contentObj);
|
|
363
|
+
assertDashboardBodyWithinLimit(contentObj);
|
|
237
364
|
const st = res.locals.indexState;
|
|
238
365
|
const existing = st.byId.get(id);
|
|
239
366
|
if (!existing)
|
|
240
367
|
return res.status(404).json({ success: false, error: 'Not found' });
|
|
241
368
|
const updated = {
|
|
242
369
|
...existing,
|
|
243
|
-
...
|
|
370
|
+
...contentObj,
|
|
244
371
|
id, // preserve id
|
|
245
372
|
updatedAt: new Date().toISOString(),
|
|
246
373
|
};
|
|
@@ -257,7 +384,7 @@ function createInstructionsRoutes() {
|
|
|
257
384
|
}
|
|
258
385
|
catch (error) {
|
|
259
386
|
if ((0, instructionRecordValidation_js_1.isInstructionValidationError)(error)) {
|
|
260
|
-
return res
|
|
387
|
+
return validationErrorResponse(res, error);
|
|
261
388
|
}
|
|
262
389
|
(0, logger_js_1.logError)('[API] Failed to update instruction:', error);
|
|
263
390
|
res.status(500).json({ success: false, error: 'Failed to update instruction' });
|
|
@@ -279,6 +406,9 @@ function createInstructionsRoutes() {
|
|
|
279
406
|
res.json({ success: true, message: 'Instruction deleted', timestamp: Date.now() });
|
|
280
407
|
}
|
|
281
408
|
catch (error) {
|
|
409
|
+
if ((0, instructionRecordValidation_js_1.isInstructionValidationError)(error)) {
|
|
410
|
+
return validationErrorResponse(res, error);
|
|
411
|
+
}
|
|
282
412
|
(0, logger_js_1.logError)('[API] Failed to delete instruction:', error);
|
|
283
413
|
res.status(500).json({ success: false, error: 'Failed to delete instruction' });
|
|
284
414
|
}
|
|
@@ -57,7 +57,7 @@ function createScriptsRoutes() {
|
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
try {
|
|
60
|
-
const scriptsDir = path_1.default.join(process.cwd(), 'scripts');
|
|
60
|
+
const scriptsDir = path_1.default.join(process.cwd(), 'scripts', 'dist');
|
|
61
61
|
const filePath = path_1.default.join(scriptsDir, meta.file); // nosemgrep: javascript.express.security.audit.express-path-join-resolve-traversal.express-path-join-resolve-traversal -- path validated below via startsWith check
|
|
62
62
|
let resolved;
|
|
63
63
|
try {
|
|
@@ -566,5 +566,79 @@ function createSqliteRoutes() {
|
|
|
566
566
|
return res.status(code).json({ success: false, error: err.message });
|
|
567
567
|
}
|
|
568
568
|
});
|
|
569
|
+
// ── Validation ────────────────────────────────────────────────────────
|
|
570
|
+
/** GET /sqlite/validate — Comprehensive database validation (read-only) */
|
|
571
|
+
router.get('/sqlite/validate', adminAuth_js_1.dashboardAdminAuth, (_req, res) => {
|
|
572
|
+
try {
|
|
573
|
+
assertSqliteActive();
|
|
574
|
+
const sqlitePath = getSqlitePath();
|
|
575
|
+
const db = new node_sqlite_1.DatabaseSync(sqlitePath, { readOnly: true });
|
|
576
|
+
const checks = [];
|
|
577
|
+
try {
|
|
578
|
+
// 1. Integrity check
|
|
579
|
+
const intRows = db.prepare('PRAGMA integrity_check').all();
|
|
580
|
+
const intOk = intRows.length === 1 && intRows[0].integrity_check === 'ok';
|
|
581
|
+
checks.push({ name: 'integrity_check', pass: intOk, detail: intOk ? 'ok' : JSON.stringify(intRows) });
|
|
582
|
+
// 2. Table counts
|
|
583
|
+
const instrCount = db.prepare('SELECT COUNT(*) as cnt FROM instructions').get()?.cnt ?? 0;
|
|
584
|
+
checks.push({ name: 'instructions_exist', pass: instrCount >= 0, detail: `count=${instrCount}` });
|
|
585
|
+
// 3. FTS5 sync
|
|
586
|
+
let ftsCount = -1;
|
|
587
|
+
try {
|
|
588
|
+
ftsCount = db.prepare('SELECT COUNT(*) as cnt FROM instructions_fts').get()?.cnt ?? -1;
|
|
589
|
+
checks.push({ name: 'fts5_sync', pass: ftsCount === instrCount, detail: `instructions=${instrCount}, fts=${ftsCount}` });
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
checks.push({ name: 'fts5_sync', pass: false, detail: 'FTS5 table not accessible' });
|
|
593
|
+
}
|
|
594
|
+
// 4. Orphaned usage records
|
|
595
|
+
try {
|
|
596
|
+
const orphanUsage = db.prepare('SELECT COUNT(*) as cnt FROM usage WHERE instruction_id NOT IN (SELECT id FROM instructions)').get()?.cnt ?? 0;
|
|
597
|
+
checks.push({ name: 'usage_no_orphans', pass: orphanUsage === 0, detail: `orphans=${orphanUsage}` });
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
checks.push({ name: 'usage_no_orphans', pass: true, detail: 'usage table not present (ok)' });
|
|
601
|
+
}
|
|
602
|
+
// 5. Embedding consistency
|
|
603
|
+
try {
|
|
604
|
+
const embCount = db.prepare('SELECT COUNT(*) as cnt FROM embeddings').get()?.cnt ?? 0;
|
|
605
|
+
const embMetaCount = db.prepare('SELECT COUNT(*) as cnt FROM embedding_meta').get()?.cnt ?? 0;
|
|
606
|
+
checks.push({ name: 'embedding_meta_sync', pass: embCount === embMetaCount, detail: `embeddings=${embCount}, meta=${embMetaCount}` });
|
|
607
|
+
const orphanEmb = db.prepare('SELECT COUNT(*) as cnt FROM embedding_meta WHERE instruction_id NOT IN (SELECT id FROM instructions)').get()?.cnt ?? 0;
|
|
608
|
+
checks.push({ name: 'embedding_no_orphans', pass: orphanEmb === 0, detail: `orphans=${orphanEmb}` });
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
checks.push({ name: 'embedding_meta_sync', pass: true, detail: 'embedding tables not present (ok)' });
|
|
612
|
+
}
|
|
613
|
+
// 6. NULL required fields
|
|
614
|
+
const nullIds = db.prepare("SELECT COUNT(*) as cnt FROM instructions WHERE id IS NULL OR id = ''").get()?.cnt ?? 0;
|
|
615
|
+
checks.push({ name: 'no_null_ids', pass: nullIds === 0, detail: `count=${nullIds}` });
|
|
616
|
+
const nullTitles = db.prepare("SELECT COUNT(*) as cnt FROM instructions WHERE title IS NULL OR title = ''").get()?.cnt ?? 0;
|
|
617
|
+
checks.push({ name: 'no_null_titles', pass: nullTitles === 0, detail: `count=${nullTitles}` });
|
|
618
|
+
const nullBodies = db.prepare("SELECT COUNT(*) as cnt FROM instructions WHERE body IS NULL OR body = ''").get()?.cnt ?? 0;
|
|
619
|
+
checks.push({ name: 'no_null_bodies', pass: nullBodies === 0, detail: `count=${nullBodies}` });
|
|
620
|
+
// 7. WAL mode
|
|
621
|
+
const jm = db.prepare('PRAGMA journal_mode').get();
|
|
622
|
+
const isWal = jm?.journal_mode === 'wal';
|
|
623
|
+
checks.push({ name: 'wal_mode', pass: isWal, detail: `journal_mode=${jm?.journal_mode ?? 'unknown'}` });
|
|
624
|
+
const allPass = checks.every(c => c.pass);
|
|
625
|
+
return res.json({
|
|
626
|
+
success: true,
|
|
627
|
+
valid: allPass,
|
|
628
|
+
totalChecks: checks.length,
|
|
629
|
+
passed: checks.filter(c => c.pass).length,
|
|
630
|
+
failed: checks.filter(c => !c.pass).length,
|
|
631
|
+
checks,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
db.close();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
const code = err.statusCode ?? 500;
|
|
640
|
+
return res.status(code).json({ success: false, error: err.message });
|
|
641
|
+
}
|
|
642
|
+
});
|
|
569
643
|
return router;
|
|
570
644
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type AudienceScope = 'individual' | 'group' | 'all';
|
|
2
2
|
export type RequirementLevel = 'mandatory' | 'critical' | 'recommended' | 'optional' | 'deprecated';
|
|
3
|
-
export type ContentType = 'instruction' | 'template' | '
|
|
3
|
+
export type ContentType = 'instruction' | 'template' | 'workflow' | 'reference' | 'example' | 'agent';
|
|
4
4
|
export interface InstructionEntry {
|
|
5
5
|
id: string;
|
|
6
6
|
title: string;
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -98,7 +98,7 @@ export declare const instructionEntry: {
|
|
|
98
98
|
};
|
|
99
99
|
};
|
|
100
100
|
readonly contentType: {
|
|
101
|
-
readonly enum: readonly ["instruction", "template", "
|
|
101
|
+
readonly enum: readonly ["instruction", "template", "workflow", "reference", "example", "agent"];
|
|
102
102
|
};
|
|
103
103
|
readonly version: {
|
|
104
104
|
readonly type: "string";
|
package/dist/schemas/index.js
CHANGED
|
@@ -44,7 +44,7 @@ exports.instructionEntry = {
|
|
|
44
44
|
workspaceId: { type: 'string' },
|
|
45
45
|
userId: { type: 'string' },
|
|
46
46
|
teamIds: { type: 'array', items: { type: 'string' } },
|
|
47
|
-
contentType: { enum: ['instruction', 'template', '
|
|
47
|
+
contentType: { enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'] },
|
|
48
48
|
version: { type: 'string' },
|
|
49
49
|
status: { enum: ['draft', 'review', 'approved', 'deprecated'] },
|
|
50
50
|
owner: { type: 'string' },
|
package/dist/server/sdkServer.js
CHANGED
|
@@ -21,6 +21,7 @@ const registry_1 = require("./registry");
|
|
|
21
21
|
const zod_1 = require("zod");
|
|
22
22
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
23
23
|
const mcpLogBridge_1 = require("../services/mcpLogBridge");
|
|
24
|
+
const logger_1 = require("../services/logger");
|
|
24
25
|
const handshakeManager_1 = require("./handshakeManager");
|
|
25
26
|
const transportFactory_1 = require("./transportFactory");
|
|
26
27
|
const mcpReadOnlySurfaces_1 = require("./mcpReadOnlySurfaces");
|
|
@@ -205,6 +206,8 @@ function createSdkServer(ServerClass) {
|
|
|
205
206
|
const args = p.arguments || {};
|
|
206
207
|
if (name === 'health_check')
|
|
207
208
|
(0, handshakeManager_1.record)('tools_call_health');
|
|
209
|
+
const __callStart = Date.now();
|
|
210
|
+
(0, logger_1.logInfo)('[rpc] tools/call', { tool: name, id: req?.id ?? null });
|
|
208
211
|
try {
|
|
209
212
|
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
210
213
|
process.stderr.write(`[rpc] call method=tools/call tool=${name} id=${req?.id ?? 'n/a'}\n`);
|
|
@@ -216,9 +219,11 @@ function createSdkServer(ServerClass) {
|
|
|
216
219
|
}
|
|
217
220
|
try {
|
|
218
221
|
const result = await Promise.resolve(handler(args));
|
|
222
|
+
const bytes = Buffer.byteLength(JSON.stringify(result), 'utf8');
|
|
223
|
+
(0, logger_1.logInfo)('[rpc] tools/call ok', { tool: name, ms: Date.now() - __callStart, bytes });
|
|
219
224
|
try {
|
|
220
225
|
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
221
|
-
process.stderr.write(`[rpc] tool_result tool=${name} bytes=${
|
|
226
|
+
process.stderr.write(`[rpc] tool_result tool=${name} bytes=${bytes}\n`);
|
|
222
227
|
}
|
|
223
228
|
catch { /* ignore */ }
|
|
224
229
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
@@ -226,7 +231,10 @@ function createSdkServer(ServerClass) {
|
|
|
226
231
|
catch (e) {
|
|
227
232
|
const code = e?.code;
|
|
228
233
|
const sem = e?.__semantic === true;
|
|
234
|
+
const msgRaw = e instanceof Error ? e.message : String(e);
|
|
235
|
+
const stack = e instanceof Error ? e.stack : undefined;
|
|
229
236
|
if (Number.isSafeInteger(code)) {
|
|
237
|
+
(0, logger_1.logError)('[rpc] tools/call error', { tool: name, ms: Date.now() - __callStart, code, semantic: sem, message: msgRaw, stack });
|
|
230
238
|
try {
|
|
231
239
|
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
232
240
|
process.stderr.write(`[rpc] tool_error_passthru tool=${name} code=${code} semantic=${sem ? '1' : '0'} msg=${e?.message || ''}\n`);
|
|
@@ -234,13 +242,13 @@ function createSdkServer(ServerClass) {
|
|
|
234
242
|
catch { /* ignore */ }
|
|
235
243
|
throw e;
|
|
236
244
|
}
|
|
237
|
-
|
|
245
|
+
(0, logger_1.logError)('[rpc] tools/call error', { tool: name, ms: Date.now() - __callStart, code: code ?? null, message: msgRaw, stack });
|
|
238
246
|
try {
|
|
239
247
|
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
240
|
-
process.stderr.write(`[rpc] tool_error_wrap tool=${name} msg=${
|
|
248
|
+
process.stderr.write(`[rpc] tool_error_wrap tool=${name} msg=${msgRaw.replace(/\s+/g, ' ')} code=${code ?? 'n/a'}\n`);
|
|
241
249
|
}
|
|
242
250
|
catch { /* ignore */ }
|
|
243
|
-
throw { code: -32603, message: 'Tool execution failed', data: { message:
|
|
251
|
+
throw { code: -32603, message: 'Tool execution failed', data: { message: msgRaw, method: name } };
|
|
244
252
|
}
|
|
245
253
|
});
|
|
246
254
|
// Lightweight ping handler (simple reachability / latency measurement)
|
|
@@ -4,7 +4,7 @@ export declare function runWithCorrelation<T>(correlationId: string, fn: () => T
|
|
|
4
4
|
export declare function getCurrentCorrelationId(): string | undefined;
|
|
5
5
|
/** Reset the cached audit log path, forcing re-resolution on next write. */
|
|
6
6
|
export declare function resetAuditLogCache(): void;
|
|
7
|
-
export type AuditKind = 'mutation' | 'read' | 'http';
|
|
7
|
+
export type AuditKind = 'mutation' | 'read' | 'http' | 'feedback';
|
|
8
8
|
export interface AuditEntry {
|
|
9
9
|
ts: string;
|
|
10
10
|
kind: AuditKind;
|
|
@@ -19,7 +19,7 @@ const logger_1 = require("./logger");
|
|
|
19
19
|
const toolRegistry_1 = require("./toolRegistry");
|
|
20
20
|
// Append-only JSONL audit log for all server operations.
|
|
21
21
|
// Each line: { ts, kind, action, ids?, meta? }
|
|
22
|
-
// kind: 'mutation' | 'read' | 'http' — classifies the entry type.
|
|
22
|
+
// kind: 'mutation' | 'read' | 'http' | 'feedback' — classifies the entry type.
|
|
23
23
|
// Path and enablement are driven by runtime configuration (instructions.auditLog).
|
|
24
24
|
// AsyncLocalStorage carries the correlation ID from registry wrapper into handler scope.
|
|
25
25
|
// This lets logAudit() calls inside handlers automatically include correlationId
|
|
@@ -58,6 +58,8 @@ export declare function embedText(text: string, modelName: string, cacheDir: str
|
|
|
58
58
|
*/
|
|
59
59
|
export declare function checkModelReadiness(modelName: string, cacheDir: string, localOnly: boolean): {
|
|
60
60
|
ready: boolean;
|
|
61
|
+
cached: boolean;
|
|
62
|
+
modelPath: string;
|
|
61
63
|
message?: string;
|
|
62
64
|
};
|
|
63
65
|
/** Signature for the embed function (injectable for testing). */
|
|
@@ -187,22 +187,34 @@ async function embedText(text, modelName, cacheDir, device = 'cpu', localOnly =
|
|
|
187
187
|
* @returns Object with `ready` flag and optional remediation `message`.
|
|
188
188
|
*/
|
|
189
189
|
function checkModelReadiness(modelName, cacheDir, localOnly) {
|
|
190
|
-
if (!localOnly) {
|
|
191
|
-
return { ready: true }; // Model can be downloaded on demand
|
|
192
|
-
}
|
|
193
190
|
// HuggingFace transformers caches models as: models--<org>--<name>
|
|
194
191
|
const modelDirName = `models--${modelName.replace(/\//g, '--')}`;
|
|
195
192
|
const modelPath = path_1.default.join(cacheDir, modelDirName);
|
|
193
|
+
let cached = false;
|
|
196
194
|
try {
|
|
197
195
|
if (fs_1.default.existsSync(modelPath) && fs_1.default.readdirSync(modelPath).length > 0) {
|
|
198
|
-
|
|
196
|
+
cached = true;
|
|
199
197
|
}
|
|
200
198
|
}
|
|
201
199
|
catch {
|
|
202
200
|
// Directory doesn't exist or can't be read
|
|
203
201
|
}
|
|
202
|
+
if (cached) {
|
|
203
|
+
return { ready: true, cached: true, modelPath };
|
|
204
|
+
}
|
|
205
|
+
if (!localOnly) {
|
|
206
|
+
return {
|
|
207
|
+
ready: true,
|
|
208
|
+
cached: false,
|
|
209
|
+
modelPath,
|
|
210
|
+
message: `Embedding model '${modelName}' is not yet cached. ` +
|
|
211
|
+
`It will be downloaded to '${cacheDir}' on first compute (~25 MB).`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
204
214
|
return {
|
|
205
215
|
ready: false,
|
|
216
|
+
cached: false,
|
|
217
|
+
modelPath,
|
|
206
218
|
message: `Embedding model '${modelName}' not found in cache (${cacheDir}). ` +
|
|
207
219
|
`LOCAL_ONLY is enabled, so the model cannot be downloaded automatically. ` +
|
|
208
220
|
`To fix: set INDEX_SERVER_SEMANTIC_LOCAL_ONLY=0 to allow download, ` +
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-compute embeddings after operations that mutate the loaded instruction set
|
|
3
|
+
* (zip import, restore, bulk migrations).
|
|
4
|
+
*
|
|
5
|
+
* Behaviour:
|
|
6
|
+
* - No-op when semantic is disabled (`INDEX_SERVER_SEMANTIC_ENABLED` falsy).
|
|
7
|
+
* - No-op when explicitly opted out via `INDEX_SERVER_AUTO_EMBED_ON_IMPORT=0`.
|
|
8
|
+
* - Runs asynchronously (fire-and-forget) so the caller's request returns promptly.
|
|
9
|
+
* - Coalesces concurrent triggers via the existing in-flight lock inside
|
|
10
|
+
* `getInstructionEmbeddings`.
|
|
11
|
+
*
|
|
12
|
+
* Logs success / failure at INFO / WARN so events surface in the events panel
|
|
13
|
+
* (constitution OB-3, OB-5).
|
|
14
|
+
*/
|
|
15
|
+
/** Whether auto-compute is enabled given current env / runtime config. */
|
|
16
|
+
export declare function autoEmbedEnabled(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Trigger an embedding compute pass after an import / restore.
|
|
19
|
+
*
|
|
20
|
+
* @param reason - Free-form context (e.g. `import-zip`, `restore`) used in logs.
|
|
21
|
+
* @returns Promise resolving when compute finishes (or immediately if skipped).
|
|
22
|
+
*/
|
|
23
|
+
export declare function triggerEmbeddingComputeAfterImport(reason: string): Promise<{
|
|
24
|
+
triggered: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
entries?: number;
|
|
27
|
+
ms?: number;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Fire-and-forget variant — schedules the compute on the next tick and returns immediately.
|
|
31
|
+
* Use this from request handlers so HTTP responses are not blocked on model warm-up.
|
|
32
|
+
*/
|
|
33
|
+
export declare function scheduleEmbeddingComputeAfterImport(reason: string): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-compute embeddings after operations that mutate the loaded instruction set
|
|
4
|
+
* (zip import, restore, bulk migrations).
|
|
5
|
+
*
|
|
6
|
+
* Behaviour:
|
|
7
|
+
* - No-op when semantic is disabled (`INDEX_SERVER_SEMANTIC_ENABLED` falsy).
|
|
8
|
+
* - No-op when explicitly opted out via `INDEX_SERVER_AUTO_EMBED_ON_IMPORT=0`.
|
|
9
|
+
* - Runs asynchronously (fire-and-forget) so the caller's request returns promptly.
|
|
10
|
+
* - Coalesces concurrent triggers via the existing in-flight lock inside
|
|
11
|
+
* `getInstructionEmbeddings`.
|
|
12
|
+
*
|
|
13
|
+
* Logs success / failure at INFO / WARN so events surface in the events panel
|
|
14
|
+
* (constitution OB-3, OB-5).
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.autoEmbedEnabled = autoEmbedEnabled;
|
|
18
|
+
exports.triggerEmbeddingComputeAfterImport = triggerEmbeddingComputeAfterImport;
|
|
19
|
+
exports.scheduleEmbeddingComputeAfterImport = scheduleEmbeddingComputeAfterImport;
|
|
20
|
+
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
21
|
+
const indexContext_1 = require("./indexContext");
|
|
22
|
+
const embeddingService_1 = require("./embeddingService");
|
|
23
|
+
const logger_1 = require("./logger");
|
|
24
|
+
const envUtils_1 = require("../utils/envUtils");
|
|
25
|
+
let lastTriggerAt = 0;
|
|
26
|
+
/** Whether auto-compute is enabled given current env / runtime config. */
|
|
27
|
+
function autoEmbedEnabled() {
|
|
28
|
+
const cfg = (0, runtimeConfig_1.getRuntimeConfig)();
|
|
29
|
+
if (!cfg.semantic.enabled)
|
|
30
|
+
return false;
|
|
31
|
+
const raw = process.env.INDEX_SERVER_AUTO_EMBED_ON_IMPORT;
|
|
32
|
+
// Default ON when semantic is enabled; explicit '0' / 'false' opts out.
|
|
33
|
+
if (raw === undefined)
|
|
34
|
+
return true;
|
|
35
|
+
return (0, envUtils_1.getBooleanEnv)('INDEX_SERVER_AUTO_EMBED_ON_IMPORT', true);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Trigger an embedding compute pass after an import / restore.
|
|
39
|
+
*
|
|
40
|
+
* @param reason - Free-form context (e.g. `import-zip`, `restore`) used in logs.
|
|
41
|
+
* @returns Promise resolving when compute finishes (or immediately if skipped).
|
|
42
|
+
*/
|
|
43
|
+
async function triggerEmbeddingComputeAfterImport(reason) {
|
|
44
|
+
if (!autoEmbedEnabled()) {
|
|
45
|
+
return { triggered: false, reason: 'auto-embed disabled or semantic disabled' };
|
|
46
|
+
}
|
|
47
|
+
// Light debounce — coalesce rapid back-to-back imports.
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (now - lastTriggerAt < 1000) {
|
|
50
|
+
return { triggered: false, reason: 'debounced' };
|
|
51
|
+
}
|
|
52
|
+
lastTriggerAt = now;
|
|
53
|
+
const cfg = (0, runtimeConfig_1.getRuntimeConfig)();
|
|
54
|
+
const sem = cfg.semantic;
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
(0, indexContext_1.ensureLoaded)();
|
|
58
|
+
const state = (0, indexContext_1.getIndexState)();
|
|
59
|
+
if (!state.list || state.list.length === 0) {
|
|
60
|
+
(0, logger_1.logInfo)(`[embedding-trigger] Skipped (no instructions loaded) reason=${reason}`);
|
|
61
|
+
return { triggered: false, reason: 'no instructions loaded' };
|
|
62
|
+
}
|
|
63
|
+
(0, logger_1.logInfo)(`[embedding-trigger] Starting auto-compute reason=${reason} entries=${state.list.length}`);
|
|
64
|
+
await (0, embeddingService_1.getInstructionEmbeddings)(state.list, state.hash, sem.embeddingPath, sem.model, sem.cacheDir, sem.device, sem.localOnly);
|
|
65
|
+
const ms = Date.now() - start;
|
|
66
|
+
(0, logger_1.logInfo)(`[embedding-trigger] Auto-compute complete reason=${reason} entries=${state.list.length} ms=${ms}`);
|
|
67
|
+
return { triggered: true, entries: state.list.length, ms };
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
// Use WARN (not ERROR) so we don't escalate transient model-load issues to ERROR-level paging.
|
|
72
|
+
(0, logger_1.logWarn)(`[embedding-trigger] Auto-compute failed reason=${reason}: ${msg}`);
|
|
73
|
+
return { triggered: false, reason: `failed: ${msg}` };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fire-and-forget variant — schedules the compute on the next tick and returns immediately.
|
|
78
|
+
* Use this from request handlers so HTTP responses are not blocked on model warm-up.
|
|
79
|
+
*/
|
|
80
|
+
function scheduleEmbeddingComputeAfterImport(reason) {
|
|
81
|
+
if (!autoEmbedEnabled())
|
|
82
|
+
return;
|
|
83
|
+
setImmediate(() => {
|
|
84
|
+
triggerEmbeddingComputeAfterImport(reason).catch(() => { });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ring buffer of recent log events at WARN/ERROR severity.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces operationally-meaningful events to the admin dashboard so operators
|
|
5
|
+
* do not have to tail server logs. Read-only, bounded; never persists to disk.
|
|
6
|
+
*
|
|
7
|
+
* Capacity defaults to 500. The logger emits records via `recordEvent()` only
|
|
8
|
+
* for WARN or ERROR levels so that volume on healthy systems is negligible.
|
|
9
|
+
*
|
|
10
|
+
* Constitution alignment: OB-1, OB-3, OB-4, OB-5 (structured, severity-visible).
|
|
11
|
+
*/
|
|
12
|
+
export type EventLevel = 'WARN' | 'ERROR';
|
|
13
|
+
export interface BufferedEvent {
|
|
14
|
+
/** Monotonically increasing per-process id (used for unread counts). */
|
|
15
|
+
id: number;
|
|
16
|
+
/** ISO 8601 timestamp. */
|
|
17
|
+
ts: string;
|
|
18
|
+
/** Severity. */
|
|
19
|
+
level: EventLevel;
|
|
20
|
+
/** Message; prefix `[module]` is preserved. */
|
|
21
|
+
msg: string;
|
|
22
|
+
/** Optional stack/detail snippet. */
|
|
23
|
+
detail?: string;
|
|
24
|
+
/** Process id of emitter. */
|
|
25
|
+
pid?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Emit a WARN/ERROR event into the buffer. Called by the logger. */
|
|
28
|
+
export declare function recordEvent(level: EventLevel, msg: string, detail?: string, pid?: number): void;
|
|
29
|
+
/** List recent events (most recent last). */
|
|
30
|
+
export declare function listEvents(opts?: {
|
|
31
|
+
sinceId?: number;
|
|
32
|
+
level?: EventLevel;
|
|
33
|
+
limit?: number;
|
|
34
|
+
}): BufferedEvent[];
|
|
35
|
+
/** Compute new-event counts since the supplied id (used for the dashboard counter bubble). */
|
|
36
|
+
export declare function eventCounts(sinceId?: number): {
|
|
37
|
+
warn: number;
|
|
38
|
+
error: number;
|
|
39
|
+
total: number;
|
|
40
|
+
latestId: number;
|
|
41
|
+
};
|
|
42
|
+
/** Clear the buffer (used by `Mark all read` and tests). */
|
|
43
|
+
export declare function clearEvents(): void;
|
|
44
|
+
/** Test-only helper. */
|
|
45
|
+
export declare function _eventBufferSize(): number;
|