@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.
Files changed (131) hide show
  1. package/README.md +6 -0
  2. package/docs/design-previews/aurora-command-ui-website.html +884 -0
  3. package/docs/design-previews/aurora-command-ui.html +682 -0
  4. package/docs/design-previews/bold-editorial-ui-website.html +658 -0
  5. package/docs/design-previews/bold-editorial-ui.html +717 -0
  6. package/docs/design-previews/clean-saas-ui-website.html +1202 -0
  7. package/docs/design-previews/clean-saas-ui.html +549 -0
  8. package/docs/design-previews/cognitive-core-ui-website.html +1009 -0
  9. package/docs/design-previews/cognitive-core-ui.html +463 -0
  10. package/docs/design-previews/glassmorphism-ui-website.html +572 -0
  11. package/docs/design-previews/glassmorphism-ui.html +886 -0
  12. package/docs/design-previews/index.html +699 -0
  13. package/docs/design-previews/interface-design-website.html +1187 -0
  14. package/docs/design-previews/interface-design.html +513 -0
  15. package/docs/design-previews/neo-brutalist-ui-website.html +621 -0
  16. package/docs/design-previews/neo-brutalist-ui.html +797 -0
  17. package/docs/design-previews/premium-command-center-ui-website.html +1217 -0
  18. package/docs/design-previews/premium-command-center-ui.html +552 -0
  19. package/docs/design-previews/warm-craft-ui-website.html +684 -0
  20. package/docs/design-previews/warm-craft-ui.html +739 -0
  21. package/docs/en/cli-reference.md +20 -9
  22. package/docs/pt/README.md +7 -0
  23. package/docs/pt/agent-sharding.md +132 -0
  24. package/docs/pt/agentes.md +8 -2
  25. package/docs/pt/busca-de-contexto.md +129 -0
  26. package/docs/pt/cache-de-contexto.md +156 -0
  27. package/docs/pt/comandos-cli.md +28 -0
  28. package/docs/pt/design-hybrid-forge.md +107 -0
  29. package/docs/pt/inicio-rapido.md +54 -3
  30. package/docs/pt/inteligencia-adaptativa.md +324 -0
  31. package/docs/pt/monitor-de-contexto.md +104 -0
  32. package/docs/pt/recuperacao-de-sessao.md +125 -0
  33. package/docs/pt/sandbox.md +125 -0
  34. package/docs/pt/skills.md +98 -6
  35. package/package.json +1 -1
  36. package/src/agent-loader.js +280 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/agent-loader.js +85 -0
  39. package/src/commands/context-cache.js +90 -0
  40. package/src/commands/context-monitor.js +92 -0
  41. package/src/commands/context-search.js +66 -0
  42. package/src/commands/design-hybrid-options.js +385 -0
  43. package/src/commands/health.js +214 -0
  44. package/src/commands/init.js +54 -13
  45. package/src/commands/install.js +52 -13
  46. package/src/commands/learning-evolve.js +355 -0
  47. package/src/commands/live.js +34 -0
  48. package/src/commands/recovery.js +43 -0
  49. package/src/commands/sandbox.js +37 -0
  50. package/src/commands/setup-context.js +22 -2
  51. package/src/commands/setup.js +178 -0
  52. package/src/commands/skill.js +79 -32
  53. package/src/commands/tool-registry-cmd.js +232 -0
  54. package/src/commands/update.js +7 -0
  55. package/src/constants.js +9 -0
  56. package/src/context-cache.js +159 -0
  57. package/src/context-search.js +326 -0
  58. package/src/design-variation-catalog.js +503 -0
  59. package/src/i18n/messages/en.js +32 -2
  60. package/src/i18n/messages/es.js +30 -2
  61. package/src/i18n/messages/fr.js +30 -2
  62. package/src/i18n/messages/pt-BR.js +32 -2
  63. package/src/install-animation.js +260 -0
  64. package/src/install-profile.js +143 -0
  65. package/src/install-wizard.js +474 -0
  66. package/src/installer.js +38 -10
  67. package/src/parser.js +7 -1
  68. package/src/recovery-context-session.js +154 -0
  69. package/src/runtime-store.js +97 -1
  70. package/src/sandbox.js +177 -0
  71. package/src/tool-executor.js +94 -0
  72. package/src/updater.js +11 -3
  73. package/template/.aioson/agents/analyst.md +58 -3
  74. package/template/.aioson/agents/architect.md +38 -0
  75. package/template/.aioson/agents/design-hybrid-forge.md +127 -0
  76. package/template/.aioson/agents/dev.md +103 -0
  77. package/template/.aioson/agents/deyvin.md +57 -0
  78. package/template/.aioson/agents/pm.md +58 -0
  79. package/template/.aioson/agents/product.md +28 -0
  80. package/template/.aioson/agents/qa.md +79 -0
  81. package/template/.aioson/agents/setup.md +65 -3
  82. package/template/.aioson/agents/sheldon.md +107 -6
  83. package/template/.aioson/agents/tester.md +156 -0
  84. package/template/.aioson/config.md +15 -0
  85. package/template/.aioson/context/forensics/.gitkeep +0 -0
  86. package/template/.aioson/context/seeds/seed-example.md +27 -0
  87. package/template/.aioson/context/user-profile.md +42 -0
  88. package/template/.aioson/locales/en/agents/setup.md +33 -1
  89. package/template/.aioson/locales/es/agents/setup.md +33 -1
  90. package/template/.aioson/locales/fr/agents/setup.md +33 -1
  91. package/template/.aioson/locales/pt-BR/agents/setup.md +33 -1
  92. package/template/.aioson/skills/design/aurora-command-ui/SKILL.md +243 -0
  93. package/template/.aioson/skills/design/aurora-command-ui/references/art-direction.md +293 -0
  94. package/template/.aioson/skills/design/aurora-command-ui/references/components.md +827 -0
  95. package/template/.aioson/skills/design/aurora-command-ui/references/dashboards.md +250 -0
  96. package/template/.aioson/skills/design/aurora-command-ui/references/design-tokens.md +585 -0
  97. package/template/.aioson/skills/design/aurora-command-ui/references/motion.md +365 -0
  98. package/template/.aioson/skills/design/aurora-command-ui/references/patterns.md +482 -0
  99. package/template/.aioson/skills/design/aurora-command-ui/references/websites.md +387 -0
  100. package/template/.aioson/skills/design/glassmorphism-ui/SKILL.md +222 -0
  101. package/template/.aioson/skills/design/glassmorphism-ui/references/art-direction.md +159 -0
  102. package/template/.aioson/skills/design/glassmorphism-ui/references/components.md +498 -0
  103. package/template/.aioson/skills/design/glassmorphism-ui/references/dashboards.md +236 -0
  104. package/template/.aioson/skills/design/glassmorphism-ui/references/design-tokens.md +274 -0
  105. package/template/.aioson/skills/design/glassmorphism-ui/references/motion.md +355 -0
  106. package/template/.aioson/skills/design/glassmorphism-ui/references/patterns.md +198 -0
  107. package/template/.aioson/skills/design/glassmorphism-ui/references/websites.md +307 -0
  108. package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +213 -0
  109. package/template/.aioson/skills/design/neo-brutalist-ui/references/art-direction.md +228 -0
  110. package/template/.aioson/skills/design/neo-brutalist-ui/references/components.md +855 -0
  111. package/template/.aioson/skills/design/neo-brutalist-ui/references/dashboards.md +334 -0
  112. package/template/.aioson/skills/design/neo-brutalist-ui/references/design-tokens.md +342 -0
  113. package/template/.aioson/skills/design/neo-brutalist-ui/references/motion.md +286 -0
  114. package/template/.aioson/skills/design/neo-brutalist-ui/references/patterns.md +458 -0
  115. package/template/.aioson/skills/design/neo-brutalist-ui/references/websites.md +723 -0
  116. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +45 -0
  117. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +109 -0
  118. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +44 -0
  119. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +37 -0
  120. package/template/.aioson/skills/process/aioson-spec-driven/references/hardening-lane.md +49 -0
  121. package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +66 -0
  122. package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +75 -0
  123. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +144 -0
  124. package/template/.aioson/skills/process/design-hybrid-forge/references/crossover-protocol.md +221 -0
  125. package/template/.aioson/skills/process/design-hybrid-forge/references/naming-registry.md +88 -0
  126. package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +291 -0
  127. package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +117 -0
  128. package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +188 -0
  129. package/template/.aioson/skills/process/design-hybrid-forge/references/variation-library.md +125 -0
  130. package/template/AGENTS.md +23 -1
  131. 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 };