wogiflow 1.0.12 → 1.0.13
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/.workflow/specs/architecture.md.template +24 -0
- package/.workflow/specs/stack.md.template +33 -0
- package/.workflow/specs/testing.md.template +36 -0
- package/README.md +90 -1
- package/package.json +1 -1
- package/scripts/MEMORY-ARCHITECTURE.md +150 -0
- package/scripts/flow +20 -19
- package/scripts/flow-auto-context.js +97 -3
- package/scripts/flow-conflict-resolver.js +735 -0
- package/scripts/flow-context-gatherer.js +520 -0
- package/scripts/flow-context-monitor.js +148 -19
- package/scripts/flow-damage-control.js +5 -1
- package/scripts/flow-export-profile +168 -1
- package/scripts/flow-import-profile +257 -6
- package/scripts/flow-instruction-richness.js +182 -18
- package/scripts/flow-knowledge-router.js +2 -0
- package/scripts/flow-knowledge-sync.js +2 -0
- package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
- package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
- package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
- package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
- package/scripts/flow-memory-db.js +386 -1
- package/scripts/flow-memory-sync.js +2 -0
- package/scripts/flow-model-adapter.js +53 -29
- package/scripts/flow-model-router.js +246 -1
- package/scripts/flow-morning.js +94 -0
- package/scripts/flow-onboard +223 -10
- package/scripts/flow-orchestrate-validation.js +539 -0
- package/scripts/flow-orchestrate.js +16 -507
- package/scripts/flow-pattern-extractor.js +1265 -0
- package/scripts/flow-prompt-composer.js +222 -2
- package/scripts/flow-quality-guard.js +594 -0
- package/scripts/flow-section-index.js +713 -0
- package/scripts/flow-section-resolver.js +484 -0
- package/scripts/flow-session-end.js +188 -2
- package/scripts/flow-skill-create.js +19 -3
- package/scripts/flow-skill-matcher.js +122 -7
- package/scripts/flow-statusline-setup.js +218 -0
- package/scripts/flow-step-review.js +19 -0
- package/scripts/flow-tech-debt.js +734 -0
- package/scripts/flow-utils.js +2 -0
- package/scripts/hooks/core/long-input-gate.js +293 -0
- package/scripts/flow-parallel-detector.js +0 -399
- package/scripts/flow-parallel-dispatch.js +0 -987
- /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Wogi Flow - Memory Database Module
|
|
5
5
|
*
|
|
6
|
+
* See MEMORY-ARCHITECTURE.md for how this fits with other memory/knowledge modules.
|
|
7
|
+
*
|
|
6
8
|
* Shared database operations for memory storage.
|
|
7
9
|
* Used by both MCP server and CLI tools.
|
|
8
10
|
*
|
|
@@ -27,6 +29,37 @@ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
|
27
29
|
const MEMORY_DIR = path.join(WORKFLOW_DIR, 'memory');
|
|
28
30
|
const DB_PATH = path.join(MEMORY_DIR, 'local.db');
|
|
29
31
|
|
|
32
|
+
// ============================================================
|
|
33
|
+
// Safe JSON Helpers
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Safely parse pins JSON with validation
|
|
38
|
+
* Prevents prototype pollution and validates structure
|
|
39
|
+
* @param {string} pinsJson - JSON string of pins array
|
|
40
|
+
* @returns {string[]} - Parsed pins array (empty on error)
|
|
41
|
+
*/
|
|
42
|
+
function safeParsePins(pinsJson) {
|
|
43
|
+
if (!pinsJson || pinsJson === '[]') return [];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Check for prototype pollution attempts
|
|
47
|
+
if (/__proto__|constructor|prototype/i.test(pinsJson)) {
|
|
48
|
+
console.warn('[safeParsePins] Suspicious content detected in pins JSON');
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parsed = JSON.parse(pinsJson);
|
|
53
|
+
|
|
54
|
+
// Validate it's an array of strings
|
|
55
|
+
if (!Array.isArray(parsed)) return [];
|
|
56
|
+
|
|
57
|
+
return parsed.filter(p => typeof p === 'string');
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
// ============================================================
|
|
31
64
|
// Database Singleton
|
|
32
65
|
// ============================================================
|
|
@@ -162,6 +195,26 @@ async function initDatabase() {
|
|
|
162
195
|
)
|
|
163
196
|
`);
|
|
164
197
|
|
|
198
|
+
// Section index table for Smart Context System (Phase 1)
|
|
199
|
+
db.run(`
|
|
200
|
+
CREATE TABLE IF NOT EXISTS sections (
|
|
201
|
+
id TEXT PRIMARY KEY,
|
|
202
|
+
source TEXT NOT NULL,
|
|
203
|
+
category TEXT,
|
|
204
|
+
title TEXT NOT NULL,
|
|
205
|
+
pins TEXT,
|
|
206
|
+
content TEXT NOT NULL,
|
|
207
|
+
line_start INTEGER,
|
|
208
|
+
line_end INTEGER,
|
|
209
|
+
content_hash TEXT,
|
|
210
|
+
embedding TEXT,
|
|
211
|
+
access_count INTEGER DEFAULT 0,
|
|
212
|
+
last_accessed TEXT,
|
|
213
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
214
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
215
|
+
)
|
|
216
|
+
`);
|
|
217
|
+
|
|
165
218
|
// Migrate existing databases - add new columns if they don't exist
|
|
166
219
|
const migrations = [
|
|
167
220
|
'ALTER TABLE facts ADD COLUMN last_accessed TEXT',
|
|
@@ -183,6 +236,9 @@ async function initDatabase() {
|
|
|
183
236
|
try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_cold_archived ON facts_cold(archived_at)'); } catch {}
|
|
184
237
|
try { db.run('CREATE INDEX IF NOT EXISTS idx_proposals_status ON proposals(status)'); } catch {}
|
|
185
238
|
try { db.run('CREATE INDEX IF NOT EXISTS idx_prd_prd_id ON prd_chunks(prd_id)'); } catch {}
|
|
239
|
+
try { db.run('CREATE INDEX IF NOT EXISTS idx_sections_source ON sections(source)'); } catch {}
|
|
240
|
+
try { db.run('CREATE INDEX IF NOT EXISTS idx_sections_category ON sections(category)'); } catch {}
|
|
241
|
+
try { db.run('CREATE INDEX IF NOT EXISTS idx_sections_hash ON sections(content_hash)'); } catch {}
|
|
186
242
|
|
|
187
243
|
saveDatabase();
|
|
188
244
|
return db;
|
|
@@ -240,7 +296,7 @@ async function getEmbedder() {
|
|
|
240
296
|
}
|
|
241
297
|
return null;
|
|
242
298
|
}
|
|
243
|
-
throw
|
|
299
|
+
throw err; // Re-throw other errors
|
|
244
300
|
}
|
|
245
301
|
}
|
|
246
302
|
return embedder;
|
|
@@ -1051,6 +1107,326 @@ async function restoreFromColdStorage(factId) {
|
|
|
1051
1107
|
return { restored: true };
|
|
1052
1108
|
}
|
|
1053
1109
|
|
|
1110
|
+
// ============================================================
|
|
1111
|
+
// Section Index Operations (Smart Context System)
|
|
1112
|
+
// ============================================================
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Sync sections from section-index.json to database
|
|
1116
|
+
* @param {Object} index - Section index object from flow-section-index.js
|
|
1117
|
+
* @returns {Object} - { synced, updated, unchanged }
|
|
1118
|
+
*/
|
|
1119
|
+
async function syncSectionsFromIndex(index) {
|
|
1120
|
+
await initDatabase();
|
|
1121
|
+
const results = { synced: 0, updated: 0, unchanged: 0, deleted: 0 };
|
|
1122
|
+
|
|
1123
|
+
if (!index || !index.sources) {
|
|
1124
|
+
return results;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Collect all section IDs from index
|
|
1128
|
+
const indexSectionIds = new Set();
|
|
1129
|
+
|
|
1130
|
+
for (const [sourceName, sourceData] of Object.entries(index.sources)) {
|
|
1131
|
+
const items = sourceData.sections || sourceData.rows || [];
|
|
1132
|
+
|
|
1133
|
+
for (const item of items) {
|
|
1134
|
+
indexSectionIds.add(item.id);
|
|
1135
|
+
|
|
1136
|
+
// Check if section exists
|
|
1137
|
+
const existing = db.exec('SELECT content_hash FROM sections WHERE id = ?', [item.id]);
|
|
1138
|
+
const existingRows = queryToRows(existing);
|
|
1139
|
+
|
|
1140
|
+
if (existingRows.length === 0) {
|
|
1141
|
+
// Insert new section
|
|
1142
|
+
db.run(`
|
|
1143
|
+
INSERT INTO sections (id, source, category, title, pins, content, line_start, line_end, content_hash)
|
|
1144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1145
|
+
`, [
|
|
1146
|
+
item.id,
|
|
1147
|
+
sourceName,
|
|
1148
|
+
item.category || null,
|
|
1149
|
+
item.title || item.name,
|
|
1150
|
+
JSON.stringify(item.pins || []),
|
|
1151
|
+
item.content || JSON.stringify(item.data || {}),
|
|
1152
|
+
item.lineStart || item.line || null,
|
|
1153
|
+
item.lineEnd || item.line || null,
|
|
1154
|
+
item.contentHash || null
|
|
1155
|
+
]);
|
|
1156
|
+
results.synced++;
|
|
1157
|
+
} else if (existingRows[0].content_hash !== item.contentHash) {
|
|
1158
|
+
// Update existing section if content changed
|
|
1159
|
+
db.run(`
|
|
1160
|
+
UPDATE sections SET
|
|
1161
|
+
category = ?, title = ?, pins = ?, content = ?,
|
|
1162
|
+
line_start = ?, line_end = ?, content_hash = ?,
|
|
1163
|
+
updated_at = datetime('now')
|
|
1164
|
+
WHERE id = ?
|
|
1165
|
+
`, [
|
|
1166
|
+
item.category || null,
|
|
1167
|
+
item.title || item.name,
|
|
1168
|
+
JSON.stringify(item.pins || []),
|
|
1169
|
+
item.content || JSON.stringify(item.data || {}),
|
|
1170
|
+
item.lineStart || item.line || null,
|
|
1171
|
+
item.lineEnd || item.line || null,
|
|
1172
|
+
item.contentHash || null,
|
|
1173
|
+
item.id
|
|
1174
|
+
]);
|
|
1175
|
+
results.updated++;
|
|
1176
|
+
} else {
|
|
1177
|
+
results.unchanged++;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Remove sections that are no longer in the index
|
|
1183
|
+
const existingResult = db.exec('SELECT id FROM sections');
|
|
1184
|
+
const existingIds = queryToRows(existingResult).map(r => r.id);
|
|
1185
|
+
|
|
1186
|
+
for (const existingId of existingIds) {
|
|
1187
|
+
if (!indexSectionIds.has(existingId)) {
|
|
1188
|
+
db.run('DELETE FROM sections WHERE id = ?', [existingId]);
|
|
1189
|
+
results.deleted++;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
saveDatabase();
|
|
1194
|
+
return results;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Search sections by pins (keyword matching)
|
|
1199
|
+
* @param {string[]} pins - Pins to match
|
|
1200
|
+
* @param {Object} options - { limit, trackAccess }
|
|
1201
|
+
* @returns {Object[]} - Matching sections with match scores
|
|
1202
|
+
*/
|
|
1203
|
+
async function searchSectionsByPins(pins, options = {}) {
|
|
1204
|
+
await initDatabase();
|
|
1205
|
+
const { limit = 20, trackAccess = true } = options;
|
|
1206
|
+
|
|
1207
|
+
const result = db.exec('SELECT * FROM sections');
|
|
1208
|
+
const sections = queryToRows(result);
|
|
1209
|
+
|
|
1210
|
+
if (sections.length === 0) return [];
|
|
1211
|
+
|
|
1212
|
+
const pinsLower = pins.map(p => p.toLowerCase());
|
|
1213
|
+
|
|
1214
|
+
// Score each section by pin matches
|
|
1215
|
+
const scored = sections.map(section => {
|
|
1216
|
+
const sectionPins = safeParsePins(section.pins).map(p => p.toLowerCase());
|
|
1217
|
+
const matchCount = pinsLower.filter(p => sectionPins.includes(p)).length;
|
|
1218
|
+
const matchScore = pinsLower.length > 0 ? matchCount / pinsLower.length : 0;
|
|
1219
|
+
|
|
1220
|
+
return {
|
|
1221
|
+
...section,
|
|
1222
|
+
pins: safeParsePins(section.pins),
|
|
1223
|
+
matchCount,
|
|
1224
|
+
matchScore
|
|
1225
|
+
};
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// Filter and sort
|
|
1229
|
+
const matches = scored
|
|
1230
|
+
.filter(s => s.matchCount > 0)
|
|
1231
|
+
.sort((a, b) => b.matchScore - a.matchScore)
|
|
1232
|
+
.slice(0, limit);
|
|
1233
|
+
|
|
1234
|
+
// Track access
|
|
1235
|
+
if (trackAccess && matches.length > 0) {
|
|
1236
|
+
const ids = matches.map(m => m.id);
|
|
1237
|
+
db.run(`
|
|
1238
|
+
UPDATE sections SET
|
|
1239
|
+
access_count = access_count + 1,
|
|
1240
|
+
last_accessed = datetime('now')
|
|
1241
|
+
WHERE id IN (${ids.map(() => '?').join(',')})
|
|
1242
|
+
`, ids);
|
|
1243
|
+
saveDatabase();
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return matches;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Search sections by semantic similarity (with embedding)
|
|
1251
|
+
* Falls back to text search if embeddings unavailable
|
|
1252
|
+
* @param {string} query - Search query
|
|
1253
|
+
* @param {Object} options - { limit, category, trackAccess }
|
|
1254
|
+
* @returns {Object[]} - Matching sections with similarity scores
|
|
1255
|
+
*/
|
|
1256
|
+
async function searchSectionsBySimilarity(query, options = {}) {
|
|
1257
|
+
await initDatabase();
|
|
1258
|
+
const { limit = 10, category = null, trackAccess = true } = options;
|
|
1259
|
+
|
|
1260
|
+
const queryEmbedding = await getEmbedding(query);
|
|
1261
|
+
|
|
1262
|
+
let sql = 'SELECT * FROM sections WHERE 1=1';
|
|
1263
|
+
const params = [];
|
|
1264
|
+
|
|
1265
|
+
if (category) {
|
|
1266
|
+
sql += ' AND category = ?';
|
|
1267
|
+
params.push(category);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const result = db.exec(sql, params);
|
|
1271
|
+
const sections = queryToRows(result);
|
|
1272
|
+
|
|
1273
|
+
if (sections.length === 0) return [];
|
|
1274
|
+
|
|
1275
|
+
let scored;
|
|
1276
|
+
if (queryEmbedding) {
|
|
1277
|
+
// Semantic search with embeddings
|
|
1278
|
+
scored = sections.map(section => {
|
|
1279
|
+
const embedding = section.embedding ? jsonToEmbedding(section.embedding) : [];
|
|
1280
|
+
const similarity = embedding.length > 0 ? cosineSimilarity(queryEmbedding, embedding) : 0;
|
|
1281
|
+
return { ...section, pins: safeParsePins(section.pins), similarity };
|
|
1282
|
+
});
|
|
1283
|
+
} else {
|
|
1284
|
+
// Fallback: text matching
|
|
1285
|
+
const queryLower = query.toLowerCase();
|
|
1286
|
+
const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
|
|
1287
|
+
scored = sections.map(section => {
|
|
1288
|
+
const contentLower = (section.content + ' ' + section.title).toLowerCase();
|
|
1289
|
+
const matches = queryWords.filter(w => contentLower.includes(w)).length;
|
|
1290
|
+
const similarity = queryWords.length > 0 ? matches / queryWords.length : 0;
|
|
1291
|
+
return { ...section, pins: safeParsePins(section.pins), similarity };
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Sort and limit
|
|
1296
|
+
const matches = scored
|
|
1297
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
1298
|
+
.slice(0, limit);
|
|
1299
|
+
|
|
1300
|
+
// Track access
|
|
1301
|
+
if (trackAccess && matches.length > 0) {
|
|
1302
|
+
const ids = matches.filter(m => m.similarity > 0.1).map(m => m.id);
|
|
1303
|
+
if (ids.length > 0) {
|
|
1304
|
+
db.run(`
|
|
1305
|
+
UPDATE sections SET
|
|
1306
|
+
access_count = access_count + 1,
|
|
1307
|
+
last_accessed = datetime('now')
|
|
1308
|
+
WHERE id IN (${ids.map(() => '?').join(',')})
|
|
1309
|
+
`, ids);
|
|
1310
|
+
saveDatabase();
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return matches;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Get section by ID
|
|
1319
|
+
* @param {string} sectionId - Section ID
|
|
1320
|
+
* @param {boolean} trackAccess - Whether to track access
|
|
1321
|
+
* @returns {Object|null} - Section object or null
|
|
1322
|
+
*/
|
|
1323
|
+
async function getSectionById(sectionId, trackAccess = true) {
|
|
1324
|
+
await initDatabase();
|
|
1325
|
+
|
|
1326
|
+
const result = db.exec('SELECT * FROM sections WHERE id = ?', [sectionId]);
|
|
1327
|
+
const rows = queryToRows(result);
|
|
1328
|
+
|
|
1329
|
+
if (rows.length === 0) return null;
|
|
1330
|
+
|
|
1331
|
+
const section = rows[0];
|
|
1332
|
+
section.pins = safeParsePins(section.pins);
|
|
1333
|
+
|
|
1334
|
+
// Track access
|
|
1335
|
+
if (trackAccess) {
|
|
1336
|
+
db.run(`
|
|
1337
|
+
UPDATE sections SET
|
|
1338
|
+
access_count = access_count + 1,
|
|
1339
|
+
last_accessed = datetime('now')
|
|
1340
|
+
WHERE id = ?
|
|
1341
|
+
`, [sectionId]);
|
|
1342
|
+
saveDatabase();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return section;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Get all sections from a source
|
|
1350
|
+
* @param {string} source - Source file name (e.g., "decisions.md")
|
|
1351
|
+
* @returns {Object[]} - All sections from that source
|
|
1352
|
+
*/
|
|
1353
|
+
async function getSectionsBySource(source) {
|
|
1354
|
+
await initDatabase();
|
|
1355
|
+
|
|
1356
|
+
const result = db.exec('SELECT * FROM sections WHERE source = ? ORDER BY line_start', [source]);
|
|
1357
|
+
const sections = queryToRows(result);
|
|
1358
|
+
|
|
1359
|
+
return sections.map(s => ({
|
|
1360
|
+
...s,
|
|
1361
|
+
pins: safeParsePins(s.pins)
|
|
1362
|
+
}));
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Get section statistics
|
|
1367
|
+
* @returns {Object} - Stats about sections
|
|
1368
|
+
*/
|
|
1369
|
+
async function getSectionStats() {
|
|
1370
|
+
await initDatabase();
|
|
1371
|
+
|
|
1372
|
+
function count(sql, params = []) {
|
|
1373
|
+
const result = db.exec(sql, params);
|
|
1374
|
+
if (!result.length || !result[0].values.length) return 0;
|
|
1375
|
+
return result[0].values[0][0];
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function grouped(sql) {
|
|
1379
|
+
const result = db.exec(sql);
|
|
1380
|
+
if (!result.length) return {};
|
|
1381
|
+
return Object.fromEntries(result[0].values.map(row => [row[0] || 'unknown', row[1]]));
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return {
|
|
1385
|
+
total: count('SELECT COUNT(*) FROM sections'),
|
|
1386
|
+
bySource: grouped('SELECT source, COUNT(*) FROM sections GROUP BY source'),
|
|
1387
|
+
byCategory: grouped('SELECT category, COUNT(*) FROM sections GROUP BY category'),
|
|
1388
|
+
neverAccessed: count('SELECT COUNT(*) FROM sections WHERE access_count = 0'),
|
|
1389
|
+
topAccessed: queryToRows(db.exec(`
|
|
1390
|
+
SELECT id, title, source, access_count
|
|
1391
|
+
FROM sections
|
|
1392
|
+
WHERE access_count > 0
|
|
1393
|
+
ORDER BY access_count DESC
|
|
1394
|
+
LIMIT 5
|
|
1395
|
+
`))
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Generate embeddings for sections that don't have them
|
|
1401
|
+
* @returns {Object} - { generated, skipped, failed }
|
|
1402
|
+
*/
|
|
1403
|
+
async function generateSectionEmbeddings() {
|
|
1404
|
+
await initDatabase();
|
|
1405
|
+
const results = { generated: 0, skipped: 0, failed: 0 };
|
|
1406
|
+
|
|
1407
|
+
const result = db.exec('SELECT id, content, title FROM sections WHERE embedding IS NULL');
|
|
1408
|
+
const sections = queryToRows(result);
|
|
1409
|
+
|
|
1410
|
+
for (const section of sections) {
|
|
1411
|
+
try {
|
|
1412
|
+
const text = `${section.title}\n${section.content}`;
|
|
1413
|
+
const embedding = await getEmbedding(text);
|
|
1414
|
+
|
|
1415
|
+
if (embedding) {
|
|
1416
|
+
db.run('UPDATE sections SET embedding = ? WHERE id = ?', [embeddingToJson(embedding), section.id]);
|
|
1417
|
+
results.generated++;
|
|
1418
|
+
} else {
|
|
1419
|
+
results.skipped++;
|
|
1420
|
+
}
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
results.failed++;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
saveDatabase();
|
|
1427
|
+
return results;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1054
1430
|
// ============================================================
|
|
1055
1431
|
// Exports
|
|
1056
1432
|
// ============================================================
|
|
@@ -1104,6 +1480,15 @@ module.exports = {
|
|
|
1104
1480
|
getPromotionCandidates,
|
|
1105
1481
|
restoreFromColdStorage,
|
|
1106
1482
|
|
|
1483
|
+
// Sections (Smart Context System)
|
|
1484
|
+
syncSectionsFromIndex,
|
|
1485
|
+
searchSectionsByPins,
|
|
1486
|
+
searchSectionsBySimilarity,
|
|
1487
|
+
getSectionById,
|
|
1488
|
+
getSectionsBySource,
|
|
1489
|
+
getSectionStats,
|
|
1490
|
+
generateSectionEmbeddings,
|
|
1491
|
+
|
|
1107
1492
|
// Paths
|
|
1108
1493
|
DB_PATH,
|
|
1109
1494
|
MEMORY_DIR
|
|
@@ -56,34 +56,40 @@ const MODEL_PATTERNS = {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Get current model from config or environment
|
|
59
|
+
*
|
|
60
|
+
* Note: This delegates to flow-models.js which is the single source of truth
|
|
61
|
+
* for model detection. This function returns just the normalized name string
|
|
62
|
+
* for backward compatibility with callers expecting a string.
|
|
59
63
|
*/
|
|
60
64
|
function getCurrentModel() {
|
|
61
|
-
|
|
65
|
+
try {
|
|
66
|
+
// Use flow-models as the single source of truth
|
|
67
|
+
const { getCurrentModel: getModelFromRegistry } = require('./flow-models');
|
|
68
|
+
const result = getModelFromRegistry();
|
|
69
|
+
return normalizeModelName(result.name || 'claude-opus');
|
|
70
|
+
} catch {
|
|
71
|
+
// Fallback if flow-models not available
|
|
72
|
+
const config = getConfig();
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
if (config.hybrid?.enabled) {
|
|
75
|
+
if (config.hybrid.executor?.model) {
|
|
76
|
+
return normalizeModelName(config.hybrid.executor.model);
|
|
77
|
+
}
|
|
78
|
+
if (config.hybrid.model) {
|
|
79
|
+
return normalizeModelName(config.hybrid.model);
|
|
80
|
+
}
|
|
68
81
|
}
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
return normalizeModelName(
|
|
82
|
+
|
|
83
|
+
if (process.env.CLAUDE_MODEL) {
|
|
84
|
+
return normalizeModelName(process.env.CLAUDE_MODEL);
|
|
72
85
|
}
|
|
73
|
-
}
|
|
74
86
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
87
|
+
if (config.modelAdapters?.currentModel) {
|
|
88
|
+
return normalizeModelName(config.modelAdapters.currentModel);
|
|
89
|
+
}
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
if (config.modelAdapters?.currentModel) {
|
|
82
|
-
return normalizeModelName(config.modelAdapters.currentModel);
|
|
91
|
+
return 'claude-opus';
|
|
83
92
|
}
|
|
84
|
-
|
|
85
|
-
// Default to claude-opus (most capable)
|
|
86
|
-
return 'claude-opus';
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
/**
|
|
@@ -130,6 +136,18 @@ function getAdapterPath(modelName) {
|
|
|
130
136
|
return path.join(ADAPTERS_DIR, `${normalized}.md`);
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Safely read file content with try-catch
|
|
141
|
+
*/
|
|
142
|
+
function safeReadFile(filePath) {
|
|
143
|
+
try {
|
|
144
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
145
|
+
} catch (err) {
|
|
146
|
+
// File may have been deleted/moved between existsSync and readFileSync (TOCTOU)
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
/**
|
|
134
152
|
* Load adapter file content
|
|
135
153
|
*/
|
|
@@ -142,9 +160,11 @@ function loadAdapter(modelName) {
|
|
|
142
160
|
const familyPath = path.join(ADAPTERS_DIR, `${family}-default.md`);
|
|
143
161
|
|
|
144
162
|
if (fs.existsSync(familyPath)) {
|
|
163
|
+
const content = safeReadFile(familyPath);
|
|
164
|
+
if (content === null) return null;
|
|
145
165
|
return {
|
|
146
166
|
path: familyPath,
|
|
147
|
-
content
|
|
167
|
+
content,
|
|
148
168
|
isDefault: true
|
|
149
169
|
};
|
|
150
170
|
}
|
|
@@ -152,9 +172,11 @@ function loadAdapter(modelName) {
|
|
|
152
172
|
// Load template as last resort
|
|
153
173
|
const templatePath = path.join(ADAPTERS_DIR, '_template.md');
|
|
154
174
|
if (fs.existsSync(templatePath)) {
|
|
175
|
+
const content = safeReadFile(templatePath);
|
|
176
|
+
if (content === null) return null;
|
|
155
177
|
return {
|
|
156
178
|
path: templatePath,
|
|
157
|
-
content
|
|
179
|
+
content,
|
|
158
180
|
isTemplate: true
|
|
159
181
|
};
|
|
160
182
|
}
|
|
@@ -162,9 +184,11 @@ function loadAdapter(modelName) {
|
|
|
162
184
|
return null;
|
|
163
185
|
}
|
|
164
186
|
|
|
187
|
+
const content = safeReadFile(adapterPath);
|
|
188
|
+
if (content === null) return null;
|
|
165
189
|
return {
|
|
166
190
|
path: adapterPath,
|
|
167
|
-
content
|
|
191
|
+
content,
|
|
168
192
|
isDefault: false
|
|
169
193
|
};
|
|
170
194
|
}
|
|
@@ -476,7 +500,7 @@ function storeSingleLearning(modelName, learning, context = {}) {
|
|
|
476
500
|
let content = '';
|
|
477
501
|
|
|
478
502
|
if (fs.existsSync(adapterPath)) {
|
|
479
|
-
content =
|
|
503
|
+
content = safeReadFile(adapterPath) || '';
|
|
480
504
|
} else {
|
|
481
505
|
// Ensure directory exists
|
|
482
506
|
const dir = path.dirname(adapterPath);
|
|
@@ -487,8 +511,8 @@ function storeSingleLearning(modelName, learning, context = {}) {
|
|
|
487
511
|
// Create from template or minimal
|
|
488
512
|
const templatePath = path.join(ADAPTERS_DIR, '_template.md');
|
|
489
513
|
if (fs.existsSync(templatePath)) {
|
|
490
|
-
|
|
491
|
-
|
|
514
|
+
const templateContent = safeReadFile(templatePath);
|
|
515
|
+
content = templateContent ? templateContent.replace('{{MODEL_NAME}}', modelName) : `# ${modelName} Adapter\n\n## Learnings\n`;
|
|
492
516
|
} else {
|
|
493
517
|
content = `# ${modelName} Adapter\n\n## Learnings\n`;
|
|
494
518
|
}
|
|
@@ -539,13 +563,13 @@ function addLearningToAdapter(modelName, errors) {
|
|
|
539
563
|
let content = '';
|
|
540
564
|
|
|
541
565
|
if (fs.existsSync(adapterPath)) {
|
|
542
|
-
content =
|
|
566
|
+
content = safeReadFile(adapterPath) || '';
|
|
543
567
|
} else {
|
|
544
568
|
// Create from template
|
|
545
569
|
const templatePath = path.join(ADAPTERS_DIR, '_template.md');
|
|
546
570
|
if (fs.existsSync(templatePath)) {
|
|
547
|
-
|
|
548
|
-
|
|
571
|
+
const templateContent = safeReadFile(templatePath);
|
|
572
|
+
content = templateContent ? templateContent.replace('{{MODEL_NAME}}', modelName) : `# ${modelName} Adapter\n\n## Learnings\n`;
|
|
549
573
|
} else {
|
|
550
574
|
content = `# ${modelName} Adapter\n\n## Learnings\n`;
|
|
551
575
|
}
|