stellavault 0.8.4 → 0.8.6

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.
@@ -159,6 +159,29 @@ function findMdFiles(dir, files = []) {
159
159
  }
160
160
  return files;
161
161
  }
162
+ function scanFile(vaultPath, filePath) {
163
+ const rel = relative(vaultPath, filePath).replace(/\\/g, "/");
164
+ try {
165
+ const stat = statSync(filePath);
166
+ if (stat.size === 0)
167
+ return { skipped: { path: rel, reason: "empty" } };
168
+ if (stat.size > MAX_FILE_BYTES)
169
+ return { skipped: { path: rel, reason: "too-large", detail: `${stat.size}B` } };
170
+ const doc = parseDocument(vaultPath, filePath);
171
+ if (!doc.content || doc.content.trim().length === 0) {
172
+ return { skipped: { path: rel, reason: "empty", detail: "no content after frontmatter" } };
173
+ }
174
+ return { document: doc };
175
+ } catch (err) {
176
+ const msg = err?.message ?? String(err);
177
+ const reason = /ENOENT|EACCES|EPERM/.test(msg) ? "unreadable" : "parse-error";
178
+ return { skipped: { path: rel, reason, detail: msg.slice(0, 200) } };
179
+ }
180
+ }
181
+ function docIdForPath(vaultPath, filePath) {
182
+ const relativePath = relative(vaultPath, filePath).replace(/\\/g, "/");
183
+ return createHash("sha256").update(relativePath).digest("hex").slice(0, 16);
184
+ }
162
185
  function parseDocument(vaultPath, filePath) {
163
186
  const raw = readFileSync2(filePath, "utf-8");
164
187
  const stat = statSync(filePath);
@@ -654,12 +677,25 @@ var init_retry = __esm({
654
677
  });
655
678
 
656
679
  // packages/core/dist/indexer/local-embedder.js
680
+ import { mkdirSync as mkdirSync2 } from "node:fs";
681
+ import { join as join3 } from "node:path";
682
+ import { homedir as homedir2 } from "node:os";
657
683
  function getPipeline(modelName) {
658
684
  let p = pipelineCache.get(modelName);
659
685
  if (p)
660
686
  return p;
661
687
  p = (async () => {
662
- const { pipeline: createPipeline } = await import("@xenova/transformers");
688
+ const { pipeline: createPipeline, env } = await import("@xenova/transformers");
689
+ const cacheOverride = process.env.STELLAVAULT_MODEL_CACHE;
690
+ if (cacheOverride) {
691
+ env.cacheDir = cacheOverride;
692
+ } else if (/\.asar[\\/]/.test(env.cacheDir ?? "")) {
693
+ env.cacheDir = join3(homedir2(), ".stellavault", "model-cache");
694
+ }
695
+ try {
696
+ mkdirSync2(env.cacheDir, { recursive: true });
697
+ } catch {
698
+ }
663
699
  return createPipeline("feature-extraction", `Xenova/${modelName}`, { quantized: true });
664
700
  })();
665
701
  pipelineCache.set(modelName, p);
@@ -675,6 +711,8 @@ function createLocalEmbedder(modelName = "nomic-embed-text-v1.5") {
675
711
  pipeline = await getPipeline(modelName);
676
712
  },
677
713
  async embed(text) {
714
+ if (!pipeline)
715
+ pipeline = await getPipeline(modelName);
678
716
  let output = await pipeline(text, { pooling: "mean", normalize: true });
679
717
  const result = Array.from(output.data).slice(0, dims);
680
718
  try {
@@ -685,6 +723,8 @@ function createLocalEmbedder(modelName = "nomic-embed-text-v1.5") {
685
723
  return result;
686
724
  },
687
725
  async embedBatch(texts, batchSize = 16) {
726
+ if (!pipeline)
727
+ pipeline = await getPipeline(modelName);
688
728
  const results = [];
689
729
  let processed = 0;
690
730
  for (let i = 0; i < texts.length; i += batchSize) {
@@ -793,10 +833,14 @@ __export(indexer_exports, {
793
833
  chunkDocument: () => chunkDocument,
794
834
  createLocalEmbedder: () => createLocalEmbedder,
795
835
  createWatcher: () => createWatcher,
836
+ docIdForPath: () => docIdForPath,
796
837
  estimateTokens: () => estimateTokens,
838
+ indexFiles: () => indexFiles,
797
839
  indexVault: () => indexVault,
840
+ scanFile: () => scanFile,
798
841
  scanVault: () => scanVault
799
842
  });
843
+ import { existsSync as existsSync2 } from "node:fs";
800
844
  async function indexVault(vaultPath, options) {
801
845
  const start = Date.now();
802
846
  const { store, embedder, chunkOptions, onProgress } = options;
@@ -861,6 +905,80 @@ async function indexVault(vaultPath, options) {
861
905
  failedFiles
862
906
  };
863
907
  }
908
+ async function indexFiles(vaultPath, filePaths, options) {
909
+ const start = Date.now();
910
+ const { store, embedder, chunkOptions, onProgress } = options;
911
+ const existingDocs = await store.getAllDocuments();
912
+ const existingMap = new Map(existingDocs.map((d) => [d.id, d.contentHash]));
913
+ let indexed = 0;
914
+ let skipped = 0;
915
+ let deleted = 0;
916
+ let failed = 0;
917
+ let totalChunks = 0;
918
+ const skippedFiles = [];
919
+ const failedFiles = [];
920
+ const uniquePaths = [...new Set(filePaths)];
921
+ for (let i = 0; i < uniquePaths.length; i++) {
922
+ const filePath = uniquePaths[i];
923
+ if (!existsSync2(filePath)) {
924
+ try {
925
+ await store.deleteByDocumentId(docIdForPath(vaultPath, filePath));
926
+ deleted++;
927
+ } catch (err) {
928
+ failed++;
929
+ failedFiles.push({ path: filePath, error: err?.message ?? String(err) });
930
+ console.error(errors.indexingFailed(filePath, err).format());
931
+ }
932
+ continue;
933
+ }
934
+ const result = scanFile(vaultPath, filePath);
935
+ if ("skipped" in result) {
936
+ skipped++;
937
+ skippedFiles.push(result.skipped);
938
+ continue;
939
+ }
940
+ const doc = result.document;
941
+ onProgress?.(i + 1, uniquePaths.length, doc);
942
+ if (existingMap.get(doc.id) === doc.contentHash) {
943
+ skipped++;
944
+ continue;
945
+ }
946
+ try {
947
+ const chunks = chunkDocument(doc.id, doc.content, chunkOptions);
948
+ const texts = chunks.map((c) => c.content);
949
+ const embeddings = await withRetry(() => embedder.embedBatch(texts), { maxRetries: 2, baseDelayMs: 1e3 });
950
+ const chunksWithEmbeddings = chunks.map((c, j) => ({
951
+ ...c,
952
+ embedding: embeddings[j],
953
+ entities: extractEntities({
954
+ content: c.content,
955
+ heading: c.heading,
956
+ title: doc.title,
957
+ tags: doc.tags
958
+ })
959
+ }));
960
+ await store.upsertDocument(doc);
961
+ await store.upsertChunks(chunksWithEmbeddings);
962
+ indexed++;
963
+ totalChunks += chunks.length;
964
+ } catch (err) {
965
+ failed++;
966
+ failedFiles.push({ path: doc.filePath, error: err?.message ?? String(err) });
967
+ console.error(errors.indexingFailed(doc.filePath, err).format());
968
+ }
969
+ }
970
+ return {
971
+ indexed,
972
+ skipped,
973
+ deleted,
974
+ failed,
975
+ totalChunks,
976
+ elapsedMs: Date.now() - start,
977
+ totalFiles: uniquePaths.length,
978
+ skippedFiles,
979
+ failedFiles
980
+ };
981
+ }
864
982
  var init_indexer = __esm({
865
983
  "packages/core/dist/indexer/index.js"() {
866
984
  "use strict";
@@ -875,76 +993,56 @@ var init_indexer = __esm({
875
993
  }
876
994
  });
877
995
 
878
- // packages/core/dist/utils/math.js
879
- function cosineSimilarity(a, b) {
880
- if (a.length !== b.length)
881
- return 0;
882
- let dot = 0, normA = 0, normB = 0;
883
- for (let i = 0; i < a.length; i++) {
884
- dot += a[i] * b[i];
885
- normA += a[i] * a[i];
886
- normB += b[i] * b[i];
887
- }
888
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
889
- return denom === 0 ? 0 : dot / denom;
890
- }
891
- function dotProduct(a, b) {
892
- let sum = 0;
893
- for (let i = 0; i < a.length; i++)
894
- sum += a[i] * b[i];
895
- return sum;
896
- }
897
- function normalizeVector(v) {
898
- let norm = 0;
899
- for (let i = 0; i < v.length; i++)
900
- norm += v[i] * v[i];
901
- norm = Math.sqrt(norm);
902
- if (norm === 0)
903
- return v;
904
- for (let i = 0; i < v.length; i++)
905
- v[i] /= norm;
906
- return v;
907
- }
908
- function euclideanDist(a, b) {
909
- let sum = 0;
910
- for (let i = 0; i < a.length; i++) {
911
- const diff = a[i] - b[i];
912
- sum += diff * diff;
913
- }
914
- return Math.sqrt(sum);
915
- }
916
- var init_math = __esm({
917
- "packages/core/dist/utils/math.js"() {
918
- "use strict";
919
- }
920
- });
921
-
922
996
  // packages/core/dist/api/graph-data.js
923
997
  var graph_data_exports = {};
924
998
  __export(graph_data_exports, {
925
- buildGraphData: () => buildGraphData
999
+ buildClusteredGraph: () => buildClusteredGraph,
1000
+ buildGraphData: () => buildGraphData,
1001
+ flattenClusterLevel: () => flattenClusterLevel
926
1002
  });
927
1003
  import { createHash as createHash2 } from "node:crypto";
1004
+ function paletteHex(clusterId) {
1005
+ return PALETTE_HEX[(clusterId % PALETTE_HEX.length + PALETTE_HEX.length) % PALETTE_HEX.length];
1006
+ }
928
1007
  async function buildGraphData(store, options = {}) {
929
- const { edgeThreshold = 0.15, maxEdgesPerNode = 5 } = options;
930
- const [docs, embeddings] = await Promise.all([
931
- store.getAllDocuments(),
932
- store.getDocumentEmbeddings()
933
- ]);
1008
+ const {
1009
+ // 0.35: 384-dim 임베딩에서 의미있는 코사인 임계. 0.15는 사실상 모든 페어가 통과 →
1010
+ // 중간 neighbor 배열이 수백만 객체로 폭증(빌드 지연) + 시각적으로 과밀한 엣지 거미줄.
1011
+ edgeThreshold = 0.35,
1012
+ maxEdgesPerNode = 5
1013
+ } = options;
1014
+ const docs = await store.getDocumentsMeta();
934
1015
  const edges = [];
935
1016
  const edgeCounts = /* @__PURE__ */ new Map();
936
- const docsWithVecs = docs.filter((d) => embeddings.has(d.id));
937
- const normalizedVecs = /* @__PURE__ */ new Map();
938
- for (const doc of docsWithVecs) {
939
- normalizedVecs.set(doc.id, normalizeVector([...embeddings.get(doc.id)]));
940
- }
941
- const docIds = [...normalizedVecs.keys()];
942
- const vecArray = docIds.map((id) => normalizedVecs.get(id));
1017
+ const NODE_CAP = Math.max(200, Math.floor(options.nodeCap ?? (Number(process.env.GRAPH_NODE_CAP) || 1500)));
1018
+ const ranked = [...docs].sort((a, b) => String(b.lastModified ?? "").localeCompare(String(a.lastModified ?? "")));
1019
+ if (docs.length > NODE_CAP) {
1020
+ console.warn(`[graph] capped to ${NODE_CAP} most-recent notes (of ${docs.length}) \u2014 raise GRAPH_NODE_CAP to include more.`);
1021
+ }
1022
+ const embeddings = await store.getDocumentEmbeddingsByIds(ranked.slice(0, NODE_CAP).map((d) => d.id));
1023
+ const docsWithVecs = ranked.filter((d) => embeddings.has(d.id)).slice(0, NODE_CAP);
1024
+ const docIds = docsWithVecs.map((d) => d.id);
943
1025
  const n = docIds.length;
1026
+ const dim = n > 0 ? embeddings.get(docIds[0]).length : 0;
1027
+ const flat = new Float32Array(n * dim);
1028
+ for (let i = 0; i < n; i++) {
1029
+ const v = embeddings.get(docIds[i]);
1030
+ let mag = 0;
1031
+ for (let d = 0; d < dim; d++)
1032
+ mag += v[d] * v[d];
1033
+ mag = Math.sqrt(mag) || 1;
1034
+ const off = i * dim;
1035
+ for (let d = 0; d < dim; d++)
1036
+ flat[off + d] = v[d] / mag;
1037
+ }
944
1038
  const neighbors = Array.from({ length: n }, () => []);
945
1039
  for (let i = 0; i < n; i++) {
1040
+ const oi = i * dim;
946
1041
  for (let j = i + 1; j < n; j++) {
947
- const sim = dotProduct(vecArray[i], vecArray[j]);
1042
+ const oj = j * dim;
1043
+ let sim = 0;
1044
+ for (let d = 0; d < dim; d++)
1045
+ sim += flat[oi + d] * flat[oj + d];
948
1046
  if (sim >= edgeThreshold) {
949
1047
  neighbors[i].push({ peer: j, sim });
950
1048
  neighbors[j].push({ peer: i, sim });
@@ -991,16 +1089,16 @@ async function buildGraphData(store, options = {}) {
991
1089
  nodeCount: folderCounts.get(i) ?? 0
992
1090
  }));
993
1091
  } else {
994
- const docIds2 = docs.filter((d) => embeddings.has(d.id)).map((d) => d.id);
995
- const vectors = docIds2.map((id) => embeddings.get(id));
996
- const k = Math.min(Math.max(5, Math.round(Math.sqrt(docIds2.length / 5))), 10);
997
- const assignments = kMeans(vectors, k);
1092
+ const clusterIds = docIds;
1093
+ const k = options.clusterCount && options.clusterCount > 0 ? Math.floor(options.clusterCount) : Math.min(Math.max(5, Math.round(Math.sqrt(clusterIds.length / 5))), 10);
1094
+ const assignments = kMeans(flat, n, dim, Math.min(k, clusterIds.length || 1), 10);
1095
+ const docById = new Map(docsWithVecs.map((d) => [d.id, d]));
998
1096
  const clusterDocInfos = /* @__PURE__ */ new Map();
999
- for (let i = 0; i < docIds2.length; i++) {
1097
+ for (let i = 0; i < clusterIds.length; i++) {
1000
1098
  const cId = assignments[i];
1001
1099
  if (!clusterDocInfos.has(cId))
1002
1100
  clusterDocInfos.set(cId, []);
1003
- const doc = docs.find((d) => d.id === docIds2[i]);
1101
+ const doc = docById.get(clusterIds[i]);
1004
1102
  if (doc)
1005
1103
  clusterDocInfos.get(cId).push({ id: doc.id, title: doc.title });
1006
1104
  }
@@ -1021,8 +1119,8 @@ async function buildGraphData(store, options = {}) {
1021
1119
  });
1022
1120
  }
1023
1121
  assignmentMap = /* @__PURE__ */ new Map();
1024
- for (let i = 0; i < docIds2.length; i++) {
1025
- assignmentMap.set(docIds2[i], assignments[i]);
1122
+ for (let i = 0; i < clusterIds.length; i++) {
1123
+ assignmentMap.set(clusterIds[i], assignments[i]);
1026
1124
  }
1027
1125
  }
1028
1126
  const connectionCounts = /* @__PURE__ */ new Map();
@@ -1031,7 +1129,7 @@ async function buildGraphData(store, options = {}) {
1031
1129
  connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
1032
1130
  }
1033
1131
  const maxConnections = Math.max(1, ...connectionCounts.values());
1034
- const nodes = docs.map((doc) => {
1132
+ const nodes = docsWithVecs.map((doc) => {
1035
1133
  const conns = connectionCounts.get(doc.id) ?? 0;
1036
1134
  const ratio = conns / maxConnections;
1037
1135
  const size = 1 + 6 * Math.pow(ratio, 0.5);
@@ -1059,40 +1157,315 @@ async function buildGraphData(store, options = {}) {
1059
1157
  }
1060
1158
  };
1061
1159
  }
1062
- function kMeans(vectors, k, maxIter = 50) {
1063
- if (vectors.length === 0)
1160
+ function flattenClusterLevel(level) {
1161
+ const nodes = level.superNodes.map((sn) => ({
1162
+ id: `cluster:${sn.clusterId}`,
1163
+ label: sn.label,
1164
+ filePath: "",
1165
+ tags: [],
1166
+ clusterId: sn.clusterId,
1167
+ position: sn.position,
1168
+ size: sn.size,
1169
+ source: "cluster",
1170
+ type: "cluster",
1171
+ isCluster: true,
1172
+ memberCount: sn.memberCount,
1173
+ representativeId: sn.representativeId
1174
+ }));
1175
+ const edges = level.metaEdges.map((me) => ({
1176
+ source: `cluster:${me.sourceCluster}`,
1177
+ target: `cluster:${me.targetCluster}`,
1178
+ weight: me.weight
1179
+ }));
1180
+ const clusters = level.superNodes.map((sn) => ({
1181
+ id: sn.clusterId,
1182
+ label: sn.label,
1183
+ color: paletteHex(sn.clusterId),
1184
+ nodeCount: sn.memberCount
1185
+ }));
1186
+ return {
1187
+ nodes,
1188
+ edges,
1189
+ clusters,
1190
+ stats: {
1191
+ nodeCount: nodes.length,
1192
+ edgeCount: edges.length,
1193
+ clusterCount: clusters.length
1194
+ }
1195
+ };
1196
+ }
1197
+ async function buildClusteredGraph(store, options = {}) {
1198
+ const mode = options.mode ?? "semantic";
1199
+ const clusterCap = Math.max(200, Math.floor(options.clusterCap ?? (Number(process.env.GRAPH_CLUSTER_CAP) || 3e3)));
1200
+ const clusterCount = Math.max(1, Math.floor(options.clusterCount ?? Math.min(80, Math.max(6, Math.round(Math.sqrt(clusterCap / 2.5))))));
1201
+ const data = await buildGraphData(store, {
1202
+ mode,
1203
+ nodeCap: clusterCap,
1204
+ clusterCount,
1205
+ edgeThreshold: options.edgeThreshold,
1206
+ maxEdgesPerNode: options.maxEdgesPerNode
1207
+ });
1208
+ const nodeCluster = /* @__PURE__ */ new Map();
1209
+ const byCluster = /* @__PURE__ */ new Map();
1210
+ for (const node of data.nodes) {
1211
+ const cid = node.clusterId ?? 0;
1212
+ nodeCluster.set(node.id, cid);
1213
+ (byCluster.get(cid) ?? byCluster.set(cid, []).get(cid)).push(node);
1214
+ }
1215
+ const degree = /* @__PURE__ */ new Map();
1216
+ for (const e of data.edges) {
1217
+ degree.set(e.source, (degree.get(e.source) ?? 0) + 1);
1218
+ degree.set(e.target, (degree.get(e.target) ?? 0) + 1);
1219
+ }
1220
+ const intraByCluster = /* @__PURE__ */ new Map();
1221
+ const metaMap = /* @__PURE__ */ new Map();
1222
+ for (const e of data.edges) {
1223
+ const ca = nodeCluster.get(e.source), cb = nodeCluster.get(e.target);
1224
+ if (ca == null || cb == null)
1225
+ continue;
1226
+ if (ca === cb) {
1227
+ (intraByCluster.get(ca) ?? intraByCluster.set(ca, []).get(ca)).push(e);
1228
+ } else {
1229
+ const lo = Math.min(ca, cb), hi = Math.max(ca, cb);
1230
+ const key = `${lo}:${hi}`;
1231
+ const m = metaMap.get(key);
1232
+ if (m) {
1233
+ m.weight += e.weight;
1234
+ m.count += 1;
1235
+ } else
1236
+ metaMap.set(key, { sourceCluster: lo, targetCluster: hi, weight: e.weight, count: 1 });
1237
+ }
1238
+ }
1239
+ const clusterLabel = /* @__PURE__ */ new Map();
1240
+ for (const c of data.clusters)
1241
+ clusterLabel.set(c.id, c.label);
1242
+ const superNodes = [];
1243
+ for (const [cid, mem] of byCluster) {
1244
+ let rep = mem[0], repDeg = -1;
1245
+ for (const m of mem) {
1246
+ const d = degree.get(m.id) ?? 0;
1247
+ if (d > repDeg) {
1248
+ repDeg = d;
1249
+ rep = m;
1250
+ }
1251
+ }
1252
+ superNodes.push({
1253
+ clusterId: cid,
1254
+ // strip buildGraphData's trailing " (N)" — memberCount is a separate field.
1255
+ label: (clusterLabel.get(cid) ?? `Cluster ${cid + 1}`).replace(/\s*\(\d+\)\s*$/, ""),
1256
+ color: CLUSTER_COLORS[cid % CLUSTER_COLORS.length],
1257
+ memberCount: mem.length,
1258
+ position: [0, 0, 0],
1259
+ // assigned below by Fibonacci rank
1260
+ size: 2 + Math.min(12, Math.sqrt(mem.length)),
1261
+ representativeId: rep?.id ?? ""
1262
+ });
1263
+ }
1264
+ superNodes.sort((a, b) => b.memberCount - a.memberCount);
1265
+ layoutSuperNodes(superNodes, metaMap);
1266
+ const members = /* @__PURE__ */ new Map();
1267
+ for (const [cid, mem] of byCluster) {
1268
+ const boundaryEdges = [];
1269
+ for (const e of data.edges) {
1270
+ const ca = nodeCluster.get(e.source), cb = nodeCluster.get(e.target);
1271
+ if (ca === cid && cb !== cid && cb != null)
1272
+ boundaryEdges.push({ source: e.source, targetCluster: cb, weight: e.weight });
1273
+ else if (cb === cid && ca !== cid && ca != null)
1274
+ boundaryEdges.push({ source: e.target, targetCluster: ca, weight: e.weight });
1275
+ }
1276
+ members.set(cid, { clusterId: cid, members: mem, intraEdges: intraByCluster.get(cid) ?? [], boundaryEdges });
1277
+ }
1278
+ const META_PER_CLUSTER = 2;
1279
+ const metaByCluster = /* @__PURE__ */ new Map();
1280
+ for (const m of metaMap.values()) {
1281
+ (metaByCluster.get(m.sourceCluster) ?? metaByCluster.set(m.sourceCluster, []).get(m.sourceCluster)).push(m);
1282
+ (metaByCluster.get(m.targetCluster) ?? metaByCluster.set(m.targetCluster, []).get(m.targetCluster)).push(m);
1283
+ }
1284
+ const keptMeta = /* @__PURE__ */ new Set();
1285
+ for (const list of metaByCluster.values()) {
1286
+ list.sort((a, b) => b.weight - a.weight);
1287
+ for (const m of list.slice(0, META_PER_CLUSTER))
1288
+ keptMeta.add(m);
1289
+ }
1290
+ return {
1291
+ clusterLevel: {
1292
+ level: "galaxy",
1293
+ superNodes,
1294
+ metaEdges: [...keptMeta],
1295
+ totalNodes: data.nodes.length,
1296
+ totalEdges: data.edges.length,
1297
+ layoutVersion: mode
1298
+ },
1299
+ members
1300
+ };
1301
+ }
1302
+ function fibonacciSphere(i, n, radius) {
1303
+ const phi = Math.acos(1 - 2 * (i + 0.5) / Math.max(1, n));
1304
+ const theta = Math.PI * (1 + Math.sqrt(5)) * i;
1305
+ return [
1306
+ radius * Math.sin(phi) * Math.cos(theta),
1307
+ radius * Math.sin(phi) * Math.sin(theta),
1308
+ radius * Math.cos(phi)
1309
+ ];
1310
+ }
1311
+ function layoutSuperNodes(superNodes, metaMap) {
1312
+ const n = superNodes.length;
1313
+ if (n === 0)
1314
+ return;
1315
+ if (n === 1) {
1316
+ superNodes[0].position = [0, 0, 0];
1317
+ return;
1318
+ }
1319
+ const idx = /* @__PURE__ */ new Map();
1320
+ superNodes.forEach((s, i) => idx.set(s.clusterId, i));
1321
+ const links = [];
1322
+ let maxW = 0;
1323
+ for (const m of metaMap.values()) {
1324
+ const a = idx.get(m.sourceCluster), b = idx.get(m.targetCluster);
1325
+ if (a == null || b == null || a === b)
1326
+ continue;
1327
+ links.push([a, b, m.weight]);
1328
+ if (m.weight > maxW)
1329
+ maxW = m.weight;
1330
+ }
1331
+ const wn = maxW > 0 ? 1 / maxW : 1;
1332
+ const pos = new Float32Array(n * 3);
1333
+ const vel = new Float32Array(n * 3);
1334
+ for (let i = 0; i < n; i++) {
1335
+ const p = fibonacciSphere(i, n, 100);
1336
+ pos[i * 3] = p[0];
1337
+ pos[i * 3 + 1] = p[1];
1338
+ pos[i * 3 + 2] = p[2];
1339
+ }
1340
+ const REST = 42, CHARGE = 900, ITERS = 600, DAMP = 0.9, CENTER = 0.012;
1341
+ for (let it = 0; it < ITERS; it++) {
1342
+ const alpha = 1 - it / ITERS;
1343
+ for (let i = 0; i < n; i++) {
1344
+ const ix = pos[i * 3], iy = pos[i * 3 + 1], iz = pos[i * 3 + 2];
1345
+ for (let j = i + 1; j < n; j++) {
1346
+ let dx = pos[j * 3] - ix, dy = pos[j * 3 + 1] - iy, dz = pos[j * 3 + 2] - iz;
1347
+ let d2 = dx * dx + dy * dy + dz * dz;
1348
+ if (d2 < 0.01) {
1349
+ dx = (i - j) % 3 * 0.1 || 0.1;
1350
+ dy = 0.1;
1351
+ dz = 0.1;
1352
+ d2 = dx * dx + dy * dy + dz * dz;
1353
+ }
1354
+ const f = CHARGE * alpha / d2;
1355
+ const fx = dx * f, fy = dy * f, fz = dz * f;
1356
+ vel[i * 3] -= fx;
1357
+ vel[i * 3 + 1] -= fy;
1358
+ vel[i * 3 + 2] -= fz;
1359
+ vel[j * 3] += fx;
1360
+ vel[j * 3 + 1] += fy;
1361
+ vel[j * 3 + 2] += fz;
1362
+ }
1363
+ }
1364
+ for (const [a, b, w] of links) {
1365
+ let dx = pos[b * 3] - pos[a * 3], dy = pos[b * 3 + 1] - pos[a * 3 + 1], dz = pos[b * 3 + 2] - pos[a * 3 + 2];
1366
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.01;
1367
+ const strength = 0.6 * (0.3 + 0.7 * w * wn);
1368
+ const f = (dist - REST) / dist * alpha * strength;
1369
+ const fx = dx * f, fy = dy * f, fz = dz * f;
1370
+ vel[a * 3] += fx;
1371
+ vel[a * 3 + 1] += fy;
1372
+ vel[a * 3 + 2] += fz;
1373
+ vel[b * 3] -= fx;
1374
+ vel[b * 3 + 1] -= fy;
1375
+ vel[b * 3 + 2] -= fz;
1376
+ }
1377
+ for (let i = 0; i < n; i++) {
1378
+ vel[i * 3] -= pos[i * 3] * CENTER * alpha;
1379
+ vel[i * 3 + 1] -= pos[i * 3 + 1] * CENTER * alpha;
1380
+ vel[i * 3 + 2] -= pos[i * 3 + 2] * CENTER * alpha;
1381
+ let vx = vel[i * 3] * DAMP, vy = vel[i * 3 + 1] * DAMP, vz = vel[i * 3 + 2] * DAMP;
1382
+ const sp2 = vx * vx + vy * vy + vz * vz;
1383
+ if (sp2 > 64) {
1384
+ const k = 8 / Math.sqrt(sp2);
1385
+ vx *= k;
1386
+ vy *= k;
1387
+ vz *= k;
1388
+ }
1389
+ vel[i * 3] = vx;
1390
+ vel[i * 3 + 1] = vy;
1391
+ vel[i * 3 + 2] = vz;
1392
+ pos[i * 3] += vx;
1393
+ pos[i * 3 + 1] += vy;
1394
+ pos[i * 3 + 2] += vz;
1395
+ }
1396
+ }
1397
+ let cx = 0, cy = 0, cz = 0;
1398
+ for (let i = 0; i < n; i++) {
1399
+ cx += pos[i * 3];
1400
+ cy += pos[i * 3 + 1];
1401
+ cz += pos[i * 3 + 2];
1402
+ }
1403
+ cx /= n;
1404
+ cy /= n;
1405
+ cz /= n;
1406
+ let maxR = 0;
1407
+ for (let i = 0; i < n; i++) {
1408
+ const dx = pos[i * 3] - cx, dy = pos[i * 3 + 1] - cy, dz = pos[i * 3 + 2] - cz;
1409
+ const r = Math.sqrt(dx * dx + dy * dy + dz * dz);
1410
+ if (r > maxR)
1411
+ maxR = r;
1412
+ }
1413
+ const scale = maxR > 1 ? 125 / maxR : 1;
1414
+ for (let i = 0; i < n; i++) {
1415
+ superNodes[i].position = [
1416
+ (pos[i * 3] - cx) * scale,
1417
+ (pos[i * 3 + 1] - cy) * scale,
1418
+ (pos[i * 3 + 2] - cz) * scale
1419
+ ];
1420
+ }
1421
+ }
1422
+ function kMeans(flat, n, dims, k, maxIter = 50) {
1423
+ if (n === 0 || dims === 0)
1064
1424
  return [];
1065
- const dims = vectors[0].length;
1066
- const centroids = [vectors[Math.floor(Math.random() * vectors.length)].slice()];
1425
+ k = Math.max(1, Math.min(k, n));
1426
+ const centroids = new Float32Array(k * dims);
1427
+ centroids.set(flat.subarray(0, dims), 0);
1428
+ const minDist = new Float64Array(n).fill(Infinity);
1067
1429
  for (let c = 1; c < k; c++) {
1068
- const dists = vectors.map((v) => {
1069
- let minD = Infinity;
1070
- for (const cent of centroids) {
1071
- const d = euclideanDist(v, cent);
1072
- if (d < minD)
1073
- minD = d;
1074
- }
1075
- return minD;
1076
- });
1077
- const totalDist = dists.reduce((a, b) => a + b, 0);
1078
- let r = Math.random() * totalDist;
1079
- for (let i = 0; i < dists.length; i++) {
1080
- r -= dists[i];
1430
+ const ocPrev = (c - 1) * dims;
1431
+ let total = 0;
1432
+ for (let i = 0; i < n; i++) {
1433
+ const oi = i * dims;
1434
+ let d = 0;
1435
+ for (let z = 0; z < dims; z++) {
1436
+ const diff = flat[oi + z] - centroids[ocPrev + z];
1437
+ d += diff * diff;
1438
+ }
1439
+ if (d < minDist[i])
1440
+ minDist[i] = d;
1441
+ total += minDist[i];
1442
+ }
1443
+ let r = Math.random() * total;
1444
+ let pick = n - 1;
1445
+ for (let i = 0; i < n; i++) {
1446
+ r -= minDist[i];
1081
1447
  if (r <= 0) {
1082
- centroids.push(vectors[i].slice());
1448
+ pick = i;
1083
1449
  break;
1084
1450
  }
1085
1451
  }
1086
- if (centroids.length <= c)
1087
- centroids.push(vectors[Math.floor(Math.random() * vectors.length)].slice());
1452
+ centroids.set(flat.subarray(pick * dims, pick * dims + dims), c * dims);
1088
1453
  }
1089
- const assignments = new Array(vectors.length).fill(0);
1454
+ const assignments = new Array(n).fill(0);
1455
+ const sums = new Float64Array(k * dims);
1456
+ const counts = new Uint32Array(k);
1090
1457
  for (let iter = 0; iter < maxIter; iter++) {
1091
1458
  let changed = false;
1092
- for (let i = 0; i < vectors.length; i++) {
1459
+ for (let i = 0; i < n; i++) {
1460
+ const oi = i * dims;
1093
1461
  let bestC = 0, bestD = Infinity;
1094
1462
  for (let c = 0; c < k; c++) {
1095
- const d = euclideanDist(vectors[i], centroids[c]);
1463
+ const oc = c * dims;
1464
+ let d = 0;
1465
+ for (let z = 0; z < dims; z++) {
1466
+ const diff = flat[oi + z] - centroids[oc + z];
1467
+ d += diff * diff;
1468
+ }
1096
1469
  if (d < bestD) {
1097
1470
  bestD = d;
1098
1471
  bestC = c;
@@ -1105,30 +1478,29 @@ function kMeans(vectors, k, maxIter = 50) {
1105
1478
  }
1106
1479
  if (!changed)
1107
1480
  break;
1108
- const sums = Array.from({ length: k }, () => new Float64Array(dims));
1109
- const counts = new Uint32Array(k);
1110
- for (let i = 0; i < vectors.length; i++) {
1481
+ sums.fill(0);
1482
+ counts.fill(0);
1483
+ for (let i = 0; i < n; i++) {
1111
1484
  const c = assignments[i];
1112
1485
  counts[c]++;
1113
- const s = sums[c], v = vectors[i];
1114
- for (let d = 0; d < dims; d++)
1115
- s[d] += v[d];
1486
+ const oi = i * dims, oc = c * dims;
1487
+ for (let z = 0; z < dims; z++)
1488
+ sums[oc + z] += flat[oi + z];
1116
1489
  }
1117
1490
  for (let c = 0; c < k; c++) {
1118
1491
  if (counts[c] === 0)
1119
1492
  continue;
1120
- const s = sums[c];
1121
- for (let d = 0; d < dims; d++)
1122
- centroids[c][d] = s[d] / counts[c];
1493
+ const oc = c * dims;
1494
+ for (let z = 0; z < dims; z++)
1495
+ centroids[oc + z] = sums[oc + z] / counts[c];
1123
1496
  }
1124
1497
  }
1125
1498
  return assignments;
1126
1499
  }
1127
- var CLUSTER_COLORS;
1500
+ var CLUSTER_COLORS, PALETTE_HEX;
1128
1501
  var init_graph_data = __esm({
1129
1502
  "packages/core/dist/api/graph-data.js"() {
1130
1503
  "use strict";
1131
- init_math();
1132
1504
  CLUSTER_COLORS = [
1133
1505
  "#6366f1",
1134
1506
  "#ec4899",
@@ -1146,6 +1518,23 @@ var init_graph_data = __esm({
1146
1518
  "#a3e635",
1147
1519
  "#fb923c"
1148
1520
  ];
1521
+ PALETTE_HEX = [
1522
+ "#7c3aed",
1523
+ "#ec4899",
1524
+ "#f59e0b",
1525
+ "#10b981",
1526
+ "#3b82f6",
1527
+ "#ef4444",
1528
+ "#06b6d4",
1529
+ "#84cc16",
1530
+ "#f97316",
1531
+ "#8b5cf6",
1532
+ "#14b8a6",
1533
+ "#e879f9",
1534
+ "#eab308",
1535
+ "#22d3ee",
1536
+ "#fb7185"
1537
+ ];
1149
1538
  }
1150
1539
  });
1151
1540
 
@@ -1155,8 +1544,6 @@ __export(gap_detector_exports, {
1155
1544
  detectKnowledgeGaps: () => detectKnowledgeGaps
1156
1545
  });
1157
1546
  async function detectKnowledgeGaps(store, graphData) {
1158
- const docs = await store.getAllDocuments();
1159
- const embeddings = await store.getDocumentEmbeddings();
1160
1547
  let gd;
1161
1548
  if (!graphData) {
1162
1549
  const { buildGraphData: buildGraphData2 } = await Promise.resolve().then(() => (init_graph_data(), graph_data_exports));
@@ -1165,10 +1552,13 @@ async function detectKnowledgeGaps(store, graphData) {
1165
1552
  gd = graphData;
1166
1553
  }
1167
1554
  const { nodes, edges, clusters } = gd;
1555
+ const nodeById = /* @__PURE__ */ new Map();
1556
+ for (const n of nodes)
1557
+ nodeById.set(n.id, n);
1168
1558
  const clusterEdges = /* @__PURE__ */ new Map();
1169
1559
  for (const edge of edges) {
1170
- const nodeA = nodes.find((n) => n.id === edge.source);
1171
- const nodeB = nodes.find((n) => n.id === edge.target);
1560
+ const nodeA = nodeById.get(edge.source);
1561
+ const nodeB = nodeById.get(edge.target);
1172
1562
  if (!nodeA || !nodeB || nodeA.clusterId === nodeB.clusterId)
1173
1563
  continue;
1174
1564
  const key = [
@@ -1229,10 +1619,10 @@ var ask_engine_exports = {};
1229
1619
  __export(ask_engine_exports, {
1230
1620
  askVault: () => askVault
1231
1621
  });
1232
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "node:fs";
1233
- import { join as join5, resolve as resolve5 } from "node:path";
1622
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync5 } from "node:fs";
1623
+ import { join as join6, resolve as resolve5 } from "node:path";
1234
1624
  async function askVault(searchEngine, question, options = {}) {
1235
- const { limit = 10, save = false, vaultPath, outputDir = "_stellavault/answers", mode = "default" } = options;
1625
+ const { limit = 10, save = false, vaultPath, outputDir = "_stellavault/answers", mode = "default", synthesizer } = options;
1236
1626
  const results = await searchEngine.search({ query: question, limit });
1237
1627
  const sources = results.map((r) => ({
1238
1628
  title: r.document.title,
@@ -1240,7 +1630,16 @@ async function askVault(searchEngine, question, options = {}) {
1240
1630
  score: Math.round(r.score * 1e3) / 1e3,
1241
1631
  snippet: r.chunk?.content?.substring(0, 200) ?? ""
1242
1632
  }));
1243
- const answer = mode === "quotes" ? composeQuotes(question, results) : composeAnswer(question, results);
1633
+ let answer;
1634
+ if (synthesizer && results.length > 0 && mode !== "quotes") {
1635
+ try {
1636
+ answer = await synthesizer.synthesize({ question, sources, mode: "ask" });
1637
+ } catch {
1638
+ answer = composeAnswer(question, results);
1639
+ }
1640
+ } else {
1641
+ answer = mode === "quotes" ? composeQuotes(question, results) : composeAnswer(question, results);
1642
+ }
1244
1643
  let savedTo = null;
1245
1644
  if (save && vaultPath) {
1246
1645
  savedTo = saveAnswerToVault(question, answer, sources, vaultPath, outputDir);
@@ -1311,13 +1710,13 @@ function composeQuotes(question, results) {
1311
1710
  }
1312
1711
  function saveAnswerToVault(question, answer, sources, vaultPath, outputDir) {
1313
1712
  const dir = resolve5(vaultPath, outputDir);
1314
- if (!existsSync4(dir)) {
1315
- mkdirSync5(dir, { recursive: true });
1713
+ if (!existsSync5(dir)) {
1714
+ mkdirSync6(dir, { recursive: true });
1316
1715
  }
1317
1716
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1318
1717
  const slug = question.toLowerCase().replace(/[^a-z0-9가-힣\s]/g, "").replace(/\s+/g, "-").substring(0, 60);
1319
1718
  const filename = `${date}-${slug}.md`;
1320
- const filePath = join5(outputDir, filename);
1719
+ const filePath = join6(outputDir, filename);
1321
1720
  const fullPath = resolve5(vaultPath, filePath);
1322
1721
  if (!fullPath.startsWith(resolve5(vaultPath))) {
1323
1722
  throw new Error("Invalid output path");
@@ -1352,15 +1751,15 @@ __export(wiki_compiler_exports, {
1352
1751
  extractConcepts: () => extractConcepts,
1353
1752
  scanRawDirectory: () => scanRawDirectory
1354
1753
  });
1355
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync5, statSync as statSync2 } from "node:fs";
1356
- import { join as join6, resolve as resolve6, basename, extname as extname3 } from "node:path";
1754
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync6, statSync as statSync2 } from "node:fs";
1755
+ import { join as join7, resolve as resolve6, basename, extname as extname3 } from "node:path";
1357
1756
  function scanRawDirectory(rawPath) {
1358
- if (!existsSync5(rawPath))
1757
+ if (!existsSync6(rawPath))
1359
1758
  return [];
1360
1759
  const docs = [];
1361
1760
  const files = readdirSync3(rawPath, { recursive: true });
1362
1761
  for (const file of files) {
1363
- const fullPath = join6(rawPath, file);
1762
+ const fullPath = join7(rawPath, file);
1364
1763
  if (!statSync2(fullPath).isFile())
1365
1764
  continue;
1366
1765
  const ext = extname3(file).toLowerCase();
@@ -1420,8 +1819,8 @@ function compileWiki(rawPath, wikiPath, options = {}) {
1420
1819
  if (docs.length === 0) {
1421
1820
  return { rawDocCount: 0, wikiArticles: [], indexFile: "", concepts: [] };
1422
1821
  }
1423
- if (!existsSync5(wikiPath)) {
1424
- mkdirSync6(wikiPath, { recursive: true });
1822
+ if (!existsSync6(wikiPath)) {
1823
+ mkdirSync7(wikiPath, { recursive: true });
1425
1824
  }
1426
1825
  const concepts = extractConcepts(docs);
1427
1826
  const wikiArticles = [];
@@ -1542,9 +1941,9 @@ var init_wiki_compiler = __esm({
1542
1941
 
1543
1942
  // packages/core/dist/federation/identity.js
1544
1943
  import { createHash as createHash3, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";
1545
- import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
1546
- import { homedir as homedir4 } from "node:os";
1547
- import { join as join8 } from "node:path";
1944
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
1945
+ import { homedir as homedir5 } from "node:os";
1946
+ import { join as join9 } from "node:path";
1548
1947
  function derivePeerId(publicKey) {
1549
1948
  return createHash3("sha256").update(publicKey).digest("hex").slice(0, 16);
1550
1949
  }
@@ -1562,7 +1961,7 @@ function generateIdentity(displayName) {
1562
1961
  };
1563
1962
  }
1564
1963
  function persistIdentity(identity) {
1565
- mkdirSync9(IDENTITY_DIR, { recursive: true });
1964
+ mkdirSync10(IDENTITY_DIR, { recursive: true });
1566
1965
  const content = JSON.stringify({
1567
1966
  version: IDENTITY_VERSION,
1568
1967
  algorithm: IDENTITY_ALGORITHM,
@@ -1579,7 +1978,7 @@ function persistIdentity(identity) {
1579
1978
  }
1580
1979
  }
1581
1980
  function getOrCreateIdentity(displayName) {
1582
- if (existsSync7(IDENTITY_FILE)) {
1981
+ if (existsSync8(IDENTITY_FILE)) {
1583
1982
  const raw = JSON.parse(readFileSync7(IDENTITY_FILE, "utf-8"));
1584
1983
  const isV2 = raw.version === IDENTITY_VERSION && raw.algorithm === IDENTITY_ALGORITHM;
1585
1984
  if (isV2) {
@@ -1618,8 +2017,8 @@ var IDENTITY_DIR, IDENTITY_FILE, IDENTITY_VERSION, IDENTITY_ALGORITHM;
1618
2017
  var init_identity = __esm({
1619
2018
  "packages/core/dist/federation/identity.js"() {
1620
2019
  "use strict";
1621
- IDENTITY_DIR = join8(homedir4(), ".stellavault", "federation");
1622
- IDENTITY_FILE = join8(IDENTITY_DIR, "identity.json");
2020
+ IDENTITY_DIR = join9(homedir5(), ".stellavault", "federation");
2021
+ IDENTITY_FILE = join9(IDENTITY_DIR, "identity.json");
1623
2022
  IDENTITY_VERSION = 2;
1624
2023
  IDENTITY_ALGORITHM = "ed25519";
1625
2024
  }
@@ -2088,18 +2487,18 @@ var init_privacy = __esm({
2088
2487
  });
2089
2488
 
2090
2489
  // packages/core/dist/federation/sharing.js
2091
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync8, mkdirSync as mkdirSync10 } from "node:fs";
2092
- import { join as join9 } from "node:path";
2093
- import { homedir as homedir5 } from "node:os";
2490
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync11 } from "node:fs";
2491
+ import { join as join10 } from "node:path";
2492
+ import { homedir as homedir6 } from "node:os";
2094
2493
  function loadSharingConfig() {
2095
- if (existsSync8(SHARING_FILE)) {
2494
+ if (existsSync9(SHARING_FILE)) {
2096
2495
  const raw = JSON.parse(readFileSync8(SHARING_FILE, "utf-8"));
2097
2496
  return { ...DEFAULT_CONFIG2, ...raw };
2098
2497
  }
2099
2498
  return { ...DEFAULT_CONFIG2 };
2100
2499
  }
2101
2500
  function saveSharingConfig(config) {
2102
- mkdirSync10(join9(homedir5(), ".stellavault", "federation"), { recursive: true });
2501
+ mkdirSync11(join10(homedir6(), ".stellavault", "federation"), { recursive: true });
2103
2502
  writeFileSync9(SHARING_FILE, JSON.stringify(config, null, 2), "utf-8");
2104
2503
  }
2105
2504
  function getDocumentLevel(doc, config) {
@@ -2236,7 +2635,7 @@ var init_sharing = __esm({
2236
2635
  3: 5,
2237
2636
  4: 10
2238
2637
  };
2239
- SHARING_FILE = join9(homedir5(), ".stellavault", "federation", "sharing.json");
2638
+ SHARING_FILE = join10(homedir6(), ".stellavault", "federation", "sharing.json");
2240
2639
  SENSITIVE_PATTERNS = [
2241
2640
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
2242
2641
  /\b\d{3}[-.]?\d{4}[-.]?\d{4}\b/,
@@ -2391,6 +2790,25 @@ var init_federation = __esm({
2391
2790
  }
2392
2791
  });
2393
2792
 
2793
+ // packages/core/dist/utils/math.js
2794
+ function cosineSimilarity(a, b) {
2795
+ if (a.length !== b.length)
2796
+ return 0;
2797
+ let dot = 0, normA = 0, normB = 0;
2798
+ for (let i = 0; i < a.length; i++) {
2799
+ dot += a[i] * b[i];
2800
+ normA += a[i] * a[i];
2801
+ normB += b[i] * b[i];
2802
+ }
2803
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
2804
+ return denom === 0 ? 0 : dot / denom;
2805
+ }
2806
+ var init_math = __esm({
2807
+ "packages/core/dist/utils/math.js"() {
2808
+ "use strict";
2809
+ }
2810
+ });
2811
+
2394
2812
  // packages/core/dist/intelligence/duplicate-detector.js
2395
2813
  var duplicate_detector_exports = {};
2396
2814
  __export(duplicate_detector_exports, {
@@ -2649,9 +3067,9 @@ async function extractTimedTranscriptFromHtml(html) {
2649
3067
  async function extractTimedTranscriptViaTool(videoId) {
2650
3068
  const { execSync: execSync3 } = await import("node:child_process");
2651
3069
  const { readdirSync: readdirSync9, readFileSync: readFileSync19, unlinkSync } = await import("node:fs");
2652
- const { join: join32 } = await import("node:path");
3070
+ const { join: join33 } = await import("node:path");
2653
3071
  const tmpDir = (await import("node:os")).tmpdir();
2654
- const tmpBase = join32(tmpDir, `sv-sub-${videoId}`);
3072
+ const tmpBase = join33(tmpDir, `sv-sub-${videoId}`);
2655
3073
  const url = `https://www.youtube.com/watch?v=${videoId}`;
2656
3074
  for (const lang of ["ko", "en"]) {
2657
3075
  try {
@@ -2662,10 +3080,10 @@ async function extractTimedTranscriptViaTool(videoId) {
2662
3080
  const files = readdirSync9(tmpDir).filter((f) => f.startsWith(`sv-sub-${videoId}`) && f.endsWith(".srv1"));
2663
3081
  if (files.length === 0)
2664
3082
  continue;
2665
- const xml = readFileSync19(join32(tmpDir, files[0]), "utf-8");
3083
+ const xml = readFileSync19(join33(tmpDir, files[0]), "utf-8");
2666
3084
  for (const f of files) {
2667
3085
  try {
2668
- unlinkSync(join32(tmpDir, f));
3086
+ unlinkSync(join33(tmpDir, f));
2669
3087
  } catch {
2670
3088
  }
2671
3089
  }
@@ -2860,18 +3278,18 @@ var init_youtube_extractor = __esm({
2860
3278
  });
2861
3279
 
2862
3280
  // packages/core/dist/intelligence/zettelkasten.js
2863
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync9, statSync as statSync3 } from "node:fs";
2864
- import { join as join10, resolve as resolve8, extname as extname4 } from "node:path";
3281
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10, statSync as statSync3 } from "node:fs";
3282
+ import { join as join11, resolve as resolve8, extname as extname4 } from "node:path";
2865
3283
  function scanFrontmatter(vaultPath, options) {
2866
3284
  const entries = [];
2867
3285
  const includeArchived = options?.includeArchived ?? false;
2868
3286
  function walkDir(dir, rel) {
2869
- if (!existsSync9(dir))
3287
+ if (!existsSync10(dir))
2870
3288
  return;
2871
3289
  for (const name of readdirSync4(dir)) {
2872
3290
  if (name.startsWith(".") || name === "node_modules")
2873
3291
  continue;
2874
- const full = join10(dir, name);
3292
+ const full = join11(dir, name);
2875
3293
  const relPath = rel ? `${rel}/${name}` : name;
2876
3294
  if (statSync3(full).isDirectory()) {
2877
3295
  walkDir(full, relPath);
@@ -2987,7 +3405,7 @@ function archiveFile(fullPath, vaultPath) {
2987
3405
  if (vaultPath && !resolved.startsWith(resolve8(vaultPath))) {
2988
3406
  throw new Error("Path traversal detected: file outside vault");
2989
3407
  }
2990
- if (!existsSync9(resolved))
3408
+ if (!existsSync10(resolved))
2991
3409
  throw new Error(`File not found: ${fullPath}`);
2992
3410
  const content = readFileSync9(resolved, "utf-8");
2993
3411
  if (content.startsWith("---\n")) {
@@ -3008,20 +3426,20 @@ var init_zettelkasten = __esm({
3008
3426
  });
3009
3427
 
3010
3428
  // packages/core/dist/intelligence/auto-linker.js
3011
- import { readdirSync as readdirSync5, readFileSync as readFileSync10, existsSync as existsSync10 } from "node:fs";
3012
- import { join as join11, resolve as resolve9, extname as extname5 } from "node:path";
3429
+ import { readdirSync as readdirSync5, readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
3430
+ import { join as join12, resolve as resolve9, extname as extname5 } from "node:path";
3013
3431
  function collectVaultTitles(vaultPath, folders = DEFAULT_FOLDERS) {
3014
3432
  const titles = /* @__PURE__ */ new Set();
3015
3433
  const dirs = [folders.fleeting, folders.literature, folders.permanent, folders.wiki, "_drafts"];
3016
3434
  for (const dir of dirs) {
3017
3435
  const fullDir = resolve9(vaultPath, dir);
3018
- if (!existsSync10(fullDir))
3436
+ if (!existsSync11(fullDir))
3019
3437
  continue;
3020
3438
  try {
3021
3439
  const files = readdirSync5(fullDir).filter((f) => extname5(f) === ".md");
3022
3440
  for (const file of files) {
3023
3441
  try {
3024
- const content = readFileSync10(join11(fullDir, file), "utf-8");
3442
+ const content = readFileSync10(join12(fullDir, file), "utf-8");
3025
3443
  const titleMatch = content.match(/^title:\s*"?([^"\n]+)"?\s*$/m);
3026
3444
  if (titleMatch && titleMatch[1].length > 2) {
3027
3445
  titles.add(titleMatch[1].trim());
@@ -3115,8 +3533,8 @@ __export(ingest_pipeline_exports, {
3115
3533
  ingestBatch: () => ingestBatch,
3116
3534
  promoteNote: () => promoteNote
3117
3535
  });
3118
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
3119
- import { join as join12, resolve as resolve10, basename as basename2 } from "node:path";
3536
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
3537
+ import { join as join13, resolve as resolve10, basename as basename2 } from "node:path";
3120
3538
  function decodeHtmlEntities(text) {
3121
3539
  return text.replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&quot;/g, '"').replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&nbsp;/g, " ");
3122
3540
  }
@@ -3138,13 +3556,13 @@ function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
3138
3556
  };
3139
3557
  const folder = folderMap[autoStage];
3140
3558
  const dir = resolve10(vaultPath, folder);
3141
- if (!existsSync11(dir))
3142
- mkdirSync11(dir, { recursive: true });
3559
+ if (!existsSync12(dir))
3560
+ mkdirSync12(dir, { recursive: true });
3143
3561
  const now = /* @__PURE__ */ new Date();
3144
3562
  const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
3145
3563
  const slug = title.slice(0, 50).replace(/[^a-zA-Z0-9가-힣\s]/g, "").replace(/\s+/g, "-").toLowerCase();
3146
3564
  const filename = `${timestamp}-${slug}.md`;
3147
- const filePath = join12(folder, filename);
3565
+ const filePath = join13(folder, filename);
3148
3566
  const fullPath = resolve10(vaultPath, filePath);
3149
3567
  if (!fullPath.startsWith(resolve10(vaultPath))) {
3150
3568
  throw new Error("Invalid path");
@@ -3182,7 +3600,7 @@ function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
3182
3600
  try {
3183
3601
  const rawDir = resolve10(vaultPath, folders.fleeting);
3184
3602
  const wikiDir = resolve10(vaultPath, folders.wiki);
3185
- if (existsSync11(rawDir)) {
3603
+ if (existsSync12(rawDir)) {
3186
3604
  compileWiki(rawDir, wikiDir);
3187
3605
  }
3188
3606
  } catch (err) {
@@ -3202,7 +3620,7 @@ function ingestBatch(vaultPath, inputs) {
3202
3620
  }
3203
3621
  function promoteNote(vaultPath, filePath, targetStage, folders = DEFAULT_FOLDERS) {
3204
3622
  const fullPath = resolve10(vaultPath, filePath);
3205
- if (!existsSync11(fullPath))
3623
+ if (!existsSync12(fullPath))
3206
3624
  throw new Error(`File not found: ${filePath}`);
3207
3625
  const content = readFileSync11(fullPath, "utf-8");
3208
3626
  const updated = content.replace(/^type:\s*.+$/m, `type: ${targetStage}`);
@@ -3212,9 +3630,9 @@ function promoteNote(vaultPath, filePath, targetStage, folders = DEFAULT_FOLDERS
3212
3630
  permanent: folders.permanent
3213
3631
  };
3214
3632
  const newDir = resolve10(vaultPath, folderMap[targetStage]);
3215
- if (!existsSync11(newDir))
3216
- mkdirSync11(newDir, { recursive: true });
3217
- const newPath = join12(folderMap[targetStage], basename2(filePath));
3633
+ if (!existsSync12(newDir))
3634
+ mkdirSync12(newDir, { recursive: true });
3635
+ const newPath = join13(folderMap[targetStage], basename2(filePath));
3218
3636
  const newFullPath = resolve10(vaultPath, newPath);
3219
3637
  if (!newFullPath.startsWith(resolve10(vaultPath))) {
3220
3638
  throw new Error("Invalid path");
@@ -3640,14 +4058,14 @@ var init_file_extractors = __esm({
3640
4058
 
3641
4059
  // packages/cli/dist/mcp-clients.js
3642
4060
  import { execSync as execSync2 } from "node:child_process";
3643
- import { existsSync as existsSync15, mkdirSync as mkdirSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync16 } from "node:fs";
3644
- import { dirname as dirname4, join as join21 } from "node:path";
3645
- import { homedir as homedir13 } from "node:os";
4061
+ import { existsSync as existsSync16, mkdirSync as mkdirSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync16 } from "node:fs";
4062
+ import { dirname as dirname4, join as join22 } from "node:path";
4063
+ import { homedir as homedir14 } from "node:os";
3646
4064
  function appData() {
3647
- return process.env.APPDATA ?? join21(homedir13(), "AppData", "Roaming");
4065
+ return process.env.APPDATA ?? join22(homedir14(), "AppData", "Roaming");
3648
4066
  }
3649
4067
  function xdgConfig() {
3650
- return process.env.XDG_CONFIG_HOME ?? join21(homedir13(), ".config");
4068
+ return process.env.XDG_CONFIG_HOME ?? join22(homedir14(), ".config");
3651
4069
  }
3652
4070
  function resolveServeCommand(override) {
3653
4071
  if (override?.command) {
@@ -3660,26 +4078,26 @@ function resolveServeCommand(override) {
3660
4078
  }
3661
4079
  function claudeDesktopPath() {
3662
4080
  if (isWin)
3663
- return join21(appData(), "Claude", "claude_desktop_config.json");
4081
+ return join22(appData(), "Claude", "claude_desktop_config.json");
3664
4082
  if (isMac)
3665
- return join21(homedir13(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
3666
- return join21(xdgConfig(), "Claude", "claude_desktop_config.json");
4083
+ return join22(homedir14(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
4084
+ return join22(xdgConfig(), "Claude", "claude_desktop_config.json");
3667
4085
  }
3668
4086
  function vscodePath() {
3669
4087
  if (isWin)
3670
- return join21(appData(), "Code", "User", "mcp.json");
4088
+ return join22(appData(), "Code", "User", "mcp.json");
3671
4089
  if (isMac)
3672
- return join21(homedir13(), "Library", "Application Support", "Code", "User", "mcp.json");
3673
- return join21(xdgConfig(), "Code", "User", "mcp.json");
4090
+ return join22(homedir14(), "Library", "Application Support", "Code", "User", "mcp.json");
4091
+ return join22(xdgConfig(), "Code", "User", "mcp.json");
3674
4092
  }
3675
4093
  function isDetected(client) {
3676
- return existsSync15(client.detectDir);
4094
+ return existsSync16(client.detectDir);
3677
4095
  }
3678
4096
  function writeClientConfig(client, serve) {
3679
4097
  const path = client.configPath;
3680
4098
  try {
3681
4099
  let json = {};
3682
- if (existsSync15(path)) {
4100
+ if (existsSync16(path)) {
3683
4101
  const raw = readFileSync15(path, "utf-8").trim();
3684
4102
  if (raw)
3685
4103
  json = JSON.parse(raw);
@@ -3689,7 +4107,7 @@ function writeClientConfig(client, serve) {
3689
4107
  json[key] = {};
3690
4108
  const already = Boolean(json[key].stellavault);
3691
4109
  json[key].stellavault = client.needsType ? { type: "stdio", command: serve.command, args: serve.args } : { command: serve.command, args: serve.args };
3692
- mkdirSync16(dirname4(path), { recursive: true });
4110
+ mkdirSync17(dirname4(path), { recursive: true });
3693
4111
  writeFileSync16(path, JSON.stringify(json, null, 2) + "\n", "utf-8");
3694
4112
  return { client: client.label, status: already ? "updated" : "written", path };
3695
4113
  } catch (err) {
@@ -3733,16 +4151,16 @@ var init_mcp_clients = __esm({
3733
4151
  {
3734
4152
  id: "cursor",
3735
4153
  label: "Cursor",
3736
- configPath: join21(homedir13(), ".cursor", "mcp.json"),
3737
- detectDir: join21(homedir13(), ".cursor"),
4154
+ configPath: join22(homedir14(), ".cursor", "mcp.json"),
4155
+ detectDir: join22(homedir14(), ".cursor"),
3738
4156
  serversKey: "mcpServers",
3739
4157
  needsType: false
3740
4158
  },
3741
4159
  {
3742
4160
  id: "windsurf",
3743
4161
  label: "Windsurf",
3744
- configPath: join21(homedir13(), ".codeium", "windsurf", "mcp_config.json"),
3745
- detectDir: join21(homedir13(), ".codeium"),
4162
+ configPath: join22(homedir14(), ".codeium", "windsurf", "mcp_config.json"),
4163
+ detectDir: join22(homedir14(), ".codeium"),
3746
4164
  serversKey: "mcpServers",
3747
4165
  needsType: false
3748
4166
  },
@@ -3766,14 +4184,14 @@ __export(setup_cmd_exports, {
3766
4184
  setupCommand: () => setupCommand
3767
4185
  });
3768
4186
  import chalk4 from "chalk";
3769
- import { existsSync as existsSync16 } from "node:fs";
3770
- import { join as join22 } from "node:path";
3771
- import { homedir as homedir14 } from "node:os";
4187
+ import { existsSync as existsSync17 } from "node:fs";
4188
+ import { join as join23 } from "node:path";
4189
+ import { homedir as homedir15 } from "node:os";
3772
4190
  async function setupCommand(options) {
3773
4191
  console.log("");
3774
4192
  console.log(chalk4.bold(" \u2726 Stellavault \u2014 Connect to your AI clients"));
3775
4193
  console.log(chalk4.dim(" Registering Stellavault as an MCP server.\n"));
3776
- if (!existsSync16(join22(homedir14(), ".stellavault.json"))) {
4194
+ if (!existsSync17(join23(homedir15(), ".stellavault.json"))) {
3777
4195
  console.log(chalk4.yellow(" \u26A0 No config found. Run ") + chalk4.cyan("stellavault init") + chalk4.yellow(" first to index your vault.\n"));
3778
4196
  }
3779
4197
  const serve = resolveServeCommand({ command: options.command, args: options.args });
@@ -3835,9 +4253,9 @@ import ora from "ora";
3835
4253
  import chalk from "chalk";
3836
4254
  import { createHash as createHash7 } from "node:crypto";
3837
4255
  import { writeFileSync as writeFileSync15 } from "node:fs";
3838
- import { join as join20 } from "node:path";
3839
- import { homedir as homedir12 } from "node:os";
3840
- import { mkdirSync as mkdirSync15 } from "node:fs";
4256
+ import { join as join21 } from "node:path";
4257
+ import { homedir as homedir13 } from "node:os";
4258
+ import { mkdirSync as mkdirSync16 } from "node:fs";
3841
4259
 
3842
4260
  // packages/core/dist/index.js
3843
4261
  init_config();
@@ -3847,13 +4265,21 @@ import Database from "better-sqlite3";
3847
4265
  import * as sqliteVec from "sqlite-vec";
3848
4266
  import { mkdirSync } from "node:fs";
3849
4267
  import { dirname } from "node:path";
4268
+ function loadVecExtension(db) {
4269
+ const loadablePath = sqliteVec.getLoadablePath();
4270
+ if (/\.asar[\\/]/.test(loadablePath)) {
4271
+ db.loadExtension(loadablePath.replace(/\.asar([\\/])/, ".asar.unpacked$1"));
4272
+ } else {
4273
+ sqliteVec.load(db);
4274
+ }
4275
+ }
3850
4276
  function createSqliteVecStore(dbPath, dimensions = 384) {
3851
4277
  let db;
3852
4278
  return {
3853
4279
  async initialize() {
3854
4280
  mkdirSync(dirname(dbPath), { recursive: true });
3855
4281
  db = new Database(dbPath);
3856
- sqliteVec.load(db);
4282
+ loadVecExtension(db);
3857
4283
  db.pragma("journal_mode = WAL");
3858
4284
  db.pragma("foreign_keys = ON");
3859
4285
  createTables(db, dimensions);
@@ -3989,6 +4415,19 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
3989
4415
  const rows = db.prepare("SELECT * FROM documents ORDER BY last_modified DESC").all();
3990
4416
  return rows.map(rowToDocument);
3991
4417
  },
4418
+ async getDocumentsMeta(maxDocs) {
4419
+ const lim = typeof maxDocs === "number" && Number.isFinite(maxDocs) && maxDocs > 0 ? Math.floor(maxDocs) : 0;
4420
+ const sql = "SELECT id, file_path, title, frontmatter, tags, last_modified FROM documents ORDER BY last_modified DESC" + (lim > 0 ? ` LIMIT ${lim}` : "");
4421
+ const rows = db.prepare(sql).all();
4422
+ return rows.map((r) => ({
4423
+ id: r.id,
4424
+ filePath: r.file_path,
4425
+ title: r.title,
4426
+ frontmatter: JSON.parse(r.frontmatter || "{}"),
4427
+ tags: JSON.parse(r.tags || "[]"),
4428
+ lastModified: r.last_modified
4429
+ }));
4430
+ },
3992
4431
  async getTopics() {
3993
4432
  const rows = db.prepare(`
3994
4433
  SELECT je.value as tag, COUNT(DISTINCT d.id) as count
@@ -4015,23 +4454,38 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
4015
4454
  };
4016
4455
  },
4017
4456
  async getDocumentEmbeddings(maxDocs = 1e4) {
4018
- const BATCH_SIZE = 500;
4019
4457
  const result = /* @__PURE__ */ new Map();
4020
- const stmt = db.prepare(`
4021
- SELECT c.document_id, ce.embedding
4458
+ const rows = db.prepare(`
4459
+ SELECT c.document_id AS document_id, ce.embedding AS embedding
4022
4460
  FROM chunks c
4023
4461
  JOIN chunk_embeddings ce ON ce.chunk_id = c.id
4024
- WHERE c.id IN (
4025
- SELECT MIN(id) FROM chunks GROUP BY document_id
4026
- )
4027
- LIMIT ? OFFSET ?
4028
- `);
4029
- for (let offset = 0; offset < maxDocs; offset += BATCH_SIZE) {
4030
- const rows = stmt.all(Math.min(BATCH_SIZE, maxDocs - offset), offset);
4031
- if (rows.length === 0)
4032
- break;
4033
- for (const row of rows) {
4034
- result.set(row.document_id, bufferToFloat32(row.embedding));
4462
+ WHERE c.id IN (SELECT MIN(id) FROM chunks GROUP BY document_id)
4463
+ LIMIT ?
4464
+ `).all(maxDocs);
4465
+ for (const row of rows) {
4466
+ result.set(row.document_id, bufferToFloat32(row.embedding));
4467
+ }
4468
+ return result;
4469
+ },
4470
+ async getDocumentEmbeddingsByIds(documentIds) {
4471
+ const result = /* @__PURE__ */ new Map();
4472
+ if (documentIds.length === 0)
4473
+ return result;
4474
+ const CHUNK = 800;
4475
+ for (let off = 0; off < documentIds.length; off += CHUNK) {
4476
+ const slice = documentIds.slice(off, off + CHUNK);
4477
+ const ph = slice.map(() => "?").join(",");
4478
+ const cidRows = db.prepare(`SELECT document_id AS d, MIN(id) AS cid FROM chunks WHERE document_id IN (${ph}) GROUP BY document_id`).all(...slice);
4479
+ if (cidRows.length === 0)
4480
+ continue;
4481
+ const docByCid = new Map(cidRows.map((r) => [String(r.cid), r.d]));
4482
+ const cids = cidRows.map((r) => r.cid);
4483
+ const ph2 = cids.map(() => "?").join(",");
4484
+ const embRows = db.prepare(`SELECT chunk_id AS cid, embedding FROM chunk_embeddings WHERE chunk_id IN (${ph2})`).all(...cids);
4485
+ for (const row of embRows) {
4486
+ const docId = docByCid.get(String(row.cid));
4487
+ if (docId)
4488
+ result.set(docId, bufferToFloat32(row.embedding));
4035
4489
  }
4036
4490
  }
4037
4491
  return result;
@@ -4583,10 +5037,10 @@ async function handleGenerateClaudeMd(searchEngine, store, args) {
4583
5037
  }
4584
5038
 
4585
5039
  // packages/core/dist/mcp/tools/snapshot.js
4586
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
4587
- import { join as join3, resolve as resolve2 } from "node:path";
4588
- import { homedir as homedir2 } from "node:os";
4589
- var SNAPSHOT_DIR = join3(homedir2(), ".stellavault", "snapshots");
5040
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
5041
+ import { join as join4, resolve as resolve2 } from "node:path";
5042
+ import { homedir as homedir3 } from "node:os";
5043
+ var SNAPSHOT_DIR = join4(homedir3(), ".stellavault", "snapshots");
4590
5044
  function sanitizeName(name) {
4591
5045
  const sanitized = name.replace(/[^a-zA-Z0-9가-힣_-]/g, "");
4592
5046
  if (!sanitized)
@@ -4624,7 +5078,7 @@ var loadSnapshotToolDef = {
4624
5078
  }
4625
5079
  };
4626
5080
  async function handleCreateSnapshot(searchEngine, args) {
4627
- mkdirSync2(SNAPSHOT_DIR, { recursive: true });
5081
+ mkdirSync3(SNAPSHOT_DIR, { recursive: true });
4628
5082
  const results = [];
4629
5083
  for (const query of args.queries) {
4630
5084
  const hits = await searchEngine.search({ query, limit: 5 });
@@ -4659,15 +5113,15 @@ async function handleCreateSnapshot(searchEngine, args) {
4659
5113
  async function handleLoadSnapshot(args) {
4660
5114
  const safeName = sanitizeName(args.name);
4661
5115
  const filePath = ensureWithinDir(SNAPSHOT_DIR, `${safeName}.json`);
4662
- if (!existsSync2(filePath)) {
5116
+ if (!existsSync3(filePath)) {
4663
5117
  return { error: `Snapshot not found: ${args.name}` };
4664
5118
  }
4665
5119
  return JSON.parse(readFileSync3(filePath, "utf-8"));
4666
5120
  }
4667
5121
 
4668
5122
  // packages/core/dist/mcp/tools/decision-journal.js
4669
- import { writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
4670
- import { join as join4, resolve as resolve3 } from "node:path";
5123
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
5124
+ import { join as join5, resolve as resolve3 } from "node:path";
4671
5125
  var logDecisionToolDef = {
4672
5126
  name: "log-decision",
4673
5127
  description: '\uAE30\uC220\uC801 \uACB0\uC815\uC744 \uAD6C\uC870\uD654\uD558\uC5EC \uAE30\uB85D\uD569\uB2C8\uB2E4. \uB098\uC911\uC5D0 "\uC65C \uC774 \uC120\uD0DD\uC744 \uD588\uC9C0?"\uC5D0 \uB2F5\uBCC0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.',
@@ -4696,8 +5150,8 @@ var findDecisionsToolDef = {
4696
5150
  }
4697
5151
  };
4698
5152
  async function handleLogDecision(vaultPath, args) {
4699
- const decisionsDir = join4(vaultPath, "decisions");
4700
- mkdirSync3(decisionsDir, { recursive: true });
5153
+ const decisionsDir = join5(vaultPath, "decisions");
5154
+ mkdirSync4(decisionsDir, { recursive: true });
4701
5155
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4702
5156
  const slug = args.title.replace(/[^a-zA-Z가-힣0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, 50);
4703
5157
  if (!slug)
@@ -4738,13 +5192,13 @@ ${args.reasoning}
4738
5192
  return { saved: filePath, fileName };
4739
5193
  }
4740
5194
  async function handleFindDecisions(vaultPath, args) {
4741
- const decisionsDir = join4(vaultPath, "decisions");
4742
- if (!existsSync3(decisionsDir))
5195
+ const decisionsDir = join5(vaultPath, "decisions");
5196
+ if (!existsSync4(decisionsDir))
4743
5197
  return { decisions: [], message: "No decisions directory" };
4744
5198
  const files = readdirSync2(decisionsDir).filter((f) => f.endsWith(".md"));
4745
5199
  const query = args.query.toLowerCase();
4746
5200
  const matches = files.map((f) => {
4747
- const content = readFileSync4(join4(decisionsDir, f), "utf-8");
5201
+ const content = readFileSync4(join5(decisionsDir, f), "utf-8");
4748
5202
  const score = content.toLowerCase().includes(query) ? 1 : 0;
4749
5203
  return { file: f, content: content.slice(0, 300), score };
4750
5204
  }).filter((m) => m.score > 0).slice(0, 10);
@@ -4752,11 +5206,11 @@ async function handleFindDecisions(vaultPath, args) {
4752
5206
  }
4753
5207
 
4754
5208
  // packages/core/dist/mcp/tools/export.js
4755
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
5209
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync5 } from "node:fs";
4756
5210
  import { dirname as dirname2, resolve as resolve4 } from "node:path";
4757
- import { homedir as homedir3 } from "node:os";
5211
+ import { homedir as homedir4 } from "node:os";
4758
5212
  var ALLOWED_EXPORT_DIRS = [
4759
- resolve4(homedir3(), ".stellavault"),
5213
+ resolve4(homedir4(), ".stellavault"),
4760
5214
  resolve4(".")
4761
5215
  ];
4762
5216
  function validateExportPath(outputPath) {
@@ -4784,7 +5238,7 @@ async function handleExport(store, args) {
4784
5238
  const stats = await store.getStats();
4785
5239
  const topics = await store.getTopics();
4786
5240
  const safePath = validateExportPath(args.outputPath);
4787
- mkdirSync4(dirname2(safePath), { recursive: true });
5241
+ mkdirSync5(dirname2(safePath), { recursive: true });
4788
5242
  if (args.format === "csv") {
4789
5243
  const header = "id,filePath,title,tags,lastModified,contentHash";
4790
5244
  const rows = docs.map((d) => `"${d.id}","${d.filePath}","${d.title.replace(/"/g, '""')}","${d.tags.join(";")}","${d.lastModified}","${d.contentHash}"`);
@@ -4901,6 +5355,17 @@ function updateStability(currentS, difficulty, currentR) {
4901
5355
  const newS = currentS * (1 + Math.max(0, growth));
4902
5356
  return Math.min(newS, 365);
4903
5357
  }
5358
+ function updateStabilityGraded(currentS, difficulty, currentR, grade) {
5359
+ if (grade === 1) {
5360
+ const postLapse = FSRS_PARAMS.initialStability * 0.2 * Math.pow(difficulty, -FSRS_PARAMS.b);
5361
+ return Math.max(0.1, Math.min(postLapse, currentS));
5362
+ }
5363
+ const good = updateStability(currentS, difficulty, currentR);
5364
+ const baseGrowth = good - currentS;
5365
+ const gradeFactor = grade === 2 ? 0.5 : grade === 4 ? 1.8 : 1;
5366
+ const newS = currentS + baseGrowth * gradeFactor;
5367
+ return Math.min(newS, 365);
5368
+ }
4904
5369
  function estimateInitialStability(contentLength, connectionCount) {
4905
5370
  const base = FSRS_PARAMS.initialStability;
4906
5371
  const sizeBonus = Math.min(contentLength / 1e3, 10) * FSRS_PARAMS.sizeFactor;
@@ -4956,7 +5421,7 @@ var DecayEngine = class {
4956
5421
  if (existing) {
4957
5422
  const elapsed = elapsedDays(existing.last_access, now);
4958
5423
  const currentR = computeRetrievability(existing.stability, elapsed);
4959
- const newS = updateStability(existing.stability, existing.difficulty, currentR);
5424
+ const newS = event.grade ? updateStabilityGraded(existing.stability, existing.difficulty, currentR, event.grade) : updateStability(existing.stability, existing.difficulty, currentR);
4960
5425
  this.db.prepare(`
4961
5426
  UPDATE decay_state SET stability = ?, last_access = ?, retrievability = 1.0, updated_at = ?
4962
5427
  WHERE document_id = ?
@@ -5040,22 +5505,19 @@ var DecayEngine = class {
5040
5505
  * Get documents below decay threshold.
5041
5506
  */
5042
5507
  async getDecaying(threshold = 0.5, limit = 20) {
5043
- await this.computeAll();
5044
5508
  const rows = this.db.prepare(`
5045
5509
  SELECT ds.*, d.title FROM decay_state ds
5046
5510
  JOIN documents d ON d.id = ds.document_id
5047
- WHERE ds.retrievability < ?
5048
- ORDER BY ds.retrievability ASC
5049
- LIMIT ?
5050
- `).all(threshold, limit);
5511
+ `).all();
5512
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5051
5513
  return rows.map((r) => ({
5052
5514
  documentId: r.document_id,
5053
5515
  stability: r.stability,
5054
5516
  difficulty: r.difficulty,
5055
5517
  lastAccess: r.last_access,
5056
- retrievability: r.retrievability,
5518
+ retrievability: computeRetrievability(r.stability, elapsedDays(r.last_access, now)),
5057
5519
  title: r.title
5058
- }));
5520
+ })).filter((r) => r.retrievability < threshold).sort((a, b) => a.retrievability - b.retrievability).slice(0, limit);
5059
5521
  }
5060
5522
  /**
5061
5523
  * Design Ref: §B3.3.3 — read-only live retrievability for a set of documents.
@@ -5277,14 +5739,34 @@ function invalidateGapCache(db) {
5277
5739
  } catch {
5278
5740
  }
5279
5741
  }
5742
+ var EMPTY_REPORT = { totalClusters: 0, totalGaps: 0, gaps: [], isolatedNodes: [] };
5743
+ function readAnyCachedGapReport(db) {
5744
+ try {
5745
+ ensureGapCacheTable(db);
5746
+ const row = db.prepare("SELECT * FROM gap_cache WHERE id = 1").get();
5747
+ if (!row || row.version !== CACHE_VERSION)
5748
+ return null;
5749
+ return JSON.parse(row.payload);
5750
+ } catch {
5751
+ return null;
5752
+ }
5753
+ }
5754
+ function triggerBackgroundCompute(store, db) {
5755
+ void computeAndCacheGaps(store, db).catch((err) => {
5756
+ console.warn("[gap-cache] background recompute failed:", err?.message ?? err);
5757
+ });
5758
+ }
5280
5759
  async function getGapReport(store, db, opts = {}) {
5281
5760
  if (!opts.forceRefresh) {
5282
- const cached = readCachedGapReport(db, opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS);
5283
- if (cached)
5284
- return { report: cached, fromCache: true };
5761
+ const fresh = readCachedGapReport(db, opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS);
5762
+ if (fresh)
5763
+ return { report: fresh, fromCache: true, computing: false };
5285
5764
  }
5286
- const fresh = await computeAndCacheGaps(store, db);
5287
- return { report: fresh, fromCache: false };
5765
+ triggerBackgroundCompute(store, db);
5766
+ const stale = readAnyCachedGapReport(db);
5767
+ if (stale)
5768
+ return { report: stale, fromCache: true, computing: true };
5769
+ return { report: EMPTY_REPORT, fromCache: false, computing: true };
5288
5770
  }
5289
5771
 
5290
5772
  // packages/core/dist/mcp/tools/detect-gaps.js
@@ -5309,7 +5791,7 @@ function createDetectGapsTool(store, getDb) {
5309
5791
  handler: async (args) => {
5310
5792
  const minSeverity = args.minSeverity ?? "medium";
5311
5793
  const db = getDb();
5312
- const { report, fromCache } = await getGapReport(store, db, { forceRefresh: args.forceRefresh });
5794
+ const { report, fromCache, computing } = await getGapReport(store, db, { forceRefresh: args.forceRefresh });
5313
5795
  const sevOrder = { high: 0, medium: 1, low: 2 };
5314
5796
  const threshold = sevOrder[minSeverity] ?? 1;
5315
5797
  const filtered = report.gaps.filter((g) => sevOrder[g.severity] <= threshold);
@@ -5327,8 +5809,8 @@ function createDetectGapsTool(store, getDb) {
5327
5809
  suggestedTopic: g.suggestedTopic
5328
5810
  })),
5329
5811
  isolatedNodes: report.isolatedNodes.slice(0, 10),
5330
- suggestion: filtered.length > 0 ? `${filtered[0].suggestedTopic} \uC8FC\uC81C\uB85C \uB178\uD2B8\uB97C \uC791\uC131\uD558\uBA74 \uC9C0\uC2DD \uAC2D\uC744 \uC904\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.` : "\uD604\uC7AC \uC2EC\uAC01\uD55C \uC9C0\uC2DD \uAC2D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
5331
- cacheStatus: fromCache ? "cached" : "fresh"
5812
+ suggestion: computing ? "\uAC2D \uBD84\uC11D\uC744 \uBC31\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC7AC\uACC4\uC0B0 \uC911\uC785\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uD638\uCD9C\uD558\uBA74 \uCD5C\uC2E0 \uACB0\uACFC\uAC00 \uB098\uC635\uB2C8\uB2E4." : filtered.length > 0 ? `${filtered[0].suggestedTopic} \uC8FC\uC81C\uB85C \uB178\uD2B8\uB97C \uC791\uC131\uD558\uBA74 \uC9C0\uC2DD \uAC2D\uC744 \uC904\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.` : "\uD604\uC7AC \uC2EC\uAC01\uD55C \uC9C0\uC2DD \uAC2D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
5813
+ cacheStatus: computing ? fromCache ? "stale (recomputing in background)" : "computing in background" : fromCache ? "cached" : "fresh"
5332
5814
  }, null, 2)
5333
5815
  }]
5334
5816
  };
@@ -5546,7 +6028,7 @@ function createAskTool(searchEngine, vaultPath) {
5546
6028
  init_wiki_compiler();
5547
6029
  init_config();
5548
6030
  import { resolve as resolve7 } from "node:path";
5549
- import { existsSync as existsSync6 } from "node:fs";
6031
+ import { existsSync as existsSync7 } from "node:fs";
5550
6032
  function createGenerateDraftTool(searchEngine, vaultPath) {
5551
6033
  return {
5552
6034
  name: "generate-draft",
@@ -5589,7 +6071,7 @@ function createGenerateDraftTool(searchEngine, vaultPath) {
5589
6071
  const allDocs = [];
5590
6072
  for (const dir of [folders.fleeting, folders.literature, folders.permanent, folders.wiki]) {
5591
6073
  const fullDir = resolve7(vaultPath, dir);
5592
- if (existsSync6(fullDir)) {
6074
+ if (existsSync7(fullDir)) {
5593
6075
  const docs = scanRawDirectory(fullDir);
5594
6076
  allDocs.push(...docs);
5595
6077
  }
@@ -5645,8 +6127,8 @@ Please write the ${format} draft based on the context above. Save the result to
5645
6127
  }
5646
6128
 
5647
6129
  // packages/core/dist/mcp/tools/agentic-graph.js
5648
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "node:fs";
5649
- import { join as join7, resolve as resolvePath } from "node:path";
6130
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
6131
+ import { join as join8, resolve as resolvePath } from "node:path";
5650
6132
  function createAgenticGraphTools(store, embedder, vaultPath) {
5651
6133
  let nodeCreationCount = 0;
5652
6134
  let nodeCreationWindowStart = Date.now();
@@ -5717,14 +6199,14 @@ function createAgenticGraphTools(store, embedder, vaultPath) {
5717
6199
 
5718
6200
  ${content}${relatedSection}`;
5719
6201
  const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
5720
- const dir = join7(vaultPath, folder);
5721
- const filePath = join7(dir, `${safeTitle}.md`);
6202
+ const dir = join8(vaultPath, folder);
6203
+ const filePath = join8(dir, `${safeTitle}.md`);
5722
6204
  const resolvedPath = resolvePath(filePath);
5723
6205
  const resolvedVault = resolvePath(vaultPath);
5724
6206
  if (!resolvedPath.startsWith(resolvedVault)) {
5725
6207
  return { content: [{ type: "text", text: "Error: invalid folder path." }] };
5726
6208
  }
5727
- mkdirSync7(dir, { recursive: true });
6209
+ mkdirSync8(dir, { recursive: true });
5728
6210
  writeFileSync6(filePath, fullContent, "utf-8");
5729
6211
  const relatedCount = relatedSection ? relatedSection.split("\n").filter((l) => l.startsWith("- ")).length : 0;
5730
6212
  return {
@@ -5759,7 +6241,7 @@ The note will appear in the graph after next index.`
5759
6241
  return { content: [{ type: "text", text: `Source note "${args.sourceTitle}" not found.` }] };
5760
6242
  }
5761
6243
  const { readFileSync: readFileSync19 } = await import("node:fs");
5762
- const fullPath = join7(vaultPath, source.filePath);
6244
+ const fullPath = join8(vaultPath, source.filePath);
5763
6245
  let existing = "";
5764
6246
  try {
5765
6247
  existing = readFileSync19(fullPath, "utf-8");
@@ -5821,6 +6303,14 @@ function createMcpServer(options) {
5821
6303
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
5822
6304
  await ready;
5823
6305
  const { name, arguments: args } = request.params;
6306
+ if (options.onToolCall) {
6307
+ try {
6308
+ const a = args ?? {};
6309
+ const detail = String(a.query ?? a.topic ?? a.title ?? a.documentId ?? a.id ?? "").slice(0, 80);
6310
+ options.onToolCall({ tool: name, detail });
6311
+ } catch {
6312
+ }
6313
+ }
5824
6314
  try {
5825
6315
  let result;
5826
6316
  switch (name) {
@@ -5919,6 +6409,9 @@ function createMcpServer(options) {
5919
6409
  const transport = new StdioServerTransport();
5920
6410
  await server.connect(transport);
5921
6411
  },
6412
+ // T3-3: returns a closable handle ({ port, close }) so an embedding host (the
6413
+ // desktop "Agent Memory" toggle) can stop the server. Backward compatible —
6414
+ // callers that ignore the return value are unaffected.
5922
6415
  async startHttp(port = 3334) {
5923
6416
  const { createServer } = await import("node:http");
5924
6417
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => `sv-${Date.now()}` });
@@ -5944,9 +6437,22 @@ function createMcpServer(options) {
5944
6437
  }
5945
6438
  await transport.handleRequest(req, res);
5946
6439
  });
5947
- httpServer.listen(port, "127.0.0.1", () => {
5948
- console.error(`\u{1F50C} MCP HTTP server running at http://127.0.0.1:${port}/mcp`);
6440
+ await new Promise((resolveListen, rejectListen) => {
6441
+ httpServer.once("error", rejectListen);
6442
+ httpServer.listen(port, "127.0.0.1", () => {
6443
+ httpServer.removeListener("error", rejectListen);
6444
+ console.error(`\u{1F50C} MCP HTTP server running at http://127.0.0.1:${port}/mcp`);
6445
+ resolveListen();
6446
+ });
5949
6447
  });
6448
+ return {
6449
+ port,
6450
+ close: () => new Promise((resolveClose) => {
6451
+ httpServer.close(() => resolveClose());
6452
+ void server.close().catch(() => {
6453
+ });
6454
+ })
6455
+ };
5950
6456
  },
5951
6457
  server
5952
6458
  };
@@ -6074,10 +6580,10 @@ function extractPackTags(chunks) {
6074
6580
  }
6075
6581
 
6076
6582
  // packages/core/dist/pack/exporter.js
6077
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "node:fs";
6583
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "node:fs";
6078
6584
  import { dirname as dirname3 } from "node:path";
6079
6585
  function exportPack(pack2, outputPath) {
6080
- mkdirSync8(dirname3(outputPath), { recursive: true });
6586
+ mkdirSync9(dirname3(outputPath), { recursive: true });
6081
6587
  writeFileSync7(outputPath, JSON.stringify(pack2, null, 2), "utf-8");
6082
6588
  }
6083
6589
  function packToSummary(pack2) {
@@ -6300,10 +6806,10 @@ function createKnowledgeRouter(opts) {
6300
6806
  return;
6301
6807
  }
6302
6808
  const { readFileSync: readFileSync19, writeFileSync: writeFileSync23, unlinkSync } = await import("node:fs");
6303
- const { join: join32, resolve: resolve22 } = await import("node:path");
6809
+ const { join: join33, resolve: resolve22 } = await import("node:path");
6304
6810
  const [keeper, removed] = docA.content.length >= docB.content.length ? [docA, docB] : [docB, docA];
6305
- const keeperPath = resolve22(join32(vaultPath, keeper.filePath));
6306
- const removedPath = resolve22(join32(vaultPath, removed.filePath));
6811
+ const keeperPath = resolve22(join33(vaultPath, keeper.filePath));
6812
+ const removedPath = resolve22(join33(vaultPath, removed.filePath));
6307
6813
  const vaultRoot = resolve22(vaultPath);
6308
6814
  if (!keeperPath.startsWith(vaultRoot) || !removedPath.startsWith(vaultRoot)) {
6309
6815
  res.status(400).json({ error: "Invalid file path" });
@@ -6340,8 +6846,8 @@ ${removed.content}`;
6340
6846
  res.status(400).json({ error: "clusterA, clusterB required" });
6341
6847
  return;
6342
6848
  }
6343
- const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync23 } = await import("node:fs");
6344
- const { join: join32, resolve: resolve22 } = await import("node:path");
6849
+ const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync24 } = await import("node:fs");
6850
+ const { join: join33, resolve: resolve22 } = await import("node:path");
6345
6851
  const nameA = clusterA.replace(/\s*\(\d+\)$/, "");
6346
6852
  const nameB = clusterB.replace(/\s*\(\d+\)$/, "");
6347
6853
  const resultsA = await searchEngine.search({ query: nameA, limit: 3 });
@@ -6381,13 +6887,13 @@ ${removed.content}`;
6381
6887
  ""
6382
6888
  ].join("\n");
6383
6889
  const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ");
6384
- const dir = resolve22(join32(vaultPath, "01_Knowledge"));
6890
+ const dir = resolve22(join33(vaultPath, "01_Knowledge"));
6385
6891
  if (!dir.startsWith(resolve22(vaultPath))) {
6386
6892
  res.status(400).json({ error: "Invalid path" });
6387
6893
  return;
6388
6894
  }
6389
- mkdirSync23(dir, { recursive: true });
6390
- const filePath = join32(dir, `${safeTitle}.md`);
6895
+ mkdirSync24(dir, { recursive: true });
6896
+ const filePath = join33(dir, `${safeTitle}.md`);
6391
6897
  writeFileSync23(filePath, content, "utf-8");
6392
6898
  res.json({ success: true, title: safeTitle, path: filePath });
6393
6899
  } catch (err) {
@@ -6402,7 +6908,7 @@ ${removed.content}`;
6402
6908
  import { Router as Router3 } from "express";
6403
6909
  import { createHash as createHash5 } from "node:crypto";
6404
6910
  function createIngestRouter(opts) {
6405
- const { store, vaultPath, requireAuth, assertNotPrivateUrl } = opts;
6911
+ const { store, vaultPath, requireAuth, assertPublicUrl: assertPublicUrl2 } = opts;
6406
6912
  const router = Router3();
6407
6913
  router.post("/ingest", requireAuth, async (req, res) => {
6408
6914
  try {
@@ -6419,6 +6925,14 @@ function createIngestRouter(opts) {
6419
6925
  let autoTitle = title;
6420
6926
  let autoTags = tags ?? [];
6421
6927
  let autoStage = stage ?? "fleeting";
6928
+ if (input.startsWith("http")) {
6929
+ try {
6930
+ await assertPublicUrl2(input);
6931
+ } catch (e) {
6932
+ res.status(400).json({ error: e.message });
6933
+ return;
6934
+ }
6935
+ }
6422
6936
  const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(input);
6423
6937
  if (isYouTube) {
6424
6938
  try {
@@ -6442,12 +6956,6 @@ function createIngestRouter(opts) {
6442
6956
  }
6443
6957
  }
6444
6958
  } else if (input.startsWith("http")) {
6445
- try {
6446
- assertNotPrivateUrl(input);
6447
- } catch (e) {
6448
- res.status(400).json({ error: e.message });
6449
- return;
6450
- }
6451
6959
  try {
6452
6960
  const resp = await fetch(input, { signal: AbortSignal.timeout(8e3) });
6453
6961
  const html = await resp.text();
@@ -6539,10 +7047,10 @@ function createIngestRouter(opts) {
6539
7047
  }
6540
7048
  try {
6541
7049
  const { writeFileSync: writeFileSync23, unlinkSync } = await import("node:fs");
6542
- const { join: join32 } = await import("node:path");
7050
+ const { join: join33 } = await import("node:path");
6543
7051
  const { tmpdir } = await import("node:os");
6544
7052
  const safeName = (file.originalname ?? "upload").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 100);
6545
- const tmpPath = join32(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
7053
+ const tmpPath = join33(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
6546
7054
  writeFileSync23(tmpPath, file.buffer);
6547
7055
  const { extractFileContent: extractFileContent2 } = await Promise.resolve().then(() => (init_file_extractors(), file_extractors_exports));
6548
7056
  const ext = file.originalname.split(".").pop()?.toLowerCase() ?? "";
@@ -6621,7 +7129,7 @@ function createIngestRouter(opts) {
6621
7129
  return;
6622
7130
  }
6623
7131
  try {
6624
- assertNotPrivateUrl(url);
7132
+ await assertPublicUrl2(url);
6625
7133
  } catch (e) {
6626
7134
  res.status(400).json({ error: e.message });
6627
7135
  return;
@@ -6649,11 +7157,11 @@ ${desc}
6649
7157
  if (content.length > 1e4)
6650
7158
  content = content.slice(0, 1e4) + "\n\n...(truncated)";
6651
7159
  }
6652
- const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync23 } = await import("node:fs");
6653
- const { join: join32 } = await import("node:path");
7160
+ const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync24 } = await import("node:fs");
7161
+ const { join: join33 } = await import("node:path");
6654
7162
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6655
- const clipDir = join32(vaultPath || ".", "06_Research", "clips");
6656
- mkdirSync23(clipDir, { recursive: true });
7163
+ const clipDir = join33(vaultPath || ".", "06_Research", "clips");
7164
+ mkdirSync24(clipDir, { recursive: true });
6657
7165
  const fileName = `${date} ${safeTitle}.md`;
6658
7166
  const md = `---
6659
7167
  title: "${safeTitle}"
@@ -6667,8 +7175,8 @@ tags: [clip${isYT ? ", youtube" : ""}]
6667
7175
  > Source: ${url}
6668
7176
 
6669
7177
  ${content}`;
6670
- writeFileSync23(join32(clipDir, fileName), md, "utf-8");
6671
- res.json({ success: true, fileName, path: join32(clipDir, fileName) });
7178
+ writeFileSync23(join33(clipDir, fileName), md, "utf-8");
7179
+ res.json({ success: true, fileName, path: join33(clipDir, fileName) });
6672
7180
  } catch (err) {
6673
7181
  console.error(err);
6674
7182
  res.status(500).json({ error: "Clip failed" });
@@ -6921,6 +7429,123 @@ function createAnalyticsRouter(opts) {
6921
7429
  return router;
6922
7430
  }
6923
7431
 
7432
+ // packages/core/dist/api/ssrf-guard.js
7433
+ import { lookup } from "node:dns/promises";
7434
+ import { isIP } from "node:net";
7435
+ var defaultResolver = async (host) => {
7436
+ const records = await lookup(host, { all: true });
7437
+ return records.map((r) => r.address);
7438
+ };
7439
+ function ipv4ToInt(ip) {
7440
+ const parts = ip.split(".");
7441
+ if (parts.length !== 4)
7442
+ return null;
7443
+ let value = 0;
7444
+ for (const part of parts) {
7445
+ if (!/^\d{1,3}$/.test(part))
7446
+ return null;
7447
+ const octet = Number(part);
7448
+ if (octet > 255)
7449
+ return null;
7450
+ value = value * 256 + octet;
7451
+ }
7452
+ return value >>> 0;
7453
+ }
7454
+ function isPrivateIpv4(ip) {
7455
+ const value = ipv4ToInt(ip);
7456
+ if (value === null)
7457
+ return false;
7458
+ const a = value >>> 24 & 255;
7459
+ const b = value >>> 16 & 255;
7460
+ if (a === 0 || a === 10 || a === 127)
7461
+ return true;
7462
+ if (a === 169 && b === 254)
7463
+ return true;
7464
+ if (a === 172 && b >= 16 && b <= 31)
7465
+ return true;
7466
+ if (a === 192 && b === 168)
7467
+ return true;
7468
+ if (a === 100 && b >= 64 && b <= 127)
7469
+ return true;
7470
+ return false;
7471
+ }
7472
+ function extractMappedIpv4(ip) {
7473
+ const lower = ip.toLowerCase();
7474
+ const idx = lower.lastIndexOf("::ffff:");
7475
+ if (idx === -1)
7476
+ return null;
7477
+ const tail = lower.slice(idx + "::ffff:".length);
7478
+ if (isIP(tail) === 4)
7479
+ return tail;
7480
+ const hexMatch = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
7481
+ if (hexMatch) {
7482
+ const high = parseInt(hexMatch[1], 16);
7483
+ const low = parseInt(hexMatch[2], 16);
7484
+ return [high >> 8 & 255, high & 255, low >> 8 & 255, low & 255].join(".");
7485
+ }
7486
+ return null;
7487
+ }
7488
+ function isPrivateIp(ip) {
7489
+ const kind = isIP(ip);
7490
+ if (kind === 4)
7491
+ return isPrivateIpv4(ip);
7492
+ if (kind !== 6) {
7493
+ return true;
7494
+ }
7495
+ const lower = ip.toLowerCase();
7496
+ const mapped = extractMappedIpv4(lower);
7497
+ if (mapped !== null)
7498
+ return isPrivateIpv4(mapped);
7499
+ if (lower === "::" || lower === "0:0:0:0:0:0:0:0")
7500
+ return true;
7501
+ if (lower === "::1" || lower === "0:0:0:0:0:0:0:1")
7502
+ return true;
7503
+ const firstHextet = parseInt(lower.split(":")[0] || "0", 16);
7504
+ if (firstHextet >= 65152 && firstHextet <= 65215)
7505
+ return true;
7506
+ if (firstHextet >= 64512 && firstHextet <= 65023)
7507
+ return true;
7508
+ return false;
7509
+ }
7510
+ async function assertPublicUrl(url, resolver = defaultResolver) {
7511
+ let parsed;
7512
+ try {
7513
+ parsed = new URL(url);
7514
+ } catch {
7515
+ throw new Error("Internal or non-public URL not allowed");
7516
+ }
7517
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
7518
+ throw new Error("Only http/https URLs allowed");
7519
+ }
7520
+ let host = parsed.hostname;
7521
+ if (host.startsWith("[") && host.endsWith("]")) {
7522
+ host = host.slice(1, -1);
7523
+ }
7524
+ const lowerHost = host.toLowerCase();
7525
+ const h = lowerHost.replace(/\.$/, "");
7526
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local")) {
7527
+ throw new Error("Internal or non-public URL not allowed");
7528
+ }
7529
+ let addresses;
7530
+ if (isIP(host) !== 0) {
7531
+ addresses = [host];
7532
+ } else {
7533
+ try {
7534
+ addresses = await resolver(host);
7535
+ } catch {
7536
+ throw new Error("Internal or non-public URL not allowed");
7537
+ }
7538
+ }
7539
+ if (addresses.length === 0) {
7540
+ throw new Error("Internal or non-public URL not allowed");
7541
+ }
7542
+ for (const addr of addresses) {
7543
+ if (isPrivateIp(addr)) {
7544
+ throw new Error("Internal or non-public URL not allowed");
7545
+ }
7546
+ }
7547
+ }
7548
+
6924
7549
  // packages/core/dist/api/server.js
6925
7550
  function createApiServer(options) {
6926
7551
  const { store, searchEngine, port = 3333, vaultName = "", vaultPath = "", decayEngine, graphUiPath } = options;
@@ -6976,32 +7601,45 @@ function createApiServer(options) {
6976
7601
  }
6977
7602
  res.json({ token: authToken });
6978
7603
  });
6979
- function assertNotPrivateUrl(url) {
6980
- const parsed = new URL(url);
6981
- if (!["http:", "https:"].includes(parsed.protocol))
6982
- throw new Error("Only http/https URLs allowed");
6983
- const host = parsed.hostname.toLowerCase();
6984
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.startsWith("192.168.") || host.startsWith("10.") || // Fix: 172.16.0.0/12 covers 172.16.* through 172.31.*
6985
- /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.startsWith("169.254.") || host === "0.0.0.0") {
6986
- throw new Error("Internal URLs not allowed");
6987
- }
6988
- }
6989
7604
  if (graphUiPath) {
6990
7605
  app.use(express.static(graphUiPath, { index: "index.html", extensions: ["html"] }));
6991
7606
  }
6992
7607
  const GRAPH_CACHE_TTL = 5 * 60 * 1e3;
6993
7608
  const graphCaches = /* @__PURE__ */ new Map();
7609
+ function parseView(q) {
7610
+ return q === "raw" ? "raw" : "cluster";
7611
+ }
7612
+ function parseMode(q) {
7613
+ return q === "folder" ? "folder" : "semantic";
7614
+ }
7615
+ function clampCap(view, raw) {
7616
+ if (!Number.isFinite(raw) || raw <= 0)
7617
+ return void 0;
7618
+ const max = view === "raw" ? 4e3 : 6e3;
7619
+ const rounded = Math.round(raw / 1e3) * 1e3;
7620
+ return Math.max(1e3, Math.min(max, rounded));
7621
+ }
7622
+ async function buildGraphEntry(view, mode, cap) {
7623
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7624
+ if (view === "cluster") {
7625
+ const g = await buildClusteredGraph(store, { mode, clusterCap: cap });
7626
+ return { data: flattenClusterLevel(g.clusterLevel), clustered: g, generatedAt: now, cachedAt: Date.now() };
7627
+ }
7628
+ const data = await buildGraphData(store, { mode, nodeCap: cap });
7629
+ return { data, generatedAt: now, cachedAt: Date.now() };
7630
+ }
6994
7631
  app.get("/api/graph", async (req, res) => {
6995
7632
  try {
6996
- const mode = req.query.mode === "folder" ? "folder" : "semantic";
6997
- const cached = graphCaches.get(mode);
7633
+ const view = parseView(req.query.view);
7634
+ const mode = parseMode(req.query.mode);
7635
+ const cap = clampCap(view, Number(req.query.cap));
7636
+ const cacheKey = `${view}:${mode}:${cap ?? ""}`;
7637
+ const cached = graphCaches.get(cacheKey);
6998
7638
  if (!cached || Date.now() - cached.cachedAt > GRAPH_CACHE_TTL) {
6999
- const data = await buildGraphData(store, { mode });
7000
- const now = (/* @__PURE__ */ new Date()).toISOString();
7001
- graphCaches.set(mode, { data, generatedAt: now, cachedAt: Date.now() });
7639
+ graphCaches.set(cacheKey, await buildGraphEntry(view, mode, cap));
7002
7640
  }
7003
- const entry = graphCaches.get(mode);
7004
- res.json({ data: entry.data, generatedAt: entry.generatedAt, mode });
7641
+ const entry = graphCaches.get(cacheKey);
7642
+ res.json({ data: entry.data, generatedAt: entry.generatedAt, mode, view });
7005
7643
  } catch (err) {
7006
7644
  console.error(err);
7007
7645
  res.status(500).json({ error: "Internal server error" });
@@ -7009,11 +7647,34 @@ function createApiServer(options) {
7009
7647
  });
7010
7648
  app.get("/api/graph/refresh", async (req, res) => {
7011
7649
  try {
7012
- const mode = req.query.mode === "folder" ? "folder" : "semantic";
7013
- const data = await buildGraphData(store, { mode });
7014
- const now = (/* @__PURE__ */ new Date()).toISOString();
7015
- graphCaches.set(mode, { data, generatedAt: now, cachedAt: Date.now() });
7016
- res.json({ data, generatedAt: now, mode });
7650
+ const view = parseView(req.query.view);
7651
+ const mode = parseMode(req.query.mode);
7652
+ const cap = clampCap(view, Number(req.query.cap));
7653
+ const cacheKey = `${view}:${mode}:${cap ?? ""}`;
7654
+ const entry = await buildGraphEntry(view, mode, cap);
7655
+ graphCaches.set(cacheKey, entry);
7656
+ res.json({ data: entry.data, generatedAt: entry.generatedAt, mode, view });
7657
+ } catch (err) {
7658
+ console.error(err);
7659
+ res.status(500).json({ error: "Internal server error" });
7660
+ }
7661
+ });
7662
+ app.get("/api/graph/cluster/:id", async (req, res) => {
7663
+ try {
7664
+ const mode = parseMode(req.query.mode);
7665
+ const cap = clampCap("cluster", Number(req.query.cap));
7666
+ const cacheKey = `cluster:${mode}:${cap ?? ""}`;
7667
+ let cached = graphCaches.get(cacheKey);
7668
+ if (!cached || !cached.clustered || Date.now() - cached.cachedAt > GRAPH_CACHE_TTL) {
7669
+ cached = await buildGraphEntry("cluster", mode, cap);
7670
+ graphCaches.set(cacheKey, cached);
7671
+ }
7672
+ const members = cached.clustered.members.get(Number(req.params.id));
7673
+ if (!members) {
7674
+ res.status(404).json({ error: "Cluster not found" });
7675
+ return;
7676
+ }
7677
+ res.json({ data: members });
7017
7678
  } catch (err) {
7018
7679
  console.error(err);
7019
7680
  res.status(500).json({ error: "Internal server error" });
@@ -7156,7 +7817,7 @@ function createApiServer(options) {
7156
7817
  res.status(404).json({ error: "Document not found" });
7157
7818
  return;
7158
7819
  }
7159
- const { resolve: resolve22, join: join32 } = await import("node:path");
7820
+ const { resolve: resolve22, join: join33 } = await import("node:path");
7160
7821
  const { writeFileSync: writeFileSync23, readFileSync: readFileSync19 } = await import("node:fs");
7161
7822
  const fullPath = resolve22(vaultPath, doc.filePath);
7162
7823
  if (!fullPath.startsWith(resolve22(vaultPath))) {
@@ -7207,13 +7868,13 @@ function createApiServer(options) {
7207
7868
  return;
7208
7869
  }
7209
7870
  const { resolve: resolve22 } = await import("node:path");
7210
- const { unlinkSync, existsSync: existsSync27 } = await import("node:fs");
7871
+ const { unlinkSync, existsSync: existsSync28 } = await import("node:fs");
7211
7872
  const fullPath = resolve22(vaultPath, doc.filePath);
7212
7873
  if (!fullPath.startsWith(resolve22(vaultPath))) {
7213
7874
  res.status(403).json({ error: "Access denied" });
7214
7875
  return;
7215
7876
  }
7216
- if (existsSync27(fullPath)) {
7877
+ if (existsSync28(fullPath)) {
7217
7878
  unlinkSync(fullPath);
7218
7879
  }
7219
7880
  await store.deleteByDocumentId(id);
@@ -7242,7 +7903,7 @@ function createApiServer(options) {
7242
7903
  res.status(500).json({ error: "Ask failed" });
7243
7904
  }
7244
7905
  });
7245
- app.use("/api", createIngestRouter({ store, vaultPath, requireAuth, assertNotPrivateUrl }));
7906
+ app.use("/api", createIngestRouter({ store, vaultPath, requireAuth, assertPublicUrl }));
7246
7907
  app.get("/api/recent", async (_req, res) => {
7247
7908
  try {
7248
7909
  const docs = await store.getAllDocuments();
@@ -7339,12 +8000,12 @@ function createApiServer(options) {
7339
8000
  const { resolve: resolve22 } = await import("node:path");
7340
8001
  const syncScript = resolve22(process.cwd(), "packages/sync/sync-to-obsidian.mjs");
7341
8002
  const syncDir = resolve22(process.cwd(), "packages/sync");
7342
- const { existsSync: existsSync27 } = await import("node:fs");
7343
- if (!existsSync27(syncScript)) {
8003
+ const { existsSync: existsSync28 } = await import("node:fs");
8004
+ if (!existsSync28(syncScript)) {
7344
8005
  res.json({ success: false, error: "sync script not found" });
7345
8006
  return;
7346
8007
  }
7347
- if (!existsSync27(resolve22(syncDir, ".env"))) {
8008
+ if (!existsSync28(resolve22(syncDir, ".env"))) {
7348
8009
  res.json({ success: false, error: ".env not found" });
7349
8010
  return;
7350
8011
  }
@@ -7393,6 +8054,7 @@ function createApiServer(options) {
7393
8054
  }
7394
8055
 
7395
8056
  // packages/core/dist/index.js
8057
+ init_graph_data();
7396
8058
  init_duplicate_detector();
7397
8059
  init_gap_detector();
7398
8060
 
@@ -7511,6 +8173,7 @@ function cosineSim(a, b) {
7511
8173
  // packages/core/dist/index.js
7512
8174
  init_ask_engine();
7513
8175
  init_wiki_compiler();
8176
+ init_auto_linker();
7514
8177
 
7515
8178
  // packages/core/dist/intelligence/knowledge-lint.js
7516
8179
  init_gap_detector();
@@ -7632,18 +8295,30 @@ async function lintKnowledge(store) {
7632
8295
  init_zettelkasten();
7633
8296
  init_ingest_pipeline();
7634
8297
 
8298
+ // packages/core/dist/intelligence/classify/classify.js
8299
+ init_math();
8300
+
8301
+ // packages/core/dist/intelligence/classify/cluster.js
8302
+ init_math();
8303
+
8304
+ // packages/core/dist/intelligence/classify/discover.js
8305
+ init_math();
8306
+
8307
+ // packages/core/dist/index.js
8308
+ init_file_extractors();
8309
+
7635
8310
  // packages/core/dist/multi-vault/index.js
7636
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync12 } from "node:fs";
7637
- import { join as join13 } from "node:path";
7638
- import { homedir as homedir6 } from "node:os";
7639
- var VAULTS_FILE = join13(homedir6(), ".stellavault", "vaults.json");
8311
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync13, mkdirSync as mkdirSync13 } from "node:fs";
8312
+ import { join as join14 } from "node:path";
8313
+ import { homedir as homedir7 } from "node:os";
8314
+ var VAULTS_FILE = join14(homedir7(), ".stellavault", "vaults.json");
7640
8315
  function loadVaults() {
7641
- if (!existsSync12(VAULTS_FILE))
8316
+ if (!existsSync13(VAULTS_FILE))
7642
8317
  return [];
7643
8318
  return JSON.parse(readFileSync13(VAULTS_FILE, "utf-8"));
7644
8319
  }
7645
8320
  function saveVaults(vaults) {
7646
- mkdirSync12(join13(homedir6(), ".stellavault"), { recursive: true });
8321
+ mkdirSync13(join14(homedir7(), ".stellavault"), { recursive: true });
7647
8322
  writeFileSync12(VAULTS_FILE, JSON.stringify(vaults, null, 2), "utf-8");
7648
8323
  }
7649
8324
  function addVault(id, name, vaultPath, dbPath, shared = false) {
@@ -7674,7 +8349,7 @@ async function searchAllVaults(query, embedder, createStore, options = {}) {
7674
8349
  const embedding = await embedder.embed(query);
7675
8350
  const searches = vaults.map(async (vault2) => {
7676
8351
  try {
7677
- if (!existsSync12(vault2.dbPath))
8352
+ if (!existsSync13(vault2.dbPath))
7678
8353
  return;
7679
8354
  const store = createStore(vault2.dbPath);
7680
8355
  await store.initialize();
@@ -7703,8 +8378,8 @@ async function searchAllVaults(query, embedder, createStore, options = {}) {
7703
8378
 
7704
8379
  // packages/core/dist/capture/voice.js
7705
8380
  import { execFileSync, execSync } from "node:child_process";
7706
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync13 } from "node:fs";
7707
- import { join as join14, basename as basename4 } from "node:path";
8381
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, existsSync as existsSync14 } from "node:fs";
8382
+ import { join as join15, basename as basename4 } from "node:path";
7708
8383
  var ALLOWED_MODELS = ["tiny", "base", "small", "medium", "large"];
7709
8384
  var ALLOWED_LANGUAGES = ["auto", "en", "ko", "ja", "zh", "es", "fr", "de", "it", "pt", "ru", "ar", "hi"];
7710
8385
  function validateModel(model) {
@@ -7727,7 +8402,7 @@ function isWhisperAvailable() {
7727
8402
  }
7728
8403
  async function transcribeAudio(audioPath, options = {}) {
7729
8404
  const { model = "base", language } = options;
7730
- if (!existsSync13(audioPath)) {
8405
+ if (!existsSync14(audioPath)) {
7731
8406
  throw new Error(`Audio file not found: ${audioPath}`);
7732
8407
  }
7733
8408
  if (isWhisperAvailable()) {
@@ -7735,12 +8410,12 @@ async function transcribeAudio(audioPath, options = {}) {
7735
8410
  const args = [audioPath, "--model", safeModel, "--output_format", "txt", "--output_dir", "/tmp/sv-whisper"];
7736
8411
  if (language)
7737
8412
  args.push("--language", validateLanguage(language));
7738
- mkdirSync13("/tmp/sv-whisper", { recursive: true });
8413
+ mkdirSync14("/tmp/sv-whisper", { recursive: true });
7739
8414
  try {
7740
8415
  execFileSync("whisper", args, { stdio: "pipe", timeout: 3e5 });
7741
8416
  const outputName = basename4(audioPath).replace(/\.[^.]+$/, ".txt");
7742
8417
  const { readFileSync: readFileSync19 } = await import("node:fs");
7743
- return readFileSync19(join14("/tmp/sv-whisper", outputName), "utf-8").trim();
8418
+ return readFileSync19(join15("/tmp/sv-whisper", outputName), "utf-8").trim();
7744
8419
  } catch (err) {
7745
8420
  throw new Error(`Whisper failed: ${err instanceof Error ? err.message : err}`);
7746
8421
  }
@@ -7797,9 +8472,9 @@ async function captureVoice(audioPath, options) {
7797
8472
  transcript,
7798
8473
  ""
7799
8474
  ].join("\n");
7800
- const dir = join14(vaultPath, folder);
7801
- mkdirSync13(dir, { recursive: true });
7802
- const filePath = join14(dir, `${date.slice(0, 10)} ${safeTitle}.md`);
8475
+ const dir = join15(vaultPath, folder);
8476
+ mkdirSync14(dir, { recursive: true });
8477
+ const filePath = join15(dir, `${date.slice(0, 10)} ${safeTitle}.md`);
7803
8478
  writeFileSync13(filePath, content, "utf-8");
7804
8479
  return {
7805
8480
  title: safeTitle,
@@ -7822,12 +8497,12 @@ async function captureVoice(audioPath, options) {
7822
8497
 
7823
8498
  // packages/core/dist/cloud/sync.js
7824
8499
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, createHash as createHash6 } from "node:crypto";
7825
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync14, chmodSync as chmodSync2 } from "node:fs";
7826
- import { join as join15 } from "node:path";
7827
- import { homedir as homedir7 } from "node:os";
7828
- var CLOUD_DIR = join15(homedir7(), ".stellavault", "cloud");
7829
- var KEY_FILE = join15(CLOUD_DIR, "encryption.key");
7830
- var SYNC_STATE_FILE = join15(CLOUD_DIR, "sync-state.json");
8500
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync15, mkdirSync as mkdirSync15, chmodSync as chmodSync2 } from "node:fs";
8501
+ import { join as join16 } from "node:path";
8502
+ import { homedir as homedir8 } from "node:os";
8503
+ var CLOUD_DIR = join16(homedir8(), ".stellavault", "cloud");
8504
+ var KEY_FILE = join16(CLOUD_DIR, "encryption.key");
8505
+ var SYNC_STATE_FILE = join16(CLOUD_DIR, "sync-state.json");
7831
8506
  function encrypt(data, key) {
7832
8507
  const iv = randomBytes4(16);
7833
8508
  const cipher = createCipheriv("aes-256-gcm", key, iv);
@@ -7841,13 +8516,13 @@ function decrypt(encrypted, key, iv, tag) {
7841
8516
  return Buffer.concat([decipher.update(encrypted), decipher.final()]);
7842
8517
  }
7843
8518
  function getOrCreateEncryptionKey(userKey) {
7844
- mkdirSync14(CLOUD_DIR, { recursive: true });
8519
+ mkdirSync15(CLOUD_DIR, { recursive: true });
7845
8520
  if (userKey) {
7846
8521
  const key2 = createHash6("sha256").update(userKey).digest();
7847
8522
  writeFileSync14(KEY_FILE, key2.toString("hex"), "utf-8");
7848
8523
  return key2;
7849
8524
  }
7850
- if (existsSync14(KEY_FILE)) {
8525
+ if (existsSync15(KEY_FILE)) {
7851
8526
  return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
7852
8527
  }
7853
8528
  const key = randomBytes4(32);
@@ -7897,7 +8572,7 @@ async function s3Get(config, objectKey) {
7897
8572
  async function syncToCloud(dbPath, config) {
7898
8573
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7899
8574
  try {
7900
- if (!existsSync14(dbPath)) {
8575
+ if (!existsSync15(dbPath)) {
7901
8576
  return { action: "upload", dbSize: 0, encryptedSize: 0, timestamp, success: false, error: "DB not found" };
7902
8577
  }
7903
8578
  const dbData = readFileSync14(dbPath);
@@ -7907,7 +8582,7 @@ async function syncToCloud(dbPath, config) {
7907
8582
  const objectKey = `stellavault/index.db.enc`;
7908
8583
  const success = await s3Put(config, objectKey, payload);
7909
8584
  const state = { lastSync: timestamp, dbSize: dbData.length, encryptedSize: payload.length, objectKey };
7910
- mkdirSync14(CLOUD_DIR, { recursive: true });
8585
+ mkdirSync15(CLOUD_DIR, { recursive: true });
7911
8586
  writeFileSync14(SYNC_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
7912
8587
  return { action: "upload", dbSize: dbData.length, encryptedSize: payload.length, timestamp, success };
7913
8588
  } catch (err) {
@@ -7927,7 +8602,7 @@ async function restoreFromCloud(dbPath, config) {
7927
8602
  const tag = payload.subarray(16, 32);
7928
8603
  const encrypted = payload.subarray(32);
7929
8604
  const dbData = decrypt(encrypted, key, iv, tag);
7930
- if (existsSync14(dbPath)) {
8605
+ if (existsSync15(dbPath)) {
7931
8606
  writeFileSync14(dbPath + ".backup", readFileSync14(dbPath));
7932
8607
  }
7933
8608
  writeFileSync14(dbPath, dbData);
@@ -7937,40 +8612,40 @@ async function restoreFromCloud(dbPath, config) {
7937
8612
  }
7938
8613
  }
7939
8614
  function getSyncState() {
7940
- if (!existsSync14(SYNC_STATE_FILE))
8615
+ if (!existsSync15(SYNC_STATE_FILE))
7941
8616
  return null;
7942
8617
  return JSON.parse(readFileSync14(SYNC_STATE_FILE, "utf-8"));
7943
8618
  }
7944
8619
 
7945
8620
  // packages/core/dist/team/index.js
7946
- import { join as join16 } from "node:path";
7947
- import { homedir as homedir8 } from "node:os";
7948
- var TEAM_DIR = join16(homedir8(), ".stellavault", "team");
7949
- var TEAM_FILE = join16(TEAM_DIR, "team.json");
8621
+ import { join as join17 } from "node:path";
8622
+ import { homedir as homedir9 } from "node:os";
8623
+ var TEAM_DIR = join17(homedir9(), ".stellavault", "team");
8624
+ var TEAM_FILE = join17(TEAM_DIR, "team.json");
7950
8625
 
7951
8626
  // packages/core/dist/index.js
7952
8627
  init_federation();
7953
8628
 
7954
8629
  // packages/core/dist/federation/trust.js
7955
- import { join as join17 } from "node:path";
7956
- import { homedir as homedir9 } from "node:os";
7957
- var TRUST_FILE = join17(homedir9(), ".stellavault", "federation", "trust.json");
8630
+ import { join as join18 } from "node:path";
8631
+ import { homedir as homedir10 } from "node:os";
8632
+ var TRUST_FILE = join18(homedir10(), ".stellavault", "federation", "trust.json");
7958
8633
 
7959
8634
  // packages/core/dist/index.js
7960
8635
  init_sharing();
7961
8636
 
7962
8637
  // packages/core/dist/federation/reputation.js
7963
- import { join as join18 } from "node:path";
7964
- import { homedir as homedir10 } from "node:os";
7965
- var REP_FILE = join18(homedir10(), ".stellavault", "federation", "reputation.json");
8638
+ import { join as join19 } from "node:path";
8639
+ import { homedir as homedir11 } from "node:os";
8640
+ var REP_FILE = join19(homedir11(), ".stellavault", "federation", "reputation.json");
7966
8641
 
7967
8642
  // packages/core/dist/index.js
7968
8643
  init_privacy();
7969
8644
 
7970
8645
  // packages/core/dist/federation/credits.js
7971
- import { join as join19 } from "node:path";
7972
- import { homedir as homedir11 } from "node:os";
7973
- var CREDITS_FILE = join19(homedir11(), ".stellavault", "federation", "credits.json");
8646
+ import { join as join20 } from "node:path";
8647
+ import { homedir as homedir12 } from "node:os";
8648
+ var CREDITS_FILE = join20(homedir12(), ".stellavault", "federation", "credits.json");
7974
8649
 
7975
8650
  // packages/core/dist/index.js
7976
8651
  init_retry();
@@ -8006,15 +8681,15 @@ function createKnowledgeHub(config, options = {}) {
8006
8681
  // B2.2 — cross-lingual/synonym groups
8007
8682
  });
8008
8683
  const mcpServer = createMcpServer({ store, searchEngine, vaultPath: config.vaultPath, ready: options.ready });
8009
- return { store, embedder, searchEngine, mcpServer, config };
8684
+ return { store, embedder, searchEngine, mcpServer, config, getDecayEngine };
8010
8685
  }
8011
8686
 
8012
8687
  // packages/cli/dist/commands/index-cmd.js
8013
8688
  function getVaultDbPath(vaultPath) {
8014
8689
  const hash = createHash7("sha256").update(vaultPath).digest("hex").slice(0, 8);
8015
- const dir = join20(homedir12(), ".stellavault", "vaults");
8016
- mkdirSync15(dir, { recursive: true });
8017
- return join20(dir, `${hash}.db`);
8690
+ const dir = join21(homedir13(), ".stellavault", "vaults");
8691
+ mkdirSync16(dir, { recursive: true });
8692
+ return join21(dir, `${hash}.db`);
8018
8693
  }
8019
8694
  function resolveDbPath(vault2, configDbPath) {
8020
8695
  const envDbPath = process.env.STELLAVAULT_DB_PATH?.trim();
@@ -8305,19 +8980,19 @@ async function statusCommand(_opts, cmd) {
8305
8980
  import chalk6 from "chalk";
8306
8981
  import { spawn } from "node:child_process";
8307
8982
  import { resolve as resolve11, dirname as dirname5 } from "node:path";
8308
- import { existsSync as existsSync17 } from "node:fs";
8983
+ import { existsSync as existsSync18 } from "node:fs";
8309
8984
  import { fileURLToPath } from "node:url";
8310
8985
  function locateBundledGraphUi() {
8311
8986
  try {
8312
8987
  const here = dirname5(fileURLToPath(import.meta.url));
8313
8988
  const bundled = resolve11(here, "graph-ui");
8314
- if (existsSync17(resolve11(bundled, "index.html")))
8989
+ if (existsSync18(resolve11(bundled, "index.html")))
8315
8990
  return bundled;
8316
8991
  const monorepoGraphDist = resolve11(here, "..", "..", "..", "graph", "dist");
8317
- if (existsSync17(resolve11(monorepoGraphDist, "index.html")))
8992
+ if (existsSync18(resolve11(monorepoGraphDist, "index.html")))
8318
8993
  return monorepoGraphDist;
8319
8994
  const singleFileBundle = resolve11(here, "..", "dist", "graph-ui");
8320
- if (existsSync17(resolve11(singleFileBundle, "index.html")))
8995
+ if (existsSync18(resolve11(singleFileBundle, "index.html")))
8321
8996
  return singleFileBundle;
8322
8997
  } catch {
8323
8998
  }
@@ -8368,7 +9043,7 @@ async function graphCommand() {
8368
9043
  return;
8369
9044
  }
8370
9045
  const devGraphDir = resolve11(process.cwd(), "packages/graph");
8371
- const hasDevGraph = existsSync17(resolve11(devGraphDir, "package.json"));
9046
+ const hasDevGraph = existsSync18(resolve11(devGraphDir, "package.json"));
8372
9047
  if (hasDevGraph) {
8373
9048
  console.error(chalk6.dim(" \u{1F680} Starting Vite dev server..."));
8374
9049
  const vite = spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["vite", "--host"], {
@@ -8430,10 +9105,10 @@ async function cardCommand(options) {
8430
9105
 
8431
9106
  // packages/cli/dist/commands/pack-cmd.js
8432
9107
  import chalk8 from "chalk";
8433
- import { resolve as resolve13, join as join23 } from "node:path";
8434
- import { readdirSync as readdirSync6, existsSync as existsSync18, readFileSync as readFileSync16, mkdirSync as mkdirSync17 } from "node:fs";
8435
- import { homedir as homedir15 } from "node:os";
8436
- var PACKS_DIR = join23(homedir15(), ".stellavault", "packs");
9108
+ import { resolve as resolve13, join as join24 } from "node:path";
9109
+ import { readdirSync as readdirSync6, existsSync as existsSync19, readFileSync as readFileSync16, mkdirSync as mkdirSync18 } from "node:fs";
9110
+ import { homedir as homedir16 } from "node:os";
9111
+ var PACKS_DIR = join24(homedir16(), ".stellavault", "packs");
8437
9112
  async function packCreateCommand(name, options) {
8438
9113
  const config = loadConfig();
8439
9114
  const hub = createKnowledgeHub(config);
@@ -8449,8 +9124,8 @@ async function packCreateCommand(name, options) {
8449
9124
  description: options.description,
8450
9125
  limit: options.limit ? parseInt(options.limit) : 100
8451
9126
  });
8452
- mkdirSync17(PACKS_DIR, { recursive: true });
8453
- const outPath = join23(PACKS_DIR, `${name}.sv-pack`);
9127
+ mkdirSync18(PACKS_DIR, { recursive: true });
9128
+ const outPath = join24(PACKS_DIR, `${name}.sv-pack`);
8454
9129
  exportPack(pack2, outPath);
8455
9130
  console.error(chalk8.green(`\u2705 Pack created: ${name}`));
8456
9131
  console.error(` \u{1F4E6} ${pack2.chunks.length} chunks`);
@@ -8461,8 +9136,8 @@ async function packCreateCommand(name, options) {
8461
9136
  await hub.store.close();
8462
9137
  }
8463
9138
  async function packExportCommand(name, options) {
8464
- const srcPath = join23(PACKS_DIR, `${name}.sv-pack`);
8465
- if (!existsSync18(srcPath)) {
9139
+ const srcPath = join24(PACKS_DIR, `${name}.sv-pack`);
9140
+ if (!existsSync19(srcPath)) {
8466
9141
  console.error(chalk8.red(`\u274C Pack not found: ${name}`));
8467
9142
  process.exit(1);
8468
9143
  }
@@ -8474,7 +9149,7 @@ async function packExportCommand(name, options) {
8474
9149
  }
8475
9150
  async function packImportCommand(filePath) {
8476
9151
  const absPath = resolve13(process.cwd(), filePath);
8477
- if (!existsSync18(absPath)) {
9152
+ if (!existsSync19(absPath)) {
8478
9153
  console.error(chalk8.red(`\u274C File not found: ${absPath}`));
8479
9154
  process.exit(1);
8480
9155
  }
@@ -8493,7 +9168,7 @@ async function packImportCommand(filePath) {
8493
9168
  await hub.store.close();
8494
9169
  }
8495
9170
  async function packListCommand() {
8496
- mkdirSync17(PACKS_DIR, { recursive: true });
9171
+ mkdirSync18(PACKS_DIR, { recursive: true });
8497
9172
  const files = readdirSync6(PACKS_DIR).filter((f) => f.endsWith(".sv-pack"));
8498
9173
  if (files.length === 0) {
8499
9174
  console.error(chalk8.dim("No packs found. Create one: stellavault pack create <name> --from-search <query>"));
@@ -8503,7 +9178,7 @@ async function packListCommand() {
8503
9178
  `));
8504
9179
  for (const file of files) {
8505
9180
  try {
8506
- const pack2 = JSON.parse(readFileSync16(join23(PACKS_DIR, file), "utf-8"));
9181
+ const pack2 = JSON.parse(readFileSync16(join24(PACKS_DIR, file), "utf-8"));
8507
9182
  console.error(` ${chalk8.bold(pack2.name)} v${pack2.version} \u2014 ${pack2.chunks.length} chunks (${pack2.license})`);
8508
9183
  } catch {
8509
9184
  console.error(` ${file} (invalid)`);
@@ -8511,8 +9186,8 @@ async function packListCommand() {
8511
9186
  }
8512
9187
  }
8513
9188
  async function packInfoCommand(name) {
8514
- const filePath = join23(PACKS_DIR, `${name}.sv-pack`);
8515
- if (!existsSync18(filePath)) {
9189
+ const filePath = join24(PACKS_DIR, `${name}.sv-pack`);
9190
+ if (!existsSync19(filePath)) {
8516
9191
  console.error(chalk8.red(`\u274C Pack not found: ${name}`));
8517
9192
  process.exit(1);
8518
9193
  }
@@ -8570,24 +9245,24 @@ async function decayCommand(_opts, cmd) {
8570
9245
  import chalk10 from "chalk";
8571
9246
  import { spawn as spawn2 } from "node:child_process";
8572
9247
  import { resolve as resolve14 } from "node:path";
8573
- import { existsSync as existsSync19 } from "node:fs";
9248
+ import { existsSync as existsSync20 } from "node:fs";
8574
9249
  async function syncCommand(options) {
8575
9250
  const syncDir = resolve14(process.cwd(), "packages/sync");
8576
9251
  const syncScript = resolve14(syncDir, "sync-to-obsidian.mjs");
8577
- if (!existsSync19(syncScript)) {
9252
+ if (!existsSync20(syncScript)) {
8578
9253
  console.error(chalk10.red("\u274C packages/sync/sync-to-obsidian.mjs not found"));
8579
9254
  console.error(chalk10.dim(" Run from project root: cd notion-obsidian-sync"));
8580
9255
  process.exit(1);
8581
9256
  }
8582
9257
  const envFile = resolve14(syncDir, ".env");
8583
- if (!existsSync19(envFile)) {
9258
+ if (!existsSync20(envFile)) {
8584
9259
  console.error(chalk10.red("\u274C packages/sync/.env not found"));
8585
9260
  console.error(chalk10.dim(" Copy .env.example \u2192 .env and set NOTION_TOKEN"));
8586
9261
  process.exit(1);
8587
9262
  }
8588
9263
  if (options.upload) {
8589
9264
  const uploadScript = resolve14(syncDir, "upload-pdca-to-notion.mjs");
8590
- if (!existsSync19(uploadScript)) {
9265
+ if (!existsSync20(uploadScript)) {
8591
9266
  console.error(chalk10.red("\u274C upload-pdca-to-notion.mjs not found"));
8592
9267
  process.exit(1);
8593
9268
  }
@@ -8839,8 +9514,8 @@ async function gapsCommand() {
8839
9514
 
8840
9515
  // packages/cli/dist/commands/clip-cmd.js
8841
9516
  import chalk14 from "chalk";
8842
- import { writeFileSync as writeFileSync18, mkdirSync as mkdirSync18 } from "node:fs";
8843
- import { join as join24 } from "node:path";
9517
+ import { writeFileSync as writeFileSync18, mkdirSync as mkdirSync19 } from "node:fs";
9518
+ import { join as join25 } from "node:path";
8844
9519
  async function clipCommand(url, options) {
8845
9520
  if (!url) {
8846
9521
  console.error(chalk14.red("\u274C Please provide a URL: stellavault clip <url>"));
@@ -8853,9 +9528,15 @@ async function clipCommand(url, options) {
8853
9528
  process.exit(1);
8854
9529
  }
8855
9530
  const folder = options.folder ?? "06_Research/clips";
8856
- const targetDir = join24(vaultPath, folder);
8857
- mkdirSync18(targetDir, { recursive: true });
9531
+ const targetDir = join25(vaultPath, folder);
9532
+ mkdirSync19(targetDir, { recursive: true });
8858
9533
  console.error(chalk14.dim(`\u{1F4CE} Clipping: ${url}`));
9534
+ try {
9535
+ await assertPublicUrl(url);
9536
+ } catch {
9537
+ console.error(chalk14.yellow("Private/local or non-public URLs are not allowed for security."));
9538
+ process.exit(1);
9539
+ }
8859
9540
  try {
8860
9541
  const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(url);
8861
9542
  let title;
@@ -8872,7 +9553,7 @@ async function clipCommand(url, options) {
8872
9553
  const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
8873
9554
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8874
9555
  const fileName = `${date} ${safeTitle}.md`;
8875
- const filePath = join24(targetDir, fileName);
9556
+ const filePath = join25(targetDir, fileName);
8876
9557
  const md = [
8877
9558
  "---",
8878
9559
  `title: "${safeTitle}"`,
@@ -9089,14 +9770,14 @@ async function digestCommand(options) {
9089
9770
  \u{1F9E0} Health: R=${report.averageR} | Decaying ${report.decayingCount} | Critical ${report.criticalCount}`);
9090
9771
  console.log(chalk16.dim("\n\u2550".repeat(50)));
9091
9772
  if (options.visual) {
9092
- const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync23, existsSync: existsSync27 } = await import("node:fs");
9093
- const { join: join32, resolve: resolve22 } = await import("node:path");
9773
+ const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync24, existsSync: existsSync28 } = await import("node:fs");
9774
+ const { join: join33, resolve: resolve22 } = await import("node:path");
9094
9775
  const outputDir = resolve22(config.vaultPath, "_stellavault/digests");
9095
- if (!existsSync27(outputDir))
9096
- mkdirSync23(outputDir, { recursive: true });
9776
+ if (!existsSync28(outputDir))
9777
+ mkdirSync24(outputDir, { recursive: true });
9097
9778
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9098
9779
  const filename = `digest-${date}.md`;
9099
- const outputPath = join32(outputDir, filename);
9780
+ const outputPath = join33(outputDir, filename);
9100
9781
  const pieData = (typeStats.length > 0 ? typeStats : [{ type: "note", cnt: 1 }]).map((t2) => ` "${t2.type}" : ${t2.cnt}`).join("\n");
9101
9782
  const timelineData = dailyActivity.map((d) => ` ${d.day.slice(5)} : ${d.cnt}`).join("\n");
9102
9783
  const md = [
@@ -9143,9 +9824,9 @@ Visual digest saved: ${filename}`));
9143
9824
 
9144
9825
  // packages/cli/dist/commands/init-cmd.js
9145
9826
  import { createInterface as createInterface2 } from "node:readline";
9146
- import { existsSync as existsSync20, mkdirSync as mkdirSync19, writeFileSync as writeFileSync19 } from "node:fs";
9147
- import { join as join25 } from "node:path";
9148
- import { homedir as homedir16 } from "node:os";
9827
+ import { existsSync as existsSync21, mkdirSync as mkdirSync20, writeFileSync as writeFileSync19 } from "node:fs";
9828
+ import { join as join26 } from "node:path";
9829
+ import { homedir as homedir17 } from "node:os";
9149
9830
  import ora2 from "ora";
9150
9831
  import chalk17 from "chalk";
9151
9832
  function ask(rl, question, defaultVal) {
@@ -9171,8 +9852,8 @@ async function initCommand() {
9171
9852
  console.log(chalk17.yellow(" Please enter your vault path."));
9172
9853
  continue;
9173
9854
  }
9174
- const resolved = input.replace(/^~/, homedir16());
9175
- if (!existsSync20(resolved)) {
9855
+ const resolved = input.replace(/^~/, homedir17());
9856
+ if (!existsSync21(resolved)) {
9176
9857
  console.log(chalk17.yellow(` Path not found: ${resolved}`));
9177
9858
  continue;
9178
9859
  }
@@ -9180,9 +9861,9 @@ async function initCommand() {
9180
9861
  }
9181
9862
  console.log(chalk17.green(` \u2713 Vault: ${vaultPath}
9182
9863
  `));
9183
- const configDir = join25(homedir16(), ".stellavault");
9184
- mkdirSync19(configDir, { recursive: true });
9185
- const dbPath = join25(configDir, "index.db");
9864
+ const configDir = join26(homedir17(), ".stellavault");
9865
+ mkdirSync20(configDir, { recursive: true });
9866
+ const dbPath = join26(configDir, "index.db");
9186
9867
  const configData = {
9187
9868
  vaultPath,
9188
9869
  dbPath,
@@ -9191,7 +9872,7 @@ async function initCommand() {
9191
9872
  search: { defaultLimit: 10, rrfK: 60 },
9192
9873
  mcp: { mode: "stdio", port: 3333 }
9193
9874
  };
9194
- writeFileSync19(join25(homedir16(), ".stellavault.json"), JSON.stringify(configData, null, 2), "utf-8");
9875
+ writeFileSync19(join26(homedir17(), ".stellavault.json"), JSON.stringify(configData, null, 2), "utf-8");
9195
9876
  console.log(chalk17.dim(` Config saved: ~/.stellavault.json`));
9196
9877
  console.log("");
9197
9878
  console.log(chalk17.cyan(" Step 2/3") + " \u2014 Indexing your vault");
@@ -9215,8 +9896,8 @@ async function initCommand() {
9215
9896
  spinner.succeed(chalk17.green(` Indexed ${result.indexed} docs, ${result.totalChunks} chunks (${(result.elapsedMs / 1e3).toFixed(1)}s)`));
9216
9897
  if (result.indexed === 0) {
9217
9898
  console.log(chalk17.yellow("\n Your vault is empty \u2014 creating 3 starter notes so you can explore.\n"));
9218
- const rawDir = join25(vaultPath, "raw");
9219
- mkdirSync19(rawDir, { recursive: true });
9899
+ const rawDir = join26(vaultPath, "raw");
9900
+ mkdirSync20(rawDir, { recursive: true });
9220
9901
  const samples = [
9221
9902
  {
9222
9903
  file: "welcome-to-stellavault.md",
@@ -9289,7 +9970,7 @@ Andrej Karpathy's approach: every session auto-compiles into structured knowledg
9289
9970
  }
9290
9971
  ];
9291
9972
  for (const s of samples) {
9292
- writeFileSync19(join25(rawDir, s.file), s.content, "utf-8");
9973
+ writeFileSync19(join26(rawDir, s.file), s.content, "utf-8");
9293
9974
  }
9294
9975
  const reSpinner = ora2({ text: " Indexing sample notes...", indent: 2 }).start();
9295
9976
  const reResult = await indexVault(vaultPath, {
@@ -9950,8 +10631,8 @@ import chalk26 from "chalk";
9950
10631
  // packages/core/dist/intelligence/draft-generator.js
9951
10632
  init_wiki_compiler();
9952
10633
  init_config();
9953
- import { writeFileSync as writeFileSync20, mkdirSync as mkdirSync20, existsSync as existsSync21 } from "node:fs";
9954
- import { join as join26, resolve as resolve16, basename as basename5, extname as extname7 } from "node:path";
10634
+ import { writeFileSync as writeFileSync20, mkdirSync as mkdirSync21, existsSync as existsSync22 } from "node:fs";
10635
+ import { join as join27, resolve as resolve16, basename as basename5, extname as extname7 } from "node:path";
9955
10636
  function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
9956
10637
  const { topic, format = "blog", maxSections = 8, blueprint } = options;
9957
10638
  const rawDir = resolve16(vaultPath, folders.fleeting);
@@ -9959,7 +10640,7 @@ function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
9959
10640
  const litDir = resolve16(vaultPath, folders.literature);
9960
10641
  const allDocs = [];
9961
10642
  for (const dir of [rawDir, wikiDir, litDir]) {
9962
- if (existsSync21(dir)) {
10643
+ if (existsSync22(dir)) {
9963
10644
  allDocs.push(...scanRawDirectory(dir));
9964
10645
  }
9965
10646
  }
@@ -10027,12 +10708,12 @@ function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
10027
10708
  }
10028
10709
  const wordCount = body.split(/\s+/).filter(Boolean).length;
10029
10710
  const draftsDir = resolve16(vaultPath, "_drafts");
10030
- if (!existsSync21(draftsDir))
10031
- mkdirSync20(draftsDir, { recursive: true });
10711
+ if (!existsSync22(draftsDir))
10712
+ mkdirSync21(draftsDir, { recursive: true });
10032
10713
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
10033
10714
  const slug = (topic ?? "knowledge").replace(/[^a-zA-Z0-9가-힣\s]/g, "").replace(/\s+/g, "-").toLowerCase().slice(0, 40);
10034
10715
  const filename = `${timestamp}-${slug}.md`;
10035
- const filePath = join26("_drafts", filename);
10716
+ const filePath = join27("_drafts", filename);
10036
10717
  const fullPath = resolve16(vaultPath, filePath);
10037
10718
  writeFileSync20(fullPath, body, "utf-8");
10038
10719
  return {
@@ -10284,8 +10965,8 @@ ${aiContent.text}
10284
10965
 
10285
10966
  // packages/cli/dist/commands/session-cmd.js
10286
10967
  import chalk27 from "chalk";
10287
- import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync21, existsSync as existsSync22, appendFileSync } from "node:fs";
10288
- import { resolve as resolve17, join as join27 } from "node:path";
10968
+ import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync22, existsSync as existsSync23, appendFileSync } from "node:fs";
10969
+ import { resolve as resolve17, join as join28 } from "node:path";
10289
10970
  async function sessionSaveCommand(options) {
10290
10971
  const config = loadConfig();
10291
10972
  if (!config.vaultPath) {
@@ -10294,12 +10975,12 @@ async function sessionSaveCommand(options) {
10294
10975
  }
10295
10976
  const folders = config.folders;
10296
10977
  const logDir = resolve17(config.vaultPath, folders.fleeting, "_daily-logs");
10297
- if (!existsSync22(logDir))
10298
- mkdirSync21(logDir, { recursive: true });
10978
+ if (!existsSync23(logDir))
10979
+ mkdirSync22(logDir, { recursive: true });
10299
10980
  const now = /* @__PURE__ */ new Date();
10300
10981
  const dateStr = now.toISOString().split("T")[0];
10301
10982
  const timeStr = now.toTimeString().split(" ")[0];
10302
- const logFile = join27(logDir, `daily-log-${dateStr}.md`);
10983
+ const logFile = join28(logDir, `daily-log-${dateStr}.md`);
10303
10984
  let summary = options.summary ?? "";
10304
10985
  if (!summary && !process.stdin.isTTY) {
10305
10986
  const chunks = [];
@@ -10338,7 +11019,7 @@ async function sessionSaveCommand(options) {
10338
11019
  entry.push("### Action Items", options.actions, "");
10339
11020
  }
10340
11021
  entry.push("---", "");
10341
- if (!existsSync22(logFile)) {
11022
+ if (!existsSync23(logFile)) {
10342
11023
  const header = [
10343
11024
  "---",
10344
11025
  `title: "Daily Log \u2014 ${dateStr}"`,
@@ -10370,8 +11051,8 @@ async function sessionSaveCommand(options) {
10370
11051
 
10371
11052
  // packages/cli/dist/commands/flush-cmd.js
10372
11053
  import chalk28 from "chalk";
10373
- import { readdirSync as readdirSync7, readFileSync as readFileSync17, existsSync as existsSync23 } from "node:fs";
10374
- import { resolve as resolve18, join as join28 } from "node:path";
11054
+ import { readdirSync as readdirSync7, readFileSync as readFileSync17, existsSync as existsSync24 } from "node:fs";
11055
+ import { resolve as resolve18, join as join29 } from "node:path";
10375
11056
  async function flushCommand() {
10376
11057
  const config = loadConfig();
10377
11058
  if (!config.vaultPath) {
@@ -10380,7 +11061,7 @@ async function flushCommand() {
10380
11061
  }
10381
11062
  const folders = config.folders;
10382
11063
  const logDir = resolve18(config.vaultPath, folders.fleeting, "_daily-logs");
10383
- if (!existsSync23(logDir)) {
11064
+ if (!existsSync24(logDir)) {
10384
11065
  console.log(chalk28.yellow("No daily logs found. Use `stellavault session-save` or let Claude Code hooks capture sessions."));
10385
11066
  return;
10386
11067
  }
@@ -10393,7 +11074,7 @@ async function flushCommand() {
10393
11074
  let totalSessions = 0;
10394
11075
  const allContent = [];
10395
11076
  for (const file of logFiles) {
10396
- const content = readFileSync17(join28(logDir, file), "utf-8");
11077
+ const content = readFileSync17(join29(logDir, file), "utf-8");
10397
11078
  const sessions = content.split(/^## Session/m).slice(1);
10398
11079
  totalSessions += sessions.length;
10399
11080
  allContent.push(content);
@@ -10521,8 +11202,8 @@ async function lintCommand() {
10521
11202
 
10522
11203
  // packages/cli/dist/commands/fleeting-cmd.js
10523
11204
  import chalk31 from "chalk";
10524
- import { writeFileSync as writeFileSync22, mkdirSync as mkdirSync22, existsSync as existsSync24 } from "node:fs";
10525
- import { join as join29, resolve as resolve19 } from "node:path";
11205
+ import { writeFileSync as writeFileSync22, mkdirSync as mkdirSync23, existsSync as existsSync25 } from "node:fs";
11206
+ import { join as join30, resolve as resolve19 } from "node:path";
10526
11207
  async function fleetingCommand(text, options) {
10527
11208
  if (!text || text.trim().length < 2) {
10528
11209
  console.error(chalk31.yellow('Usage: stellavault fleeting "your idea here" [--tags tag1,tag2]'));
@@ -10530,13 +11211,13 @@ async function fleetingCommand(text, options) {
10530
11211
  }
10531
11212
  const config = loadConfig();
10532
11213
  const rawDir = resolve19(config.vaultPath, "raw");
10533
- if (!existsSync24(rawDir))
10534
- mkdirSync22(rawDir, { recursive: true });
11214
+ if (!existsSync25(rawDir))
11215
+ mkdirSync23(rawDir, { recursive: true });
10535
11216
  const now = /* @__PURE__ */ new Date();
10536
11217
  const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
10537
11218
  const slug = text.slice(0, 40).replace(/[^a-zA-Z0-9가-힣\s]/g, "").replace(/\s+/g, "-").toLowerCase();
10538
11219
  const filename = `${timestamp}-${slug}.md`;
10539
- const filePath = join29(rawDir, filename);
11220
+ const filePath = join30(rawDir, filename);
10540
11221
  if (!resolve19(filePath).startsWith(resolve19(rawDir))) {
10541
11222
  console.error(chalk31.red("Invalid file path"));
10542
11223
  process.exit(1);
@@ -10563,15 +11244,15 @@ async function fleetingCommand(text, options) {
10563
11244
 
10564
11245
  // packages/cli/dist/commands/ingest-cmd.js
10565
11246
  import chalk32 from "chalk";
10566
- import { readFileSync as readFileSync18, existsSync as existsSync25, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
10567
- import { extname as extname8, resolve as resolve20, join as join30 } from "node:path";
11247
+ import { readFileSync as readFileSync18, existsSync as existsSync26, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
11248
+ import { extname as extname8, resolve as resolve20, join as join31 } from "node:path";
10568
11249
  async function ingestCommand(input, options) {
10569
11250
  if (!input) {
10570
11251
  console.error(chalk32.yellow("Usage: stellavault ingest <url|file|text|folder/> [--tags t1,t2]"));
10571
11252
  process.exit(1);
10572
11253
  }
10573
- if (existsSync25(input) && statSync5(input).isDirectory()) {
10574
- const files = readdirSync8(input).filter((f) => /\.(md|txt|pdf|docx|pptx|xlsx|xls|csv|json|xml|html|htm|yaml|yml|rtf)$/i.test(f)).map((f) => join30(input, f));
11254
+ if (existsSync26(input) && statSync5(input).isDirectory()) {
11255
+ const files = readdirSync8(input).filter((f) => /\.(md|txt|pdf|docx|pptx|xlsx|xls|csv|json|xml|html|htm|yaml|yml|rtf)$/i.test(f)).map((f) => join31(input, f));
10575
11256
  if (files.length === 0) {
10576
11257
  console.error(chalk32.yellow(`No supported files found in ${input}`));
10577
11258
  process.exit(1);
@@ -10616,13 +11297,10 @@ async function ingestSingleFile(input, options) {
10616
11297
  if (/^https?:\/\//.test(input)) {
10617
11298
  const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(input);
10618
11299
  try {
10619
- const url = new URL(input);
10620
- const host = url.hostname;
10621
- if (/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.|localhost|::1)/i.test(host)) {
10622
- console.error(chalk32.yellow("Private/local URLs are not allowed for security."));
10623
- process.exit(1);
10624
- }
11300
+ await assertPublicUrl(input);
10625
11301
  } catch {
11302
+ console.error(chalk32.yellow("Private/local or non-public URLs are not allowed for security."));
11303
+ process.exit(1);
10626
11304
  }
10627
11305
  if (isYouTube) {
10628
11306
  try {
@@ -10668,7 +11346,7 @@ async function ingestSingleFile(input, options) {
10668
11346
  source: input
10669
11347
  };
10670
11348
  }
10671
- } else if (existsSync25(input)) {
11349
+ } else if (existsSync26(input)) {
10672
11350
  const ext = extname8(input).toLowerCase();
10673
11351
  const binaryExts = /* @__PURE__ */ new Set([".pdf", ".docx", ".pptx", ".xlsx", ".xls"]);
10674
11352
  const structuredExts = /* @__PURE__ */ new Set([".json", ".csv", ".xml", ".html", ".htm", ".yaml", ".yml", ".rtf"]);
@@ -10826,9 +11504,9 @@ async function autopilotCommand(options) {
10826
11504
 
10827
11505
  // packages/cli/dist/commands/doctor-cmd.js
10828
11506
  import chalk34 from "chalk";
10829
- import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
10830
- import { join as join31 } from "node:path";
10831
- import { homedir as homedir17 } from "node:os";
11507
+ import { existsSync as existsSync27, statSync as statSync6 } from "node:fs";
11508
+ import { join as join32 } from "node:path";
11509
+ import { homedir as homedir18 } from "node:os";
10832
11510
  async function doctorCommand() {
10833
11511
  console.log("");
10834
11512
  console.log(chalk34.bold(" \u{1FA7A} Stellavault Doctor\n"));
@@ -10841,10 +11519,10 @@ async function doctorCommand() {
10841
11519
  fix: nodeVersion2 < 20 ? "Download Node.js 20+: https://nodejs.org" : void 0
10842
11520
  });
10843
11521
  const configPaths = [
10844
- join31(process.cwd(), ".stellavault.json"),
10845
- join31(homedir17(), ".stellavault.json")
11522
+ join32(process.cwd(), ".stellavault.json"),
11523
+ join32(homedir18(), ".stellavault.json")
10846
11524
  ];
10847
- const configPath = configPaths.find((p) => existsSync26(p));
11525
+ const configPath = configPaths.find((p) => existsSync27(p));
10848
11526
  checks.push({
10849
11527
  name: "Config file",
10850
11528
  pass: !!configPath,
@@ -10869,7 +11547,7 @@ async function doctorCommand() {
10869
11547
  }
10870
11548
  }
10871
11549
  if (vaultPath) {
10872
- const vaultExists = existsSync26(vaultPath);
11550
+ const vaultExists = existsSync27(vaultPath);
10873
11551
  checks.push({
10874
11552
  name: "Vault path",
10875
11553
  pass: vaultExists,
@@ -10898,7 +11576,7 @@ async function doctorCommand() {
10898
11576
  }
10899
11577
  }
10900
11578
  if (dbPath) {
10901
- const dbExists = existsSync26(dbPath);
11579
+ const dbExists = existsSync27(dbPath);
10902
11580
  checks.push({
10903
11581
  name: "Database",
10904
11582
  pass: dbExists,
@@ -10913,20 +11591,20 @@ async function doctorCommand() {
10913
11591
  fix: "Re-run: stellavault init"
10914
11592
  });
10915
11593
  }
10916
- const cacheDir = join31(homedir17(), ".cache", "onnxruntime");
10917
- const hfCache = join31(homedir17(), ".cache", "huggingface");
10918
- const xenovaCache = join31(homedir17(), ".cache", "xenova");
10919
- const modelCached = existsSync26(cacheDir) || existsSync26(hfCache) || existsSync26(xenovaCache) || existsSync26(join31(homedir17(), ".cache", "transformers"));
11594
+ const cacheDir = join32(homedir18(), ".cache", "onnxruntime");
11595
+ const hfCache = join32(homedir18(), ".cache", "huggingface");
11596
+ const xenovaCache = join32(homedir18(), ".cache", "xenova");
11597
+ const modelCached = existsSync27(cacheDir) || existsSync27(hfCache) || existsSync27(xenovaCache) || existsSync27(join32(homedir18(), ".cache", "transformers"));
10920
11598
  checks.push({
10921
11599
  name: "Embedding model cached",
10922
11600
  pass: modelCached,
10923
11601
  detail: modelCached ? "local model files found" : "not downloaded yet",
10924
11602
  fix: !modelCached ? "Will download automatically on first index (~30MB)" : void 0
10925
11603
  });
10926
- const testDir = join31(homedir17(), ".stellavault");
11604
+ const testDir = join32(homedir18(), ".stellavault");
10927
11605
  try {
10928
- const { mkdirSync: mkdirSync23 } = await import("node:fs");
10929
- mkdirSync23(testDir, { recursive: true });
11606
+ const { mkdirSync: mkdirSync24 } = await import("node:fs");
11607
+ mkdirSync24(testDir, { recursive: true });
10930
11608
  checks.push({ name: "Write permission (~/.stellavault)", pass: true, detail: "OK" });
10931
11609
  } catch {
10932
11610
  checks.push({
@@ -10976,7 +11654,7 @@ if (nodeVersion < 20) {
10976
11654
  process.exit(1);
10977
11655
  }
10978
11656
  var program = new Command();
10979
- var SV_VERSION = true ? "0.8.4" : "0.0.0-dev";
11657
+ var SV_VERSION = true ? "0.8.6" : "0.0.0-dev";
10980
11658
  program.name("stellavault").description("Stellavault \u2014 Self-compiling knowledge base for your Obsidian vault").version(SV_VERSION).option("--json", "Output in JSON format (for scripting)").option("--quiet", "Suppress non-essential output");
10981
11659
  program.command("init").description("Interactive setup wizard \u2014 get started in 3 minutes").action(initCommand);
10982
11660
  program.command("doctor").description("Diagnose setup issues (config, vault, DB, model, Node version)").action(doctorCommand);