@jaimevalasek/aioson 1.5.1 → 1.6.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/README.md +6 -0
- package/docs/design-previews/aurora-command-ui-website.html +884 -0
- package/docs/design-previews/aurora-command-ui.html +682 -0
- package/docs/design-previews/bold-editorial-ui-website.html +658 -0
- package/docs/design-previews/bold-editorial-ui.html +717 -0
- package/docs/design-previews/clean-saas-ui-website.html +1202 -0
- package/docs/design-previews/clean-saas-ui.html +549 -0
- package/docs/design-previews/cognitive-core-ui-website.html +1009 -0
- package/docs/design-previews/cognitive-core-ui.html +463 -0
- package/docs/design-previews/glassmorphism-ui-website.html +572 -0
- package/docs/design-previews/glassmorphism-ui.html +886 -0
- package/docs/design-previews/index.html +699 -0
- package/docs/design-previews/interface-design-website.html +1187 -0
- package/docs/design-previews/interface-design.html +513 -0
- package/docs/design-previews/neo-brutalist-ui-website.html +621 -0
- package/docs/design-previews/neo-brutalist-ui.html +797 -0
- package/docs/design-previews/premium-command-center-ui-website.html +1217 -0
- package/docs/design-previews/premium-command-center-ui.html +552 -0
- package/docs/design-previews/warm-craft-ui-website.html +684 -0
- package/docs/design-previews/warm-craft-ui.html +739 -0
- package/docs/en/cli-reference.md +20 -9
- package/docs/pt/README.md +7 -0
- package/docs/pt/agent-sharding.md +132 -0
- package/docs/pt/agentes.md +8 -2
- package/docs/pt/busca-de-contexto.md +129 -0
- package/docs/pt/cache-de-contexto.md +156 -0
- package/docs/pt/comandos-cli.md +28 -0
- package/docs/pt/design-hybrid-forge.md +107 -0
- package/docs/pt/inicio-rapido.md +54 -3
- package/docs/pt/inteligencia-adaptativa.md +324 -0
- package/docs/pt/monitor-de-contexto.md +104 -0
- package/docs/pt/recuperacao-de-sessao.md +125 -0
- package/docs/pt/sandbox.md +125 -0
- package/docs/pt/skills.md +98 -6
- package/package.json +1 -1
- package/src/agent-loader.js +280 -0
- package/src/cli.js +94 -0
- package/src/commands/agent-loader.js +85 -0
- package/src/commands/context-cache.js +90 -0
- package/src/commands/context-monitor.js +92 -0
- package/src/commands/context-search.js +66 -0
- package/src/commands/design-hybrid-options.js +385 -0
- package/src/commands/health.js +214 -0
- package/src/commands/init.js +54 -13
- package/src/commands/install.js +52 -13
- package/src/commands/learning-evolve.js +355 -0
- package/src/commands/live.js +34 -0
- package/src/commands/recovery.js +43 -0
- package/src/commands/sandbox.js +37 -0
- package/src/commands/setup-context.js +22 -2
- package/src/commands/setup.js +178 -0
- package/src/commands/skill.js +79 -32
- package/src/commands/tool-registry-cmd.js +232 -0
- package/src/commands/update.js +7 -0
- package/src/constants.js +9 -0
- package/src/context-cache.js +159 -0
- package/src/context-search.js +326 -0
- package/src/design-variation-catalog.js +503 -0
- package/src/i18n/messages/en.js +32 -2
- package/src/i18n/messages/es.js +30 -2
- package/src/i18n/messages/fr.js +30 -2
- package/src/i18n/messages/pt-BR.js +32 -2
- package/src/install-animation.js +260 -0
- package/src/install-profile.js +143 -0
- package/src/install-wizard.js +474 -0
- package/src/installer.js +38 -10
- package/src/parser.js +7 -1
- package/src/recovery-context-session.js +154 -0
- package/src/runtime-store.js +97 -1
- package/src/sandbox.js +177 -0
- package/src/tool-executor.js +94 -0
- package/src/updater.js +11 -3
- package/template/.aioson/agents/analyst.md +58 -3
- package/template/.aioson/agents/architect.md +38 -0
- package/template/.aioson/agents/design-hybrid-forge.md +127 -0
- package/template/.aioson/agents/dev.md +103 -0
- package/template/.aioson/agents/deyvin.md +57 -0
- package/template/.aioson/agents/pm.md +58 -0
- package/template/.aioson/agents/product.md +28 -0
- package/template/.aioson/agents/qa.md +79 -0
- package/template/.aioson/agents/setup.md +65 -3
- package/template/.aioson/agents/sheldon.md +107 -6
- package/template/.aioson/agents/tester.md +156 -0
- package/template/.aioson/config.md +15 -0
- package/template/.aioson/context/forensics/.gitkeep +0 -0
- package/template/.aioson/context/seeds/seed-example.md +27 -0
- package/template/.aioson/context/user-profile.md +42 -0
- package/template/.aioson/locales/en/agents/setup.md +33 -1
- package/template/.aioson/locales/es/agents/setup.md +33 -1
- package/template/.aioson/locales/fr/agents/setup.md +33 -1
- package/template/.aioson/locales/pt-BR/agents/setup.md +33 -1
- package/template/.aioson/skills/design/aurora-command-ui/SKILL.md +243 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/art-direction.md +293 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/components.md +827 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/dashboards.md +250 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/design-tokens.md +585 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/motion.md +365 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/patterns.md +482 -0
- package/template/.aioson/skills/design/aurora-command-ui/references/websites.md +387 -0
- package/template/.aioson/skills/design/glassmorphism-ui/SKILL.md +222 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/art-direction.md +159 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/components.md +498 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/dashboards.md +236 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/design-tokens.md +274 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/motion.md +355 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/patterns.md +198 -0
- package/template/.aioson/skills/design/glassmorphism-ui/references/websites.md +307 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +213 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/art-direction.md +228 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/components.md +855 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/dashboards.md +334 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/design-tokens.md +342 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/motion.md +286 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/patterns.md +458 -0
- package/template/.aioson/skills/design/neo-brutalist-ui/references/websites.md +723 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +45 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +109 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +44 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +37 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/hardening-lane.md +49 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +66 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +75 -0
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +144 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/crossover-protocol.md +221 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/naming-registry.md +88 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +291 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +117 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +188 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/variation-library.md +125 -0
- package/template/AGENTS.md +23 -1
- package/template/CLAUDE.md +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
|
|
8
|
+
const CACHE_DIR = path.join(os.homedir(), '.aioson', 'temp');
|
|
9
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h
|
|
10
|
+
const META_FILE = 'sessions.json';
|
|
11
|
+
|
|
12
|
+
function nowIso() {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function generateSessionId() {
|
|
17
|
+
return crypto.randomBytes(8).toString('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ensureDir(dir) {
|
|
21
|
+
await fs.mkdir(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readMeta(cacheDir) {
|
|
25
|
+
const p = path.join(cacheDir, META_FILE);
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(await fs.readFile(p, 'utf8'));
|
|
28
|
+
} catch {
|
|
29
|
+
return { sessions: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeMeta(cacheDir, meta) {
|
|
34
|
+
const p = path.join(cacheDir, META_FILE);
|
|
35
|
+
await ensureDir(cacheDir);
|
|
36
|
+
await fs.writeFile(p, JSON.stringify(meta, null, 2), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Save a context snapshot to the RAM cache (shadow file in ~/.aioson/temp/).
|
|
41
|
+
* @param {string} content — context content to save
|
|
42
|
+
* @param {object} metadata — { goal?, agent?, projectDir? }
|
|
43
|
+
* @param {object} opts — { cacheDir?, sessionId? }
|
|
44
|
+
* @returns {{ ok: boolean, sessionId: string, path: string }}
|
|
45
|
+
*/
|
|
46
|
+
async function saveContextShadow(content, metadata = {}, opts = {}) {
|
|
47
|
+
const cacheDir = opts.cacheDir || CACHE_DIR;
|
|
48
|
+
const sessionId = opts.sessionId || generateSessionId();
|
|
49
|
+
const sessionDir = path.join(cacheDir, sessionId);
|
|
50
|
+
|
|
51
|
+
await ensureDir(sessionDir);
|
|
52
|
+
|
|
53
|
+
const contentPath = path.join(sessionDir, 'context.md');
|
|
54
|
+
await fs.writeFile(contentPath, content, 'utf8');
|
|
55
|
+
|
|
56
|
+
const entry = {
|
|
57
|
+
sessionId,
|
|
58
|
+
path: contentPath,
|
|
59
|
+
createdAt: nowIso(),
|
|
60
|
+
metadata: {
|
|
61
|
+
goal: metadata.goal || '',
|
|
62
|
+
agent: metadata.agent || '',
|
|
63
|
+
projectDir: metadata.projectDir || ''
|
|
64
|
+
},
|
|
65
|
+
size: content.length
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const meta = await readMeta(cacheDir);
|
|
69
|
+
meta.sessions[sessionId] = entry;
|
|
70
|
+
await writeMeta(cacheDir, meta);
|
|
71
|
+
|
|
72
|
+
return { ok: true, sessionId, path: contentPath };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List all cached sessions, newest first.
|
|
77
|
+
* @param {object} opts — { cacheDir? }
|
|
78
|
+
* @returns {Array<session_entry>}
|
|
79
|
+
*/
|
|
80
|
+
async function listSessions(opts = {}) {
|
|
81
|
+
const cacheDir = opts.cacheDir || CACHE_DIR;
|
|
82
|
+
const meta = await readMeta(cacheDir);
|
|
83
|
+
const sessions = Object.values(meta.sessions || {});
|
|
84
|
+
sessions.sort((a, b) => {
|
|
85
|
+
return new Date(b.createdAt) - new Date(a.createdAt);
|
|
86
|
+
});
|
|
87
|
+
return sessions;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Restore a cached context by sessionId.
|
|
92
|
+
* @param {string} sessionId
|
|
93
|
+
* @param {object} opts — { cacheDir?, query? }
|
|
94
|
+
* @returns {{ ok: boolean, content: string, metadata: object }|null}
|
|
95
|
+
*/
|
|
96
|
+
async function restoreContext(sessionId, opts = {}) {
|
|
97
|
+
const cacheDir = opts.cacheDir || CACHE_DIR;
|
|
98
|
+
const meta = await readMeta(cacheDir);
|
|
99
|
+
const entry = meta.sessions[sessionId];
|
|
100
|
+
|
|
101
|
+
if (!entry) {
|
|
102
|
+
return { ok: false, error: 'session_not_found' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let content;
|
|
106
|
+
try {
|
|
107
|
+
content = await fs.readFile(entry.path, 'utf8');
|
|
108
|
+
} catch {
|
|
109
|
+
return { ok: false, error: 'file_not_found', sessionId };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If query is provided, extract relevant excerpt
|
|
113
|
+
if (opts.query) {
|
|
114
|
+
const lines = content.split('\n');
|
|
115
|
+
const qLower = opts.query.toLowerCase();
|
|
116
|
+
const relevant = lines.filter(l => l.toLowerCase().includes(qLower));
|
|
117
|
+
if (relevant.length > 0) {
|
|
118
|
+
content = relevant.join('\n');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { ok: true, sessionId, content, metadata: entry.metadata };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Remove sessions older than maxAge ms.
|
|
127
|
+
* @param {object} opts — { cacheDir?, maxAge? }
|
|
128
|
+
* @returns {{ removed: number }}
|
|
129
|
+
*/
|
|
130
|
+
async function cleanup(opts = {}) {
|
|
131
|
+
const cacheDir = opts.cacheDir || CACHE_DIR;
|
|
132
|
+
const maxAge = opts.maxAge !== undefined ? opts.maxAge : MAX_AGE_MS;
|
|
133
|
+
const cutoff = Date.now() - maxAge + 1;
|
|
134
|
+
|
|
135
|
+
const meta = await readMeta(cacheDir);
|
|
136
|
+
const sessions = meta.sessions || {};
|
|
137
|
+
let removed = 0;
|
|
138
|
+
|
|
139
|
+
for (const [id, entry] of Object.entries(sessions)) {
|
|
140
|
+
const createdMs = new Date(entry.createdAt).getTime();
|
|
141
|
+
if (createdMs < cutoff) {
|
|
142
|
+
// Remove session dir
|
|
143
|
+
try {
|
|
144
|
+
await fs.rm(path.join(cacheDir, id), { recursive: true, force: true });
|
|
145
|
+
} catch {
|
|
146
|
+
// best-effort
|
|
147
|
+
}
|
|
148
|
+
delete sessions[id];
|
|
149
|
+
removed++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
meta.sessions = sessions;
|
|
154
|
+
await writeMeta(cacheDir, meta);
|
|
155
|
+
|
|
156
|
+
return { removed };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { saveContextShadow, listSessions, restoreContext, cleanup };
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const Database = require('better-sqlite3');
|
|
7
|
+
|
|
8
|
+
const SEARCH_DIR = path.join(os.homedir(), '.aioson', 'search');
|
|
9
|
+
const DB_FILE = 'context-search.sqlite';
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const MAX_STALE_MS = 24 * 60 * 60 * 1000; // 24h
|
|
12
|
+
|
|
13
|
+
async function ensureDir(dir) {
|
|
14
|
+
await fs.mkdir(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function openDb(dbPath) {
|
|
18
|
+
const db = new Database(dbPath);
|
|
19
|
+
db.pragma('journal_mode = WAL');
|
|
20
|
+
db.pragma('synchronous = NORMAL');
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
23
|
+
version INTEGER NOT NULL
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(
|
|
27
|
+
rel_path,
|
|
28
|
+
title,
|
|
29
|
+
content,
|
|
30
|
+
tokenize = "unicode61 remove_diacritics 2"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS docs_meta (
|
|
34
|
+
rel_path TEXT PRIMARY KEY,
|
|
35
|
+
project_dir TEXT NOT NULL,
|
|
36
|
+
indexed_at TEXT NOT NULL,
|
|
37
|
+
file_mtime TEXT,
|
|
38
|
+
size INTEGER DEFAULT 0
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
// Insert schema version if not present
|
|
43
|
+
const ver = db.prepare('SELECT version FROM schema_version LIMIT 1').get();
|
|
44
|
+
if (!ver) {
|
|
45
|
+
db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return db;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class IndexManager {
|
|
52
|
+
constructor(searchDir) {
|
|
53
|
+
this._dir = searchDir || SEARCH_DIR;
|
|
54
|
+
this._db = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async open() {
|
|
58
|
+
if (!this._db) {
|
|
59
|
+
await ensureDir(this._dir);
|
|
60
|
+
const dbPath = path.join(this._dir, DB_FILE);
|
|
61
|
+
this._db = openDb(dbPath);
|
|
62
|
+
}
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
close() {
|
|
67
|
+
if (this._db) {
|
|
68
|
+
this._db.close();
|
|
69
|
+
this._db = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Index all markdown/text files in a directory.
|
|
75
|
+
* @param {string} dir — absolute path to index
|
|
76
|
+
* @param {object} opts — { extensions?, force? }
|
|
77
|
+
* @returns {{ indexed: number, skipped: number }}
|
|
78
|
+
*/
|
|
79
|
+
async indexDirectory(dir, opts = {}) {
|
|
80
|
+
const extensions = opts.extensions || ['.md', '.txt', '.json'];
|
|
81
|
+
const force = Boolean(opts.force);
|
|
82
|
+
let indexed = 0;
|
|
83
|
+
let skipped = 0;
|
|
84
|
+
|
|
85
|
+
const files = await this._listFiles(dir, extensions);
|
|
86
|
+
|
|
87
|
+
const insertDoc = this._db.prepare(
|
|
88
|
+
'INSERT INTO docs (rel_path, title, content) VALUES (?, ?, ?)'
|
|
89
|
+
);
|
|
90
|
+
const insertMeta = this._db.prepare(`
|
|
91
|
+
INSERT OR REPLACE INTO docs_meta (rel_path, project_dir, indexed_at, file_mtime, size)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?)
|
|
93
|
+
`);
|
|
94
|
+
const getMeta = this._db.prepare(
|
|
95
|
+
'SELECT indexed_at, file_mtime FROM docs_meta WHERE rel_path = ?'
|
|
96
|
+
);
|
|
97
|
+
const deletePrev = this._db.prepare(
|
|
98
|
+
"DELETE FROM docs WHERE rel_path = ?"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const doIndex = this._db.transaction((fileList) => {
|
|
102
|
+
for (const { relPath, absPath } of fileList) {
|
|
103
|
+
// Check staleness
|
|
104
|
+
if (!force) {
|
|
105
|
+
const meta = getMeta.get(relPath);
|
|
106
|
+
if (meta) {
|
|
107
|
+
skipped++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let content = '';
|
|
113
|
+
let size = 0;
|
|
114
|
+
let mtime = '';
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Synchronous read inside transaction
|
|
118
|
+
const raw = require('node:fs').readFileSync(absPath, 'utf8');
|
|
119
|
+
const stat = require('node:fs').statSync(absPath);
|
|
120
|
+
content = raw.slice(0, 100_000); // cap at 100KB
|
|
121
|
+
size = stat.size;
|
|
122
|
+
mtime = stat.mtimeMs.toString();
|
|
123
|
+
} catch {
|
|
124
|
+
skipped++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const title = extractTitle(relPath, content);
|
|
129
|
+
|
|
130
|
+
// Remove old entry if force
|
|
131
|
+
if (force) {
|
|
132
|
+
deletePrev.run(relPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
insertDoc.run(relPath, title, content);
|
|
136
|
+
insertMeta.run(relPath, dir, new Date().toISOString(), mtime, size);
|
|
137
|
+
indexed++;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
doIndex(files);
|
|
142
|
+
return { indexed, skipped };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Full-text search with BM25 ranking + recency reranking.
|
|
147
|
+
* @param {string} query
|
|
148
|
+
* @param {object} opts — { limit?, agent?, goal?, projectDir? }
|
|
149
|
+
* @returns {Array<{relPath, title, snippet, score}>}
|
|
150
|
+
*/
|
|
151
|
+
search(query, opts = {}) {
|
|
152
|
+
const limit = opts.limit || 10;
|
|
153
|
+
const projectDir = opts.projectDir || null;
|
|
154
|
+
|
|
155
|
+
if (!query || !query.trim()) return [];
|
|
156
|
+
|
|
157
|
+
// Sanitize FTS5 query: escape special characters
|
|
158
|
+
const safeQuery = sanitizeFtsQuery(query);
|
|
159
|
+
|
|
160
|
+
let sql = `
|
|
161
|
+
SELECT rel_path, title,
|
|
162
|
+
snippet(docs, 2, '[', ']', '...', 20) AS snippet,
|
|
163
|
+
rank AS bm25_score
|
|
164
|
+
FROM docs
|
|
165
|
+
WHERE docs MATCH ?
|
|
166
|
+
`;
|
|
167
|
+
const params = [safeQuery];
|
|
168
|
+
|
|
169
|
+
if (projectDir) {
|
|
170
|
+
sql += ` AND rel_path IN (SELECT rel_path FROM docs_meta WHERE project_dir = ?)`;
|
|
171
|
+
params.push(projectDir);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
175
|
+
params.push(limit * 2); // fetch more for reranking
|
|
176
|
+
|
|
177
|
+
let rows;
|
|
178
|
+
try {
|
|
179
|
+
rows = this._db.prepare(sql).all(...params);
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Rerank by recency
|
|
185
|
+
const metas = new Map();
|
|
186
|
+
if (rows.length > 0) {
|
|
187
|
+
const relPaths = rows.map(r => r.rel_path);
|
|
188
|
+
const placeholders = relPaths.map(() => '?').join(',');
|
|
189
|
+
const metaRows = this._db.prepare(
|
|
190
|
+
`SELECT rel_path, indexed_at, file_mtime FROM docs_meta WHERE rel_path IN (${placeholders})`
|
|
191
|
+
).all(...relPaths);
|
|
192
|
+
for (const m of metaRows) {
|
|
193
|
+
metas.set(m.rel_path, m);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const results = rows.map(row => {
|
|
199
|
+
const meta = metas.get(row.rel_path);
|
|
200
|
+
let recencyBonus = 0;
|
|
201
|
+
if (meta && meta.file_mtime) {
|
|
202
|
+
const ageMs = now - Number(meta.file_mtime);
|
|
203
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
204
|
+
recencyBonus = Math.max(0, 1 - ageDays / 30); // fade over 30 days
|
|
205
|
+
}
|
|
206
|
+
// BM25 rank is negative in FTS5 (lower = better), invert it
|
|
207
|
+
const bm25 = -(row.bm25_score || 0);
|
|
208
|
+
const score = bm25 + recencyBonus * 0.5;
|
|
209
|
+
return {
|
|
210
|
+
relPath: row.rel_path,
|
|
211
|
+
title: row.title,
|
|
212
|
+
snippet: row.snippet || '',
|
|
213
|
+
score
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
results.sort((a, b) => b.score - a.score);
|
|
218
|
+
return results.slice(0, limit);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Remove stale index entries older than maxAge ms.
|
|
223
|
+
* @param {number} maxAge — milliseconds (default 24h)
|
|
224
|
+
* @returns {{ removed: number }}
|
|
225
|
+
*/
|
|
226
|
+
invalidateStale(maxAge = MAX_STALE_MS) {
|
|
227
|
+
const cutoff = new Date(Date.now() - maxAge + 1).toISOString();
|
|
228
|
+
const stale = this._db.prepare(
|
|
229
|
+
'SELECT rel_path FROM docs_meta WHERE indexed_at < ?'
|
|
230
|
+
).all(cutoff);
|
|
231
|
+
|
|
232
|
+
if (stale.length === 0) return { removed: 0 };
|
|
233
|
+
|
|
234
|
+
const doRemove = this._db.transaction((rows) => {
|
|
235
|
+
const delDoc = this._db.prepare("DELETE FROM docs WHERE rel_path = ?");
|
|
236
|
+
const delMeta = this._db.prepare('DELETE FROM docs_meta WHERE rel_path = ?');
|
|
237
|
+
for (const row of rows) {
|
|
238
|
+
delDoc.run(row.rel_path);
|
|
239
|
+
delMeta.run(row.rel_path);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
doRemove(stale);
|
|
244
|
+
return { removed: stale.length };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Return index statistics.
|
|
249
|
+
* @returns {{ totalDocs: number, totalSize: number, dbPath: string }}
|
|
250
|
+
*/
|
|
251
|
+
stats() {
|
|
252
|
+
const { totalDocs } = this._db.prepare(
|
|
253
|
+
'SELECT COUNT(*) AS totalDocs FROM docs_meta'
|
|
254
|
+
).get();
|
|
255
|
+
const { totalSize } = this._db.prepare(
|
|
256
|
+
'SELECT COALESCE(SUM(size), 0) AS totalSize FROM docs_meta'
|
|
257
|
+
).get();
|
|
258
|
+
const dbPath = path.join(this._dir, DB_FILE);
|
|
259
|
+
return { totalDocs, totalSize, dbPath };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async _listFiles(dir, extensions) {
|
|
263
|
+
const results = [];
|
|
264
|
+
await walkDir(dir, extensions, results, dir);
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function walkDir(dir, extensions, results, baseDir) {
|
|
270
|
+
let entries;
|
|
271
|
+
try {
|
|
272
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
// Skip hidden dirs and node_modules
|
|
279
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
280
|
+
|
|
281
|
+
const absPath = path.join(dir, entry.name);
|
|
282
|
+
|
|
283
|
+
if (entry.isDirectory()) {
|
|
284
|
+
await walkDir(absPath, extensions, results, baseDir);
|
|
285
|
+
} else if (entry.isFile()) {
|
|
286
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
287
|
+
if (extensions.includes(ext)) {
|
|
288
|
+
const relPath = path.relative(baseDir, absPath);
|
|
289
|
+
results.push({ relPath, absPath });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function extractTitle(relPath, content) {
|
|
296
|
+
// Try first H1 heading
|
|
297
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
298
|
+
if (match) return match[1].trim();
|
|
299
|
+
// Fallback: filename without extension
|
|
300
|
+
return path.basename(relPath, path.extname(relPath));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function sanitizeFtsQuery(query) {
|
|
304
|
+
// Remove FTS5 special chars that could cause parse errors
|
|
305
|
+
return query
|
|
306
|
+
.replace(/["*^()]/g, ' ')
|
|
307
|
+
.trim()
|
|
308
|
+
.split(/\s+/)
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join(' ');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Convenience: open a global IndexManager, use it, close it.
|
|
315
|
+
*/
|
|
316
|
+
async function withIndex(fn, searchDir) {
|
|
317
|
+
const idx = new IndexManager(searchDir);
|
|
318
|
+
await idx.open();
|
|
319
|
+
try {
|
|
320
|
+
return await fn(idx);
|
|
321
|
+
} finally {
|
|
322
|
+
idx.close();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { IndexManager, withIndex, sanitizeFtsQuery };
|