@psiclawops/hypermem 0.5.0 → 0.5.2
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/ARCHITECTURE.md +12 -3
- package/README.md +30 -6
- package/bin/hypermem-status.mjs +166 -0
- package/dist/background-indexer.d.ts +132 -0
- package/dist/background-indexer.d.ts.map +1 -0
- package/dist/background-indexer.js +1044 -0
- package/dist/cache.d.ts +110 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +495 -0
- package/dist/compaction-fence.d.ts +89 -0
- package/dist/compaction-fence.d.ts.map +1 -0
- package/dist/compaction-fence.js +153 -0
- package/dist/compositor.d.ts +226 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +2558 -0
- package/dist/content-type-classifier.d.ts +41 -0
- package/dist/content-type-classifier.d.ts.map +1 -0
- package/dist/content-type-classifier.js +181 -0
- package/dist/cross-agent.d.ts +62 -0
- package/dist/cross-agent.d.ts.map +1 -0
- package/dist/cross-agent.js +259 -0
- package/dist/db.d.ts +131 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +402 -0
- package/dist/desired-state-store.d.ts +100 -0
- package/dist/desired-state-store.d.ts.map +1 -0
- package/dist/desired-state-store.js +222 -0
- package/dist/doc-chunk-store.d.ts +140 -0
- package/dist/doc-chunk-store.d.ts.map +1 -0
- package/dist/doc-chunk-store.js +391 -0
- package/dist/doc-chunker.d.ts +99 -0
- package/dist/doc-chunker.d.ts.map +1 -0
- package/dist/doc-chunker.js +324 -0
- package/dist/dreaming-promoter.d.ts +86 -0
- package/dist/dreaming-promoter.d.ts.map +1 -0
- package/dist/dreaming-promoter.js +381 -0
- package/dist/episode-store.d.ts +49 -0
- package/dist/episode-store.d.ts.map +1 -0
- package/dist/episode-store.js +135 -0
- package/dist/fact-store.d.ts +75 -0
- package/dist/fact-store.d.ts.map +1 -0
- package/dist/fact-store.js +236 -0
- package/dist/fleet-store.d.ts +144 -0
- package/dist/fleet-store.d.ts.map +1 -0
- package/dist/fleet-store.js +276 -0
- package/dist/fos-mod.d.ts +178 -0
- package/dist/fos-mod.d.ts.map +1 -0
- package/dist/fos-mod.js +416 -0
- package/dist/hybrid-retrieval.d.ts +64 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -0
- package/dist/hybrid-retrieval.js +344 -0
- package/dist/image-eviction.d.ts +49 -0
- package/dist/image-eviction.d.ts.map +1 -0
- package/dist/image-eviction.js +251 -0
- package/dist/index.d.ts +650 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1072 -0
- package/dist/keystone-scorer.d.ts +51 -0
- package/dist/keystone-scorer.d.ts.map +1 -0
- package/dist/keystone-scorer.js +52 -0
- package/dist/knowledge-graph.d.ts +110 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +305 -0
- package/dist/knowledge-lint.d.ts +29 -0
- package/dist/knowledge-lint.d.ts.map +1 -0
- package/dist/knowledge-lint.js +116 -0
- package/dist/knowledge-store.d.ts +72 -0
- package/dist/knowledge-store.d.ts.map +1 -0
- package/dist/knowledge-store.js +247 -0
- package/dist/library-schema.d.ts +22 -0
- package/dist/library-schema.d.ts.map +1 -0
- package/dist/library-schema.js +1038 -0
- package/dist/message-store.d.ts +89 -0
- package/dist/message-store.d.ts.map +1 -0
- package/dist/message-store.js +323 -0
- package/dist/metrics-dashboard.d.ts +114 -0
- package/dist/metrics-dashboard.d.ts.map +1 -0
- package/dist/metrics-dashboard.js +260 -0
- package/dist/obsidian-exporter.d.ts +57 -0
- package/dist/obsidian-exporter.d.ts.map +1 -0
- package/dist/obsidian-exporter.js +274 -0
- package/dist/obsidian-watcher.d.ts +147 -0
- package/dist/obsidian-watcher.d.ts.map +1 -0
- package/dist/obsidian-watcher.js +403 -0
- package/dist/open-domain.d.ts +46 -0
- package/dist/open-domain.d.ts.map +1 -0
- package/dist/open-domain.js +125 -0
- package/dist/preference-store.d.ts +54 -0
- package/dist/preference-store.d.ts.map +1 -0
- package/dist/preference-store.js +109 -0
- package/dist/preservation-gate.d.ts +82 -0
- package/dist/preservation-gate.d.ts.map +1 -0
- package/dist/preservation-gate.js +150 -0
- package/dist/proactive-pass.d.ts +63 -0
- package/dist/proactive-pass.d.ts.map +1 -0
- package/dist/proactive-pass.js +239 -0
- package/dist/profiles.d.ts +44 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +227 -0
- package/dist/provider-translator.d.ts +50 -0
- package/dist/provider-translator.d.ts.map +1 -0
- package/dist/provider-translator.js +403 -0
- package/dist/rate-limiter.d.ts +76 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +179 -0
- package/dist/repair-tool-pairs.d.ts +38 -0
- package/dist/repair-tool-pairs.d.ts.map +1 -0
- package/dist/repair-tool-pairs.js +138 -0
- package/dist/retrieval-policy.d.ts +51 -0
- package/dist/retrieval-policy.d.ts.map +1 -0
- package/dist/retrieval-policy.js +77 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +229 -0
- package/dist/secret-scanner.d.ts +51 -0
- package/dist/secret-scanner.d.ts.map +1 -0
- package/dist/secret-scanner.js +248 -0
- package/dist/seed.d.ts +108 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +177 -0
- package/dist/session-flusher.d.ts +53 -0
- package/dist/session-flusher.d.ts.map +1 -0
- package/dist/session-flusher.js +69 -0
- package/dist/session-topic-map.d.ts +41 -0
- package/dist/session-topic-map.d.ts.map +1 -0
- package/dist/session-topic-map.js +77 -0
- package/dist/spawn-context.d.ts +54 -0
- package/dist/spawn-context.d.ts.map +1 -0
- package/dist/spawn-context.js +159 -0
- package/dist/system-store.d.ts +73 -0
- package/dist/system-store.d.ts.map +1 -0
- package/dist/system-store.js +182 -0
- package/dist/temporal-store.d.ts +80 -0
- package/dist/temporal-store.d.ts.map +1 -0
- package/dist/temporal-store.js +149 -0
- package/dist/topic-detector.d.ts +35 -0
- package/dist/topic-detector.d.ts.map +1 -0
- package/dist/topic-detector.js +249 -0
- package/dist/topic-store.d.ts +45 -0
- package/dist/topic-store.d.ts.map +1 -0
- package/dist/topic-store.js +136 -0
- package/dist/topic-synthesizer.d.ts +51 -0
- package/dist/topic-synthesizer.d.ts.map +1 -0
- package/dist/topic-synthesizer.js +315 -0
- package/dist/trigger-registry.d.ts +63 -0
- package/dist/trigger-registry.d.ts.map +1 -0
- package/dist/trigger-registry.js +163 -0
- package/dist/types.d.ts +537 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/vector-store.d.ts +170 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +677 -0
- package/dist/version.d.ts +34 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +34 -0
- package/dist/wiki-page-emitter.d.ts +65 -0
- package/dist/wiki-page-emitter.d.ts.map +1 -0
- package/dist/wiki-page-emitter.js +258 -0
- package/dist/work-store.d.ts +112 -0
- package/dist/work-store.d.ts.map +1 -0
- package/dist/work-store.js +273 -0
- package/package.json +4 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Metrics Dashboard
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified surface for observing system health:
|
|
5
|
+
* - Memory counts (facts, pages, episodes, vectors)
|
|
6
|
+
* - Composition performance (avg assembly time, budget utilization)
|
|
7
|
+
* - Ingestion stats (indexer throughput, promotion rate)
|
|
8
|
+
* - Embedding stats (cache hit rate, Ollama availability)
|
|
9
|
+
*
|
|
10
|
+
* All queries are read-only and safe to call on hot DBs.
|
|
11
|
+
*/
|
|
12
|
+
import { HYPERMEM_COMPAT_VERSION } from './version.js';
|
|
13
|
+
function safeQuery(db, sql, params = []) {
|
|
14
|
+
try {
|
|
15
|
+
const stmt = db.prepare(sql);
|
|
16
|
+
return stmt.get(...params);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function safeQueryAll(db, sql, params = []) {
|
|
23
|
+
try {
|
|
24
|
+
const stmt = db.prepare(sql);
|
|
25
|
+
return stmt.all(...params);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function buildAgentFilter(agentIds) {
|
|
32
|
+
if (!agentIds || agentIds.length === 0)
|
|
33
|
+
return { clause: '', params: [] };
|
|
34
|
+
const placeholders = agentIds.map(() => '?').join(', ');
|
|
35
|
+
return { clause: `AND agent_id IN (${placeholders})`, params: agentIds };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Collect fact metrics from the library DB.
|
|
39
|
+
*/
|
|
40
|
+
function collectFactMetrics(libraryDb, opts) {
|
|
41
|
+
const { clause, params } = buildAgentFilter(opts.agentIds);
|
|
42
|
+
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
43
|
+
const total = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM facts WHERE 1=1 ${clause}`, params);
|
|
44
|
+
const recent = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM facts WHERE created_at > ? ${clause}`, [cutoff, ...params]);
|
|
45
|
+
const byAgentRows = (opts.includeBreakdowns !== false)
|
|
46
|
+
? safeQueryAll(libraryDb, `SELECT agent_id, COUNT(*) AS count FROM facts WHERE 1=1 ${clause} GROUP BY agent_id`, params)
|
|
47
|
+
: [];
|
|
48
|
+
return {
|
|
49
|
+
totalFacts: total?.count ?? 0,
|
|
50
|
+
byAgent: Object.fromEntries(byAgentRows.map(r => [r.agent_id, r.count])),
|
|
51
|
+
recentFacts: recent?.count ?? 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Collect wiki page metrics from the library DB (knowledge table, topic-synthesis domain).
|
|
56
|
+
*/
|
|
57
|
+
function collectWikiMetrics(libraryDb, opts) {
|
|
58
|
+
const { clause, params } = buildAgentFilter(opts.agentIds);
|
|
59
|
+
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
60
|
+
const total = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM knowledge WHERE domain = 'topic-synthesis' AND superseded_by IS NULL ${clause}`, params);
|
|
61
|
+
const recent = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM knowledge WHERE domain = 'topic-synthesis' AND superseded_by IS NULL AND created_at > ? ${clause}`, [cutoff, ...params]);
|
|
62
|
+
const oldest = safeQuery(libraryDb, `SELECT created_at FROM knowledge WHERE domain = 'topic-synthesis' AND superseded_by IS NULL ${clause} ORDER BY created_at ASC LIMIT 1`, params);
|
|
63
|
+
const byAgentRows = (opts.includeBreakdowns !== false)
|
|
64
|
+
? safeQueryAll(libraryDb, `SELECT agent_id, COUNT(*) AS count FROM knowledge WHERE domain = 'topic-synthesis' AND superseded_by IS NULL ${clause} GROUP BY agent_id`, params)
|
|
65
|
+
: [];
|
|
66
|
+
let oldestPageAgeHours = null;
|
|
67
|
+
if (oldest?.created_at) {
|
|
68
|
+
const diffMs = Date.now() - new Date(oldest.created_at).getTime();
|
|
69
|
+
oldestPageAgeHours = Math.round(diffMs / (1000 * 60 * 60));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
totalPages: total?.count ?? 0,
|
|
73
|
+
byAgent: Object.fromEntries(byAgentRows.map(r => [r.agent_id, r.count])),
|
|
74
|
+
recentPages: recent?.count ?? 0,
|
|
75
|
+
oldestPageAgeHours,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Collect episode metrics from the library DB.
|
|
80
|
+
*/
|
|
81
|
+
function collectEpisodeMetrics(libraryDb, opts) {
|
|
82
|
+
const { clause, params } = buildAgentFilter(opts.agentIds);
|
|
83
|
+
const total = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM episodes WHERE 1=1 ${clause}`, params);
|
|
84
|
+
const avgSig = safeQuery(libraryDb, `SELECT AVG(significance) AS avg FROM episodes WHERE significance IS NOT NULL ${clause}`, params);
|
|
85
|
+
const byAgentRows = (opts.includeBreakdowns !== false)
|
|
86
|
+
? safeQueryAll(libraryDb, `SELECT agent_id, COUNT(*) AS count FROM episodes WHERE 1=1 ${clause} GROUP BY agent_id`, params)
|
|
87
|
+
: [];
|
|
88
|
+
return {
|
|
89
|
+
totalEpisodes: total?.count ?? 0,
|
|
90
|
+
byAgent: Object.fromEntries(byAgentRows.map(r => [r.agent_id, r.count])),
|
|
91
|
+
avgSignificance: avgSig?.avg ?? null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Collect vector index metrics from the library DB.
|
|
96
|
+
*/
|
|
97
|
+
function collectVectorMetrics(libraryDb) {
|
|
98
|
+
const total = safeQuery(libraryDb, 'SELECT COUNT(*) as cnt FROM vec_index_map');
|
|
99
|
+
const byTableRows = safeQueryAll(libraryDb, 'SELECT source_table, COUNT(*) as cnt FROM vec_index_map GROUP BY source_table');
|
|
100
|
+
return {
|
|
101
|
+
totalVectors: total?.cnt ?? 0,
|
|
102
|
+
byTable: Object.fromEntries(byTableRows.map(r => [r.source_table, r.cnt])),
|
|
103
|
+
cacheHitRate: null, // process-lifetime stat, not persisted
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Collect composition performance metrics from the output_metrics table (main DB).
|
|
108
|
+
*/
|
|
109
|
+
function collectCompositionMetrics(mainDb, opts) {
|
|
110
|
+
const { clause, params } = buildAgentFilter(opts.agentIds);
|
|
111
|
+
const agg = safeQuery(mainDb, `SELECT
|
|
112
|
+
AVG(latency_ms) AS avg_latency,
|
|
113
|
+
AVG(output_tokens) AS avg_output,
|
|
114
|
+
AVG(input_tokens) AS avg_input,
|
|
115
|
+
AVG(cache_read_tokens) AS avg_cache,
|
|
116
|
+
COUNT(*) AS total
|
|
117
|
+
FROM output_metrics WHERE 1=1 ${clause.replace(/agent_id/g, 'agent_id')}`, params);
|
|
118
|
+
// p95: sort latency_ms and pick the 95th percentile row
|
|
119
|
+
let p95 = null;
|
|
120
|
+
if ((agg?.total ?? 0) > 0) {
|
|
121
|
+
const p95Row = safeQuery(mainDb, `SELECT latency_ms FROM output_metrics WHERE latency_ms IS NOT NULL ${clause}
|
|
122
|
+
ORDER BY latency_ms ASC
|
|
123
|
+
LIMIT 1 OFFSET MAX(0, CAST(COUNT(*) * 0.95 AS INT) - 1)`, params);
|
|
124
|
+
// Fallback: approximate p95 with a subquery
|
|
125
|
+
if (!p95Row) {
|
|
126
|
+
const p95Approx = safeQuery(mainDb, `SELECT latency_ms FROM output_metrics WHERE latency_ms IS NOT NULL ${clause}
|
|
127
|
+
ORDER BY latency_ms DESC
|
|
128
|
+
LIMIT 1 OFFSET (SELECT MAX(0, CAST(COUNT(*) * 0.05 AS INT)) FROM output_metrics WHERE latency_ms IS NOT NULL ${clause})`, [...params, ...params]);
|
|
129
|
+
p95 = p95Approx?.latency_ms ?? null;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
p95 = p95Row.latency_ms;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
avgAssemblyMs: agg?.avg_latency ? Math.round(agg.avg_latency) : null,
|
|
137
|
+
p95AssemblyMs: p95 ? Math.round(p95) : null,
|
|
138
|
+
avgOutputTokens: agg?.avg_output ? Math.round(agg.avg_output) : null,
|
|
139
|
+
avgInputTokens: agg?.avg_input ? Math.round(agg.avg_input) : null,
|
|
140
|
+
totalTurns: agg?.total ?? 0,
|
|
141
|
+
avgCacheReadTokens: agg?.avg_cache ? Math.round(agg.avg_cache) : null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Collect ingestion pipeline metrics from the library DB.
|
|
146
|
+
*/
|
|
147
|
+
function collectIngestionMetrics(libraryDb, opts) {
|
|
148
|
+
const { clause, params } = buildAgentFilter(opts.agentIds);
|
|
149
|
+
const facts = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM facts WHERE 1=1 ${clause}`, params);
|
|
150
|
+
const episodes = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM episodes WHERE 1=1 ${clause}`, params);
|
|
151
|
+
const knowledge = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM knowledge WHERE superseded_by IS NULL ${clause}`, params);
|
|
152
|
+
const totalFacts = facts?.count ?? 0;
|
|
153
|
+
const totalEpisodes = episodes?.count ?? 0;
|
|
154
|
+
// Noise rejection: approximate from fact vs message ratio if messages available
|
|
155
|
+
const messages = safeQuery(libraryDb, `SELECT COUNT(*) AS count FROM facts WHERE 1=1 ${clause}`, // placeholder — we'd need message count from main
|
|
156
|
+
params);
|
|
157
|
+
return {
|
|
158
|
+
totalMessagesIndexed: 0, // requires main DB join — surfaced separately
|
|
159
|
+
totalFactsExtracted: totalFacts,
|
|
160
|
+
noiseRejectionRate: null, // requires cross-DB join
|
|
161
|
+
totalEpisodesCreated: totalEpisodes,
|
|
162
|
+
totalKnowledgePromoted: knowledge?.count ?? 0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Collect system health from both DBs.
|
|
167
|
+
*/
|
|
168
|
+
function collectHealth(mainDb, libraryDb) {
|
|
169
|
+
let mainSchemaVersion = null;
|
|
170
|
+
let librarySchemaVersion = null;
|
|
171
|
+
let mainDbOk = false;
|
|
172
|
+
let libraryDbOk = false;
|
|
173
|
+
try {
|
|
174
|
+
const row = safeQuery(mainDb, 'SELECT schema_version FROM _meta LIMIT 1');
|
|
175
|
+
mainSchemaVersion = row?.schema_version ?? null;
|
|
176
|
+
mainDbOk = true;
|
|
177
|
+
}
|
|
178
|
+
catch { /* empty */ }
|
|
179
|
+
try {
|
|
180
|
+
const row = safeQuery(libraryDb, 'SELECT schema_version FROM _library_meta LIMIT 1');
|
|
181
|
+
librarySchemaVersion = row?.schema_version ?? null;
|
|
182
|
+
libraryDbOk = true;
|
|
183
|
+
}
|
|
184
|
+
catch { /* empty */ }
|
|
185
|
+
return {
|
|
186
|
+
mainDbOk,
|
|
187
|
+
libraryDbOk,
|
|
188
|
+
mainSchemaVersion,
|
|
189
|
+
librarySchemaVersion,
|
|
190
|
+
packageVersion: HYPERMEM_COMPAT_VERSION,
|
|
191
|
+
cacheOk: null, // caller must inject cache status
|
|
192
|
+
snapshotAt: new Date().toISOString(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Collect all metrics in a single pass.
|
|
197
|
+
* Safe to call on live DBs — all queries are read-only.
|
|
198
|
+
*/
|
|
199
|
+
export async function collectMetrics(mainDb, libraryDb, opts = {}) {
|
|
200
|
+
return {
|
|
201
|
+
facts: collectFactMetrics(libraryDb, opts),
|
|
202
|
+
wiki: collectWikiMetrics(libraryDb, opts),
|
|
203
|
+
episodes: collectEpisodeMetrics(libraryDb, opts),
|
|
204
|
+
vectors: collectVectorMetrics(libraryDb),
|
|
205
|
+
composition: collectCompositionMetrics(mainDb, opts),
|
|
206
|
+
ingestion: collectIngestionMetrics(libraryDb, opts),
|
|
207
|
+
health: collectHealth(mainDb, libraryDb),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Format metrics as a human-readable summary string.
|
|
212
|
+
* Suitable for logging or status replies.
|
|
213
|
+
*/
|
|
214
|
+
export function formatMetricsSummary(m) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
lines.push(`hypermem ${m.health.packageVersion} — metrics snapshot ${m.health.snapshotAt}`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('## Memory');
|
|
219
|
+
lines.push(` facts: ${m.facts.totalFacts.toLocaleString()} total, ${m.facts.recentFacts} added last 24h`);
|
|
220
|
+
lines.push(` wiki: ${m.wiki.totalPages} pages, ${m.wiki.recentPages} synthesized last 24h${m.wiki.oldestPageAgeHours !== null ? `, oldest ${m.wiki.oldestPageAgeHours}h` : ''}`);
|
|
221
|
+
lines.push(` episodes: ${m.episodes.totalEpisodes.toLocaleString()}${m.episodes.avgSignificance !== null ? `, avg significance ${m.episodes.avgSignificance.toFixed(2)}` : ''}`);
|
|
222
|
+
lines.push(` vectors: ${m.vectors.totalVectors.toLocaleString()} indexed`);
|
|
223
|
+
if (Object.keys(m.vectors.byTable).length > 0) {
|
|
224
|
+
for (const [table, count] of Object.entries(m.vectors.byTable)) {
|
|
225
|
+
lines.push(` ${table}: ${count.toLocaleString()}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push('## Composition');
|
|
230
|
+
if (m.composition.totalTurns > 0) {
|
|
231
|
+
lines.push(` turns: ${m.composition.totalTurns.toLocaleString()}`);
|
|
232
|
+
if (m.composition.avgAssemblyMs !== null)
|
|
233
|
+
lines.push(` avg time: ${m.composition.avgAssemblyMs}ms`);
|
|
234
|
+
if (m.composition.p95AssemblyMs !== null)
|
|
235
|
+
lines.push(` p95 time: ${m.composition.p95AssemblyMs}ms`);
|
|
236
|
+
if (m.composition.avgOutputTokens !== null)
|
|
237
|
+
lines.push(` avg out: ${m.composition.avgOutputTokens} tokens`);
|
|
238
|
+
if (m.composition.avgInputTokens !== null)
|
|
239
|
+
lines.push(` avg in: ${m.composition.avgInputTokens} tokens`);
|
|
240
|
+
if (m.composition.avgCacheReadTokens !== null)
|
|
241
|
+
lines.push(` cache rd: ${m.composition.avgCacheReadTokens} tokens/turn`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
lines.push(' no turn data yet');
|
|
245
|
+
}
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push('## Ingestion');
|
|
248
|
+
lines.push(` facts extracted: ${m.ingestion.totalFactsExtracted.toLocaleString()}`);
|
|
249
|
+
lines.push(` episodes created: ${m.ingestion.totalEpisodesCreated.toLocaleString()}`);
|
|
250
|
+
lines.push(` knowledge promoted: ${m.ingestion.totalKnowledgePromoted.toLocaleString()}`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push('## Health');
|
|
253
|
+
lines.push(` main db: ${m.health.mainDbOk ? '✅' : '❌'}${m.health.mainSchemaVersion !== null ? ` (schema v${m.health.mainSchemaVersion})` : ''}`);
|
|
254
|
+
lines.push(` library db: ${m.health.libraryDbOk ? '✅' : '❌'}${m.health.librarySchemaVersion !== null ? ` (schema v${m.health.librarySchemaVersion})` : ''}`);
|
|
255
|
+
if (m.health.cacheOk !== null) {
|
|
256
|
+
lines.push(` cache: ${m.health.cacheOk ? '✅' : '❌'}`);
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=metrics-dashboard.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obsidian-exporter.ts
|
|
3
|
+
*
|
|
4
|
+
* Exports hypermem facts and wiki pages to an Obsidian vault.
|
|
5
|
+
* Complements obsidian-watcher.ts (import direction).
|
|
6
|
+
*
|
|
7
|
+
* Output format:
|
|
8
|
+
* - Wiki pages → one .md file per topic, with frontmatter + [[wikilinks]]
|
|
9
|
+
* - Facts → grouped by domain into .md files, frontmatter-tagged
|
|
10
|
+
* - Index note → _hypermem-index.md with TOC and stats
|
|
11
|
+
*
|
|
12
|
+
* Safe to run repeatedly — existing files are overwritten only if content changed.
|
|
13
|
+
*/
|
|
14
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
15
|
+
export interface ObsidianExportConfig {
|
|
16
|
+
/** Absolute path to target Obsidian vault directory */
|
|
17
|
+
vaultPath: string;
|
|
18
|
+
/** Sub-folder inside vault to write hypermem exports. Default: 'hypermem' */
|
|
19
|
+
vaultFolder?: string;
|
|
20
|
+
/** Agent ID to scope export to. If omitted, exports all agents */
|
|
21
|
+
agentId?: string;
|
|
22
|
+
/** Include raw facts (not just wiki pages). Default: true */
|
|
23
|
+
includeFacts?: boolean;
|
|
24
|
+
/** Include synthesized wiki pages. Default: true */
|
|
25
|
+
includeWikiPages?: boolean;
|
|
26
|
+
/** Include an index note. Default: true */
|
|
27
|
+
includeIndex?: boolean;
|
|
28
|
+
/** Tag prefix added to all exported notes. Default: 'hypermem' */
|
|
29
|
+
tagPrefix?: string;
|
|
30
|
+
/** Skip files whose content hasn't changed. Default: true */
|
|
31
|
+
skipUnchanged?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ObsidianExportResult {
|
|
34
|
+
written: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
errors: string[];
|
|
37
|
+
outputDir: string;
|
|
38
|
+
files: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Export hypermem memories to an Obsidian vault.
|
|
42
|
+
*
|
|
43
|
+
* @param mainDb - hypermem main DB (facts, episodes)
|
|
44
|
+
* @param libraryDb - hypermem library DB (knowledge, wiki pages)
|
|
45
|
+
* @param config - export config
|
|
46
|
+
* @returns ObsidianExportResult with counts and file list
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const result = await exportToVault(mainDb, libraryDb, {
|
|
50
|
+
* vaultPath: '/Users/me/obsidian/my-vault',
|
|
51
|
+
* agentId: 'main',
|
|
52
|
+
* tagPrefix: 'hypermem',
|
|
53
|
+
* });
|
|
54
|
+
* console.log(`Wrote ${result.written} files to ${result.outputDir}`);
|
|
55
|
+
*/
|
|
56
|
+
export declare function exportToVault(mainDb: DatabaseSync, libraryDb: DatabaseSync, config: ObsidianExportConfig): ObsidianExportResult;
|
|
57
|
+
//# sourceMappingURL=obsidian-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"obsidian-exporter.d.ts","sourceRoot":"","sources":["../src/obsidian-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oDAAoD;IACpD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,2CAA2C;IAC3C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AA8SD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE,oBAAoB,GAC3B,oBAAoB,CAiCtB"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* obsidian-exporter.ts
|
|
3
|
+
*
|
|
4
|
+
* Exports hypermem facts and wiki pages to an Obsidian vault.
|
|
5
|
+
* Complements obsidian-watcher.ts (import direction).
|
|
6
|
+
*
|
|
7
|
+
* Output format:
|
|
8
|
+
* - Wiki pages → one .md file per topic, with frontmatter + [[wikilinks]]
|
|
9
|
+
* - Facts → grouped by domain into .md files, frontmatter-tagged
|
|
10
|
+
* - Index note → _hypermem-index.md with TOC and stats
|
|
11
|
+
*
|
|
12
|
+
* Safe to run repeatedly — existing files are overwritten only if content changed.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
18
|
+
function hashContent(content) {
|
|
19
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
20
|
+
}
|
|
21
|
+
function safeFilename(name) {
|
|
22
|
+
return name
|
|
23
|
+
.replace(/[\\/:*?"<>|]/g, '-')
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim()
|
|
26
|
+
.slice(0, 200);
|
|
27
|
+
}
|
|
28
|
+
function formatTimestamp(unixMs) {
|
|
29
|
+
return new Date(unixMs).toISOString().split('T')[0];
|
|
30
|
+
}
|
|
31
|
+
function yamlString(value) {
|
|
32
|
+
if (/[:#\[\]{},&*?|<>=!%@`]/.test(value) || value.includes('\n')) {
|
|
33
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function buildFrontmatter(fields) {
|
|
38
|
+
const lines = ['---'];
|
|
39
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
40
|
+
if (value === null || value === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
if (value.length === 0)
|
|
44
|
+
continue;
|
|
45
|
+
lines.push(`${key}:`);
|
|
46
|
+
for (const item of value)
|
|
47
|
+
lines.push(` - ${yamlString(String(item))}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push(`${key}: ${yamlString(String(value))}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push('---', '');
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
function writeIfChanged(filePath, content, skipUnchanged, result) {
|
|
57
|
+
if (skipUnchanged && existsSync(filePath)) {
|
|
58
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
59
|
+
if (existing === content) {
|
|
60
|
+
result.skipped++;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
writeFileSync(filePath, content, 'utf8');
|
|
65
|
+
result.written++;
|
|
66
|
+
result.files.push(filePath);
|
|
67
|
+
}
|
|
68
|
+
// ─── Wiki page export ────────────────────────────────────────────────────────
|
|
69
|
+
function exportWikiPages(libraryDb, outputDir, agentFilter, tagPrefix, skipUnchanged, result) {
|
|
70
|
+
const wikiDir = join(outputDir, 'wiki');
|
|
71
|
+
mkdirSync(wikiDir, { recursive: true });
|
|
72
|
+
let rows;
|
|
73
|
+
try {
|
|
74
|
+
const sql = agentFilter
|
|
75
|
+
? `SELECT * FROM knowledge WHERE agent_id = ? AND superseded_at IS NULL AND domain = 'topic-synthesis' ORDER BY updated_at DESC`
|
|
76
|
+
: `SELECT * FROM knowledge WHERE superseded_at IS NULL AND domain = 'topic-synthesis' ORDER BY updated_at DESC`;
|
|
77
|
+
rows = (agentFilter
|
|
78
|
+
? libraryDb.prepare(sql).all(agentFilter)
|
|
79
|
+
: libraryDb.prepare(sql).all());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
result.errors.push('wiki: failed to query knowledge table');
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const topicFiles = [];
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
try {
|
|
88
|
+
const rawTags = row.tags ? JSON.parse(row.tags) : [];
|
|
89
|
+
const tags = [tagPrefix, `${tagPrefix}/wiki`, ...rawTags];
|
|
90
|
+
// Resolve cross-links from knowledge_links table
|
|
91
|
+
let linkRows = [];
|
|
92
|
+
try {
|
|
93
|
+
linkRows = libraryDb.prepare(`
|
|
94
|
+
SELECT kl.from_id, kl.to_id, kl.link_type, k.key as to_key
|
|
95
|
+
FROM knowledge_links kl
|
|
96
|
+
JOIN knowledge k ON k.id = kl.to_id
|
|
97
|
+
WHERE kl.from_id = ? AND kl.from_type = 'knowledge' AND kl.to_type = 'knowledge'
|
|
98
|
+
`).all(row.id);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// knowledge_links may not exist yet — non-fatal
|
|
102
|
+
}
|
|
103
|
+
const wikilinks = linkRows
|
|
104
|
+
.map(l => `[[${safeFilename(l.to_key)}]]`)
|
|
105
|
+
.join(' ');
|
|
106
|
+
const frontmatter = buildFrontmatter({
|
|
107
|
+
title: row.key,
|
|
108
|
+
tags,
|
|
109
|
+
agent: row.agent_id,
|
|
110
|
+
topic: row.topic || row.key,
|
|
111
|
+
domain: row.domain,
|
|
112
|
+
'hypermem-type': 'wiki-page',
|
|
113
|
+
'last-synthesized': formatTimestamp(row.updated_at),
|
|
114
|
+
'created-at': formatTimestamp(row.created_at),
|
|
115
|
+
});
|
|
116
|
+
let body = row.content.trim();
|
|
117
|
+
if (linkRows.length > 0) {
|
|
118
|
+
body += `\n\n## Related Topics\n\n${wikilinks}`;
|
|
119
|
+
}
|
|
120
|
+
const note = `${frontmatter}${body}\n`;
|
|
121
|
+
const fileName = `${safeFilename(row.key)}.md`;
|
|
122
|
+
const filePath = join(wikiDir, fileName);
|
|
123
|
+
writeIfChanged(filePath, note, skipUnchanged, result);
|
|
124
|
+
topicFiles.push(fileName);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
result.errors.push(`wiki: failed to export topic '${row.key}': ${String(err)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return topicFiles;
|
|
131
|
+
}
|
|
132
|
+
// ─── Facts export ────────────────────────────────────────────────────────────
|
|
133
|
+
function exportFacts(mainDb, outputDir, agentFilter, tagPrefix, skipUnchanged, result) {
|
|
134
|
+
const factsDir = join(outputDir, 'facts');
|
|
135
|
+
mkdirSync(factsDir, { recursive: true });
|
|
136
|
+
let rows;
|
|
137
|
+
try {
|
|
138
|
+
const sql = agentFilter
|
|
139
|
+
? `SELECT * FROM facts WHERE agent_id = ? ORDER BY domain, updated_at DESC`
|
|
140
|
+
: `SELECT * FROM facts ORDER BY agent_id, domain, updated_at DESC`;
|
|
141
|
+
rows = (agentFilter
|
|
142
|
+
? mainDb.prepare(sql).all(agentFilter)
|
|
143
|
+
: mainDb.prepare(sql).all());
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
result.errors.push('facts: failed to query facts table');
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
// Group by agent + domain
|
|
150
|
+
const groups = new Map();
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
const domain = row.domain || 'general';
|
|
153
|
+
const key = `${row.agent_id}__${domain}`;
|
|
154
|
+
if (!groups.has(key))
|
|
155
|
+
groups.set(key, []);
|
|
156
|
+
groups.get(key).push(row);
|
|
157
|
+
}
|
|
158
|
+
const factFiles = [];
|
|
159
|
+
for (const [groupKey, facts] of groups) {
|
|
160
|
+
try {
|
|
161
|
+
const [agentId, domain] = groupKey.split('__');
|
|
162
|
+
const rawTags = [tagPrefix, `${tagPrefix}/facts`, `agent/${agentId}`, `domain/${domain}`];
|
|
163
|
+
const frontmatter = buildFrontmatter({
|
|
164
|
+
title: `${agentId} — ${domain} facts`,
|
|
165
|
+
tags: rawTags,
|
|
166
|
+
agent: agentId,
|
|
167
|
+
domain,
|
|
168
|
+
'hypermem-type': 'facts',
|
|
169
|
+
'fact-count': facts.length,
|
|
170
|
+
'last-updated': formatTimestamp(Math.max(...facts.map(f => f.updated_at))),
|
|
171
|
+
});
|
|
172
|
+
const lines = [`# ${agentId} — ${domain}\n`];
|
|
173
|
+
for (const fact of facts) {
|
|
174
|
+
const confidence = Math.round(fact.confidence * 100);
|
|
175
|
+
const date = formatTimestamp(fact.updated_at);
|
|
176
|
+
const factTags = fact.tags
|
|
177
|
+
? JSON.parse(fact.tags).map(t => `#${t}`).join(' ')
|
|
178
|
+
: '';
|
|
179
|
+
lines.push(`- ${fact.content} `);
|
|
180
|
+
lines.push(` *confidence: ${confidence}% · ${date}${factTags ? ' · ' + factTags : ''}*\n`);
|
|
181
|
+
}
|
|
182
|
+
const note = `${frontmatter}${lines.join('\n')}\n`;
|
|
183
|
+
const fileName = `${safeFilename(agentId)}-${safeFilename(domain)}.md`;
|
|
184
|
+
const filePath = join(factsDir, fileName);
|
|
185
|
+
writeIfChanged(filePath, note, skipUnchanged, result);
|
|
186
|
+
factFiles.push(fileName);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
result.errors.push(`facts: failed to export group '${groupKey}': ${String(err)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return factFiles;
|
|
193
|
+
}
|
|
194
|
+
// ─── Index note ───────────────────────────────────────────────────────────────
|
|
195
|
+
function exportIndex(outputDir, wikiFiles, factFiles, tagPrefix, skipUnchanged, result) {
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
const frontmatter = buildFrontmatter({
|
|
198
|
+
title: 'hypermem export index',
|
|
199
|
+
tags: [tagPrefix, `${tagPrefix}/index`],
|
|
200
|
+
'hypermem-type': 'index',
|
|
201
|
+
'exported-at': now,
|
|
202
|
+
'wiki-pages': wikiFiles.length,
|
|
203
|
+
'fact-files': factFiles.length,
|
|
204
|
+
});
|
|
205
|
+
const lines = [
|
|
206
|
+
'# hypermem export index\n',
|
|
207
|
+
`> exported at ${now}\n`,
|
|
208
|
+
];
|
|
209
|
+
if (wikiFiles.length > 0) {
|
|
210
|
+
lines.push('## wiki pages\n');
|
|
211
|
+
for (const f of wikiFiles) {
|
|
212
|
+
const name = f.replace('.md', '');
|
|
213
|
+
lines.push(`- [[wiki/${name}]]`);
|
|
214
|
+
}
|
|
215
|
+
lines.push('');
|
|
216
|
+
}
|
|
217
|
+
if (factFiles.length > 0) {
|
|
218
|
+
lines.push('## fact collections\n');
|
|
219
|
+
for (const f of factFiles) {
|
|
220
|
+
const name = f.replace('.md', '');
|
|
221
|
+
lines.push(`- [[facts/${name}]]`);
|
|
222
|
+
}
|
|
223
|
+
lines.push('');
|
|
224
|
+
}
|
|
225
|
+
const note = `${frontmatter}${lines.join('\n')}\n`;
|
|
226
|
+
const filePath = join(outputDir, '_hypermem-index.md');
|
|
227
|
+
writeIfChanged(filePath, note, skipUnchanged, result);
|
|
228
|
+
}
|
|
229
|
+
// ─── Main export function ────────────────────────────────────────────────────
|
|
230
|
+
/**
|
|
231
|
+
* Export hypermem memories to an Obsidian vault.
|
|
232
|
+
*
|
|
233
|
+
* @param mainDb - hypermem main DB (facts, episodes)
|
|
234
|
+
* @param libraryDb - hypermem library DB (knowledge, wiki pages)
|
|
235
|
+
* @param config - export config
|
|
236
|
+
* @returns ObsidianExportResult with counts and file list
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* const result = await exportToVault(mainDb, libraryDb, {
|
|
240
|
+
* vaultPath: '/Users/me/obsidian/my-vault',
|
|
241
|
+
* agentId: 'main',
|
|
242
|
+
* tagPrefix: 'hypermem',
|
|
243
|
+
* });
|
|
244
|
+
* console.log(`Wrote ${result.written} files to ${result.outputDir}`);
|
|
245
|
+
*/
|
|
246
|
+
export function exportToVault(mainDb, libraryDb, config) {
|
|
247
|
+
const vaultFolder = config.vaultFolder ?? 'hypermem';
|
|
248
|
+
const tagPrefix = config.tagPrefix ?? 'hypermem';
|
|
249
|
+
const includeFacts = config.includeFacts ?? true;
|
|
250
|
+
const includeWikiPages = config.includeWikiPages ?? true;
|
|
251
|
+
const includeIndex = config.includeIndex ?? true;
|
|
252
|
+
const skipUnchanged = config.skipUnchanged ?? true;
|
|
253
|
+
const agentFilter = config.agentId ?? null;
|
|
254
|
+
const outputDir = resolve(config.vaultPath, vaultFolder);
|
|
255
|
+
mkdirSync(outputDir, { recursive: true });
|
|
256
|
+
const result = {
|
|
257
|
+
written: 0,
|
|
258
|
+
skipped: 0,
|
|
259
|
+
errors: [],
|
|
260
|
+
outputDir,
|
|
261
|
+
files: [],
|
|
262
|
+
};
|
|
263
|
+
const wikiFiles = includeWikiPages
|
|
264
|
+
? exportWikiPages(libraryDb, outputDir, agentFilter, tagPrefix, skipUnchanged, result)
|
|
265
|
+
: [];
|
|
266
|
+
const factFiles = includeFacts
|
|
267
|
+
? exportFacts(mainDb, outputDir, agentFilter, tagPrefix, skipUnchanged, result)
|
|
268
|
+
: [];
|
|
269
|
+
if (includeIndex) {
|
|
270
|
+
exportIndex(outputDir, wikiFiles, factFiles, tagPrefix, skipUnchanged, result);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=obsidian-exporter.js.map
|