stellavault 0.8.3 → 0.8.5

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,52 @@ 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, {
999
+ buildClusteredGraph: () => buildClusteredGraph,
925
1000
  buildGraphData: () => buildGraphData
926
1001
  });
927
1002
  import { createHash as createHash2 } from "node:crypto";
928
1003
  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
- ]);
1004
+ const {
1005
+ // 0.35: 384-dim 임베딩에서 의미있는 코사인 임계. 0.15는 사실상 모든 페어가 통과 →
1006
+ // 중간 neighbor 배열이 수백만 객체로 폭증(빌드 지연) + 시각적으로 과밀한 엣지 거미줄.
1007
+ edgeThreshold = 0.35,
1008
+ maxEdgesPerNode = 5
1009
+ } = options;
1010
+ const docs = await store.getDocumentsMeta();
934
1011
  const edges = [];
935
1012
  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));
1013
+ const NODE_CAP = Math.max(200, Math.floor(options.nodeCap ?? (Number(process.env.GRAPH_NODE_CAP) || 1500)));
1014
+ const ranked = [...docs].sort((a, b) => String(b.lastModified ?? "").localeCompare(String(a.lastModified ?? "")));
1015
+ if (docs.length > NODE_CAP) {
1016
+ console.warn(`[graph] capped to ${NODE_CAP} most-recent notes (of ${docs.length}) \u2014 raise GRAPH_NODE_CAP to include more.`);
1017
+ }
1018
+ const embeddings = await store.getDocumentEmbeddingsByIds(ranked.slice(0, NODE_CAP).map((d) => d.id));
1019
+ const docsWithVecs = ranked.filter((d) => embeddings.has(d.id)).slice(0, NODE_CAP);
1020
+ const docIds = docsWithVecs.map((d) => d.id);
943
1021
  const n = docIds.length;
1022
+ const dim = n > 0 ? embeddings.get(docIds[0]).length : 0;
1023
+ const flat = new Float32Array(n * dim);
1024
+ for (let i = 0; i < n; i++) {
1025
+ const v = embeddings.get(docIds[i]);
1026
+ let mag = 0;
1027
+ for (let d = 0; d < dim; d++)
1028
+ mag += v[d] * v[d];
1029
+ mag = Math.sqrt(mag) || 1;
1030
+ const off = i * dim;
1031
+ for (let d = 0; d < dim; d++)
1032
+ flat[off + d] = v[d] / mag;
1033
+ }
944
1034
  const neighbors = Array.from({ length: n }, () => []);
945
1035
  for (let i = 0; i < n; i++) {
1036
+ const oi = i * dim;
946
1037
  for (let j = i + 1; j < n; j++) {
947
- const sim = dotProduct(vecArray[i], vecArray[j]);
1038
+ const oj = j * dim;
1039
+ let sim = 0;
1040
+ for (let d = 0; d < dim; d++)
1041
+ sim += flat[oi + d] * flat[oj + d];
948
1042
  if (sim >= edgeThreshold) {
949
1043
  neighbors[i].push({ peer: j, sim });
950
1044
  neighbors[j].push({ peer: i, sim });
@@ -991,16 +1085,16 @@ async function buildGraphData(store, options = {}) {
991
1085
  nodeCount: folderCounts.get(i) ?? 0
992
1086
  }));
993
1087
  } 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);
1088
+ const clusterIds = docIds;
1089
+ const k = options.clusterCount && options.clusterCount > 0 ? Math.floor(options.clusterCount) : Math.min(Math.max(5, Math.round(Math.sqrt(clusterIds.length / 5))), 10);
1090
+ const assignments = kMeans(flat, n, dim, Math.min(k, clusterIds.length || 1), 10);
1091
+ const docById = new Map(docsWithVecs.map((d) => [d.id, d]));
998
1092
  const clusterDocInfos = /* @__PURE__ */ new Map();
999
- for (let i = 0; i < docIds2.length; i++) {
1093
+ for (let i = 0; i < clusterIds.length; i++) {
1000
1094
  const cId = assignments[i];
1001
1095
  if (!clusterDocInfos.has(cId))
1002
1096
  clusterDocInfos.set(cId, []);
1003
- const doc = docs.find((d) => d.id === docIds2[i]);
1097
+ const doc = docById.get(clusterIds[i]);
1004
1098
  if (doc)
1005
1099
  clusterDocInfos.get(cId).push({ id: doc.id, title: doc.title });
1006
1100
  }
@@ -1021,8 +1115,8 @@ async function buildGraphData(store, options = {}) {
1021
1115
  });
1022
1116
  }
1023
1117
  assignmentMap = /* @__PURE__ */ new Map();
1024
- for (let i = 0; i < docIds2.length; i++) {
1025
- assignmentMap.set(docIds2[i], assignments[i]);
1118
+ for (let i = 0; i < clusterIds.length; i++) {
1119
+ assignmentMap.set(clusterIds[i], assignments[i]);
1026
1120
  }
1027
1121
  }
1028
1122
  const connectionCounts = /* @__PURE__ */ new Map();
@@ -1031,7 +1125,7 @@ async function buildGraphData(store, options = {}) {
1031
1125
  connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
1032
1126
  }
1033
1127
  const maxConnections = Math.max(1, ...connectionCounts.values());
1034
- const nodes = docs.map((doc) => {
1128
+ const nodes = docsWithVecs.map((doc) => {
1035
1129
  const conns = connectionCounts.get(doc.id) ?? 0;
1036
1130
  const ratio = conns / maxConnections;
1037
1131
  const size = 1 + 6 * Math.pow(ratio, 0.5);
@@ -1059,40 +1153,278 @@ async function buildGraphData(store, options = {}) {
1059
1153
  }
1060
1154
  };
1061
1155
  }
1062
- function kMeans(vectors, k, maxIter = 50) {
1063
- if (vectors.length === 0)
1156
+ async function buildClusteredGraph(store, options = {}) {
1157
+ const mode = options.mode ?? "semantic";
1158
+ const clusterCap = Math.max(200, Math.floor(options.clusterCap ?? (Number(process.env.GRAPH_CLUSTER_CAP) || 3e3)));
1159
+ const clusterCount = Math.max(1, Math.floor(options.clusterCount ?? Math.min(80, Math.max(6, Math.round(Math.sqrt(clusterCap / 2.5))))));
1160
+ const data = await buildGraphData(store, {
1161
+ mode,
1162
+ nodeCap: clusterCap,
1163
+ clusterCount,
1164
+ edgeThreshold: options.edgeThreshold,
1165
+ maxEdgesPerNode: options.maxEdgesPerNode
1166
+ });
1167
+ const nodeCluster = /* @__PURE__ */ new Map();
1168
+ const byCluster = /* @__PURE__ */ new Map();
1169
+ for (const node of data.nodes) {
1170
+ const cid = node.clusterId ?? 0;
1171
+ nodeCluster.set(node.id, cid);
1172
+ (byCluster.get(cid) ?? byCluster.set(cid, []).get(cid)).push(node);
1173
+ }
1174
+ const degree = /* @__PURE__ */ new Map();
1175
+ for (const e of data.edges) {
1176
+ degree.set(e.source, (degree.get(e.source) ?? 0) + 1);
1177
+ degree.set(e.target, (degree.get(e.target) ?? 0) + 1);
1178
+ }
1179
+ const intraByCluster = /* @__PURE__ */ new Map();
1180
+ const metaMap = /* @__PURE__ */ new Map();
1181
+ for (const e of data.edges) {
1182
+ const ca = nodeCluster.get(e.source), cb = nodeCluster.get(e.target);
1183
+ if (ca == null || cb == null)
1184
+ continue;
1185
+ if (ca === cb) {
1186
+ (intraByCluster.get(ca) ?? intraByCluster.set(ca, []).get(ca)).push(e);
1187
+ } else {
1188
+ const lo = Math.min(ca, cb), hi = Math.max(ca, cb);
1189
+ const key = `${lo}:${hi}`;
1190
+ const m = metaMap.get(key);
1191
+ if (m) {
1192
+ m.weight += e.weight;
1193
+ m.count += 1;
1194
+ } else
1195
+ metaMap.set(key, { sourceCluster: lo, targetCluster: hi, weight: e.weight, count: 1 });
1196
+ }
1197
+ }
1198
+ const clusterLabel = /* @__PURE__ */ new Map();
1199
+ for (const c of data.clusters)
1200
+ clusterLabel.set(c.id, c.label);
1201
+ const superNodes = [];
1202
+ for (const [cid, mem] of byCluster) {
1203
+ let rep = mem[0], repDeg = -1;
1204
+ for (const m of mem) {
1205
+ const d = degree.get(m.id) ?? 0;
1206
+ if (d > repDeg) {
1207
+ repDeg = d;
1208
+ rep = m;
1209
+ }
1210
+ }
1211
+ superNodes.push({
1212
+ clusterId: cid,
1213
+ // strip buildGraphData's trailing " (N)" — memberCount is a separate field.
1214
+ label: (clusterLabel.get(cid) ?? `Cluster ${cid + 1}`).replace(/\s*\(\d+\)\s*$/, ""),
1215
+ color: CLUSTER_COLORS[cid % CLUSTER_COLORS.length],
1216
+ memberCount: mem.length,
1217
+ position: [0, 0, 0],
1218
+ // assigned below by Fibonacci rank
1219
+ size: 2 + Math.min(12, Math.sqrt(mem.length)),
1220
+ representativeId: rep?.id ?? ""
1221
+ });
1222
+ }
1223
+ superNodes.sort((a, b) => b.memberCount - a.memberCount);
1224
+ layoutSuperNodes(superNodes, metaMap);
1225
+ const members = /* @__PURE__ */ new Map();
1226
+ for (const [cid, mem] of byCluster) {
1227
+ const boundaryEdges = [];
1228
+ for (const e of data.edges) {
1229
+ const ca = nodeCluster.get(e.source), cb = nodeCluster.get(e.target);
1230
+ if (ca === cid && cb !== cid && cb != null)
1231
+ boundaryEdges.push({ source: e.source, targetCluster: cb, weight: e.weight });
1232
+ else if (cb === cid && ca !== cid && ca != null)
1233
+ boundaryEdges.push({ source: e.target, targetCluster: ca, weight: e.weight });
1234
+ }
1235
+ members.set(cid, { clusterId: cid, members: mem, intraEdges: intraByCluster.get(cid) ?? [], boundaryEdges });
1236
+ }
1237
+ const META_PER_CLUSTER = 2;
1238
+ const metaByCluster = /* @__PURE__ */ new Map();
1239
+ for (const m of metaMap.values()) {
1240
+ (metaByCluster.get(m.sourceCluster) ?? metaByCluster.set(m.sourceCluster, []).get(m.sourceCluster)).push(m);
1241
+ (metaByCluster.get(m.targetCluster) ?? metaByCluster.set(m.targetCluster, []).get(m.targetCluster)).push(m);
1242
+ }
1243
+ const keptMeta = /* @__PURE__ */ new Set();
1244
+ for (const list of metaByCluster.values()) {
1245
+ list.sort((a, b) => b.weight - a.weight);
1246
+ for (const m of list.slice(0, META_PER_CLUSTER))
1247
+ keptMeta.add(m);
1248
+ }
1249
+ return {
1250
+ clusterLevel: {
1251
+ level: "galaxy",
1252
+ superNodes,
1253
+ metaEdges: [...keptMeta],
1254
+ totalNodes: data.nodes.length,
1255
+ totalEdges: data.edges.length,
1256
+ layoutVersion: mode
1257
+ },
1258
+ members
1259
+ };
1260
+ }
1261
+ function fibonacciSphere(i, n, radius) {
1262
+ const phi = Math.acos(1 - 2 * (i + 0.5) / Math.max(1, n));
1263
+ const theta = Math.PI * (1 + Math.sqrt(5)) * i;
1264
+ return [
1265
+ radius * Math.sin(phi) * Math.cos(theta),
1266
+ radius * Math.sin(phi) * Math.sin(theta),
1267
+ radius * Math.cos(phi)
1268
+ ];
1269
+ }
1270
+ function layoutSuperNodes(superNodes, metaMap) {
1271
+ const n = superNodes.length;
1272
+ if (n === 0)
1273
+ return;
1274
+ if (n === 1) {
1275
+ superNodes[0].position = [0, 0, 0];
1276
+ return;
1277
+ }
1278
+ const idx = /* @__PURE__ */ new Map();
1279
+ superNodes.forEach((s, i) => idx.set(s.clusterId, i));
1280
+ const links = [];
1281
+ let maxW = 0;
1282
+ for (const m of metaMap.values()) {
1283
+ const a = idx.get(m.sourceCluster), b = idx.get(m.targetCluster);
1284
+ if (a == null || b == null || a === b)
1285
+ continue;
1286
+ links.push([a, b, m.weight]);
1287
+ if (m.weight > maxW)
1288
+ maxW = m.weight;
1289
+ }
1290
+ const wn = maxW > 0 ? 1 / maxW : 1;
1291
+ const pos = new Float32Array(n * 3);
1292
+ const vel = new Float32Array(n * 3);
1293
+ for (let i = 0; i < n; i++) {
1294
+ const p = fibonacciSphere(i, n, 100);
1295
+ pos[i * 3] = p[0];
1296
+ pos[i * 3 + 1] = p[1];
1297
+ pos[i * 3 + 2] = p[2];
1298
+ }
1299
+ const REST = 42, CHARGE = 900, ITERS = 600, DAMP = 0.9, CENTER = 0.012;
1300
+ for (let it = 0; it < ITERS; it++) {
1301
+ const alpha = 1 - it / ITERS;
1302
+ for (let i = 0; i < n; i++) {
1303
+ const ix = pos[i * 3], iy = pos[i * 3 + 1], iz = pos[i * 3 + 2];
1304
+ for (let j = i + 1; j < n; j++) {
1305
+ let dx = pos[j * 3] - ix, dy = pos[j * 3 + 1] - iy, dz = pos[j * 3 + 2] - iz;
1306
+ let d2 = dx * dx + dy * dy + dz * dz;
1307
+ if (d2 < 0.01) {
1308
+ dx = (i - j) % 3 * 0.1 || 0.1;
1309
+ dy = 0.1;
1310
+ dz = 0.1;
1311
+ d2 = dx * dx + dy * dy + dz * dz;
1312
+ }
1313
+ const f = CHARGE * alpha / d2;
1314
+ const fx = dx * f, fy = dy * f, fz = dz * f;
1315
+ vel[i * 3] -= fx;
1316
+ vel[i * 3 + 1] -= fy;
1317
+ vel[i * 3 + 2] -= fz;
1318
+ vel[j * 3] += fx;
1319
+ vel[j * 3 + 1] += fy;
1320
+ vel[j * 3 + 2] += fz;
1321
+ }
1322
+ }
1323
+ for (const [a, b, w] of links) {
1324
+ 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];
1325
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.01;
1326
+ const strength = 0.6 * (0.3 + 0.7 * w * wn);
1327
+ const f = (dist - REST) / dist * alpha * strength;
1328
+ const fx = dx * f, fy = dy * f, fz = dz * f;
1329
+ vel[a * 3] += fx;
1330
+ vel[a * 3 + 1] += fy;
1331
+ vel[a * 3 + 2] += fz;
1332
+ vel[b * 3] -= fx;
1333
+ vel[b * 3 + 1] -= fy;
1334
+ vel[b * 3 + 2] -= fz;
1335
+ }
1336
+ for (let i = 0; i < n; i++) {
1337
+ vel[i * 3] -= pos[i * 3] * CENTER * alpha;
1338
+ vel[i * 3 + 1] -= pos[i * 3 + 1] * CENTER * alpha;
1339
+ vel[i * 3 + 2] -= pos[i * 3 + 2] * CENTER * alpha;
1340
+ let vx = vel[i * 3] * DAMP, vy = vel[i * 3 + 1] * DAMP, vz = vel[i * 3 + 2] * DAMP;
1341
+ const sp2 = vx * vx + vy * vy + vz * vz;
1342
+ if (sp2 > 64) {
1343
+ const k = 8 / Math.sqrt(sp2);
1344
+ vx *= k;
1345
+ vy *= k;
1346
+ vz *= k;
1347
+ }
1348
+ vel[i * 3] = vx;
1349
+ vel[i * 3 + 1] = vy;
1350
+ vel[i * 3 + 2] = vz;
1351
+ pos[i * 3] += vx;
1352
+ pos[i * 3 + 1] += vy;
1353
+ pos[i * 3 + 2] += vz;
1354
+ }
1355
+ }
1356
+ let cx = 0, cy = 0, cz = 0;
1357
+ for (let i = 0; i < n; i++) {
1358
+ cx += pos[i * 3];
1359
+ cy += pos[i * 3 + 1];
1360
+ cz += pos[i * 3 + 2];
1361
+ }
1362
+ cx /= n;
1363
+ cy /= n;
1364
+ cz /= n;
1365
+ let maxR = 0;
1366
+ for (let i = 0; i < n; i++) {
1367
+ const dx = pos[i * 3] - cx, dy = pos[i * 3 + 1] - cy, dz = pos[i * 3 + 2] - cz;
1368
+ const r = Math.sqrt(dx * dx + dy * dy + dz * dz);
1369
+ if (r > maxR)
1370
+ maxR = r;
1371
+ }
1372
+ const scale = maxR > 1 ? 125 / maxR : 1;
1373
+ for (let i = 0; i < n; i++) {
1374
+ superNodes[i].position = [
1375
+ (pos[i * 3] - cx) * scale,
1376
+ (pos[i * 3 + 1] - cy) * scale,
1377
+ (pos[i * 3 + 2] - cz) * scale
1378
+ ];
1379
+ }
1380
+ }
1381
+ function kMeans(flat, n, dims, k, maxIter = 50) {
1382
+ if (n === 0 || dims === 0)
1064
1383
  return [];
1065
- const dims = vectors[0].length;
1066
- const centroids = [vectors[Math.floor(Math.random() * vectors.length)].slice()];
1384
+ k = Math.max(1, Math.min(k, n));
1385
+ const centroids = new Float32Array(k * dims);
1386
+ centroids.set(flat.subarray(0, dims), 0);
1387
+ const minDist = new Float64Array(n).fill(Infinity);
1067
1388
  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];
1389
+ const ocPrev = (c - 1) * dims;
1390
+ let total = 0;
1391
+ for (let i = 0; i < n; i++) {
1392
+ const oi = i * dims;
1393
+ let d = 0;
1394
+ for (let z = 0; z < dims; z++) {
1395
+ const diff = flat[oi + z] - centroids[ocPrev + z];
1396
+ d += diff * diff;
1397
+ }
1398
+ if (d < minDist[i])
1399
+ minDist[i] = d;
1400
+ total += minDist[i];
1401
+ }
1402
+ let r = Math.random() * total;
1403
+ let pick = n - 1;
1404
+ for (let i = 0; i < n; i++) {
1405
+ r -= minDist[i];
1081
1406
  if (r <= 0) {
1082
- centroids.push(vectors[i].slice());
1407
+ pick = i;
1083
1408
  break;
1084
1409
  }
1085
1410
  }
1086
- if (centroids.length <= c)
1087
- centroids.push(vectors[Math.floor(Math.random() * vectors.length)].slice());
1411
+ centroids.set(flat.subarray(pick * dims, pick * dims + dims), c * dims);
1088
1412
  }
1089
- const assignments = new Array(vectors.length).fill(0);
1413
+ const assignments = new Array(n).fill(0);
1414
+ const sums = new Float64Array(k * dims);
1415
+ const counts = new Uint32Array(k);
1090
1416
  for (let iter = 0; iter < maxIter; iter++) {
1091
1417
  let changed = false;
1092
- for (let i = 0; i < vectors.length; i++) {
1418
+ for (let i = 0; i < n; i++) {
1419
+ const oi = i * dims;
1093
1420
  let bestC = 0, bestD = Infinity;
1094
1421
  for (let c = 0; c < k; c++) {
1095
- const d = euclideanDist(vectors[i], centroids[c]);
1422
+ const oc = c * dims;
1423
+ let d = 0;
1424
+ for (let z = 0; z < dims; z++) {
1425
+ const diff = flat[oi + z] - centroids[oc + z];
1426
+ d += diff * diff;
1427
+ }
1096
1428
  if (d < bestD) {
1097
1429
  bestD = d;
1098
1430
  bestC = c;
@@ -1105,21 +1437,21 @@ function kMeans(vectors, k, maxIter = 50) {
1105
1437
  }
1106
1438
  if (!changed)
1107
1439
  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++) {
1440
+ sums.fill(0);
1441
+ counts.fill(0);
1442
+ for (let i = 0; i < n; i++) {
1111
1443
  const c = assignments[i];
1112
1444
  counts[c]++;
1113
- const s = sums[c], v = vectors[i];
1114
- for (let d = 0; d < dims; d++)
1115
- s[d] += v[d];
1445
+ const oi = i * dims, oc = c * dims;
1446
+ for (let z = 0; z < dims; z++)
1447
+ sums[oc + z] += flat[oi + z];
1116
1448
  }
1117
1449
  for (let c = 0; c < k; c++) {
1118
1450
  if (counts[c] === 0)
1119
1451
  continue;
1120
- const s = sums[c];
1121
- for (let d = 0; d < dims; d++)
1122
- centroids[c][d] = s[d] / counts[c];
1452
+ const oc = c * dims;
1453
+ for (let z = 0; z < dims; z++)
1454
+ centroids[oc + z] = sums[oc + z] / counts[c];
1123
1455
  }
1124
1456
  }
1125
1457
  return assignments;
@@ -1128,7 +1460,6 @@ var CLUSTER_COLORS;
1128
1460
  var init_graph_data = __esm({
1129
1461
  "packages/core/dist/api/graph-data.js"() {
1130
1462
  "use strict";
1131
- init_math();
1132
1463
  CLUSTER_COLORS = [
1133
1464
  "#6366f1",
1134
1465
  "#ec4899",
@@ -1155,8 +1486,6 @@ __export(gap_detector_exports, {
1155
1486
  detectKnowledgeGaps: () => detectKnowledgeGaps
1156
1487
  });
1157
1488
  async function detectKnowledgeGaps(store, graphData) {
1158
- const docs = await store.getAllDocuments();
1159
- const embeddings = await store.getDocumentEmbeddings();
1160
1489
  let gd;
1161
1490
  if (!graphData) {
1162
1491
  const { buildGraphData: buildGraphData2 } = await Promise.resolve().then(() => (init_graph_data(), graph_data_exports));
@@ -1165,10 +1494,13 @@ async function detectKnowledgeGaps(store, graphData) {
1165
1494
  gd = graphData;
1166
1495
  }
1167
1496
  const { nodes, edges, clusters } = gd;
1497
+ const nodeById = /* @__PURE__ */ new Map();
1498
+ for (const n of nodes)
1499
+ nodeById.set(n.id, n);
1168
1500
  const clusterEdges = /* @__PURE__ */ new Map();
1169
1501
  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);
1502
+ const nodeA = nodeById.get(edge.source);
1503
+ const nodeB = nodeById.get(edge.target);
1172
1504
  if (!nodeA || !nodeB || nodeA.clusterId === nodeB.clusterId)
1173
1505
  continue;
1174
1506
  const key = [
@@ -1229,10 +1561,10 @@ var ask_engine_exports = {};
1229
1561
  __export(ask_engine_exports, {
1230
1562
  askVault: () => askVault
1231
1563
  });
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";
1564
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync6, existsSync as existsSync5 } from "node:fs";
1565
+ import { join as join6, resolve as resolve5 } from "node:path";
1234
1566
  async function askVault(searchEngine, question, options = {}) {
1235
- const { limit = 10, save = false, vaultPath, outputDir = "_stellavault/answers", mode = "default" } = options;
1567
+ const { limit = 10, save = false, vaultPath, outputDir = "_stellavault/answers", mode = "default", synthesizer } = options;
1236
1568
  const results = await searchEngine.search({ query: question, limit });
1237
1569
  const sources = results.map((r) => ({
1238
1570
  title: r.document.title,
@@ -1240,7 +1572,16 @@ async function askVault(searchEngine, question, options = {}) {
1240
1572
  score: Math.round(r.score * 1e3) / 1e3,
1241
1573
  snippet: r.chunk?.content?.substring(0, 200) ?? ""
1242
1574
  }));
1243
- const answer = mode === "quotes" ? composeQuotes(question, results) : composeAnswer(question, results);
1575
+ let answer;
1576
+ if (synthesizer && results.length > 0 && mode !== "quotes") {
1577
+ try {
1578
+ answer = await synthesizer.synthesize({ question, sources, mode: "ask" });
1579
+ } catch {
1580
+ answer = composeAnswer(question, results);
1581
+ }
1582
+ } else {
1583
+ answer = mode === "quotes" ? composeQuotes(question, results) : composeAnswer(question, results);
1584
+ }
1244
1585
  let savedTo = null;
1245
1586
  if (save && vaultPath) {
1246
1587
  savedTo = saveAnswerToVault(question, answer, sources, vaultPath, outputDir);
@@ -1311,13 +1652,13 @@ function composeQuotes(question, results) {
1311
1652
  }
1312
1653
  function saveAnswerToVault(question, answer, sources, vaultPath, outputDir) {
1313
1654
  const dir = resolve5(vaultPath, outputDir);
1314
- if (!existsSync4(dir)) {
1315
- mkdirSync5(dir, { recursive: true });
1655
+ if (!existsSync5(dir)) {
1656
+ mkdirSync6(dir, { recursive: true });
1316
1657
  }
1317
1658
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1318
1659
  const slug = question.toLowerCase().replace(/[^a-z0-9가-힣\s]/g, "").replace(/\s+/g, "-").substring(0, 60);
1319
1660
  const filename = `${date}-${slug}.md`;
1320
- const filePath = join5(outputDir, filename);
1661
+ const filePath = join6(outputDir, filename);
1321
1662
  const fullPath = resolve5(vaultPath, filePath);
1322
1663
  if (!fullPath.startsWith(resolve5(vaultPath))) {
1323
1664
  throw new Error("Invalid output path");
@@ -1352,15 +1693,15 @@ __export(wiki_compiler_exports, {
1352
1693
  extractConcepts: () => extractConcepts,
1353
1694
  scanRawDirectory: () => scanRawDirectory
1354
1695
  });
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";
1696
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, existsSync as existsSync6, statSync as statSync2 } from "node:fs";
1697
+ import { join as join7, resolve as resolve6, basename, extname as extname3 } from "node:path";
1357
1698
  function scanRawDirectory(rawPath) {
1358
- if (!existsSync5(rawPath))
1699
+ if (!existsSync6(rawPath))
1359
1700
  return [];
1360
1701
  const docs = [];
1361
1702
  const files = readdirSync3(rawPath, { recursive: true });
1362
1703
  for (const file of files) {
1363
- const fullPath = join6(rawPath, file);
1704
+ const fullPath = join7(rawPath, file);
1364
1705
  if (!statSync2(fullPath).isFile())
1365
1706
  continue;
1366
1707
  const ext = extname3(file).toLowerCase();
@@ -1420,8 +1761,8 @@ function compileWiki(rawPath, wikiPath, options = {}) {
1420
1761
  if (docs.length === 0) {
1421
1762
  return { rawDocCount: 0, wikiArticles: [], indexFile: "", concepts: [] };
1422
1763
  }
1423
- if (!existsSync5(wikiPath)) {
1424
- mkdirSync6(wikiPath, { recursive: true });
1764
+ if (!existsSync6(wikiPath)) {
1765
+ mkdirSync7(wikiPath, { recursive: true });
1425
1766
  }
1426
1767
  const concepts = extractConcepts(docs);
1427
1768
  const wikiArticles = [];
@@ -1542,9 +1883,9 @@ var init_wiki_compiler = __esm({
1542
1883
 
1543
1884
  // packages/core/dist/federation/identity.js
1544
1885
  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";
1886
+ import { chmodSync, existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
1887
+ import { homedir as homedir5 } from "node:os";
1888
+ import { join as join9 } from "node:path";
1548
1889
  function derivePeerId(publicKey) {
1549
1890
  return createHash3("sha256").update(publicKey).digest("hex").slice(0, 16);
1550
1891
  }
@@ -1562,7 +1903,7 @@ function generateIdentity(displayName) {
1562
1903
  };
1563
1904
  }
1564
1905
  function persistIdentity(identity) {
1565
- mkdirSync9(IDENTITY_DIR, { recursive: true });
1906
+ mkdirSync10(IDENTITY_DIR, { recursive: true });
1566
1907
  const content = JSON.stringify({
1567
1908
  version: IDENTITY_VERSION,
1568
1909
  algorithm: IDENTITY_ALGORITHM,
@@ -1579,7 +1920,7 @@ function persistIdentity(identity) {
1579
1920
  }
1580
1921
  }
1581
1922
  function getOrCreateIdentity(displayName) {
1582
- if (existsSync7(IDENTITY_FILE)) {
1923
+ if (existsSync8(IDENTITY_FILE)) {
1583
1924
  const raw = JSON.parse(readFileSync7(IDENTITY_FILE, "utf-8"));
1584
1925
  const isV2 = raw.version === IDENTITY_VERSION && raw.algorithm === IDENTITY_ALGORITHM;
1585
1926
  if (isV2) {
@@ -1618,8 +1959,8 @@ var IDENTITY_DIR, IDENTITY_FILE, IDENTITY_VERSION, IDENTITY_ALGORITHM;
1618
1959
  var init_identity = __esm({
1619
1960
  "packages/core/dist/federation/identity.js"() {
1620
1961
  "use strict";
1621
- IDENTITY_DIR = join8(homedir4(), ".stellavault", "federation");
1622
- IDENTITY_FILE = join8(IDENTITY_DIR, "identity.json");
1962
+ IDENTITY_DIR = join9(homedir5(), ".stellavault", "federation");
1963
+ IDENTITY_FILE = join9(IDENTITY_DIR, "identity.json");
1623
1964
  IDENTITY_VERSION = 2;
1624
1965
  IDENTITY_ALGORITHM = "ed25519";
1625
1966
  }
@@ -2088,18 +2429,18 @@ var init_privacy = __esm({
2088
2429
  });
2089
2430
 
2090
2431
  // 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";
2432
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync11 } from "node:fs";
2433
+ import { join as join10 } from "node:path";
2434
+ import { homedir as homedir6 } from "node:os";
2094
2435
  function loadSharingConfig() {
2095
- if (existsSync8(SHARING_FILE)) {
2436
+ if (existsSync9(SHARING_FILE)) {
2096
2437
  const raw = JSON.parse(readFileSync8(SHARING_FILE, "utf-8"));
2097
2438
  return { ...DEFAULT_CONFIG2, ...raw };
2098
2439
  }
2099
2440
  return { ...DEFAULT_CONFIG2 };
2100
2441
  }
2101
2442
  function saveSharingConfig(config) {
2102
- mkdirSync10(join9(homedir5(), ".stellavault", "federation"), { recursive: true });
2443
+ mkdirSync11(join10(homedir6(), ".stellavault", "federation"), { recursive: true });
2103
2444
  writeFileSync9(SHARING_FILE, JSON.stringify(config, null, 2), "utf-8");
2104
2445
  }
2105
2446
  function getDocumentLevel(doc, config) {
@@ -2236,7 +2577,7 @@ var init_sharing = __esm({
2236
2577
  3: 5,
2237
2578
  4: 10
2238
2579
  };
2239
- SHARING_FILE = join9(homedir5(), ".stellavault", "federation", "sharing.json");
2580
+ SHARING_FILE = join10(homedir6(), ".stellavault", "federation", "sharing.json");
2240
2581
  SENSITIVE_PATTERNS = [
2241
2582
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
2242
2583
  /\b\d{3}[-.]?\d{4}[-.]?\d{4}\b/,
@@ -2391,6 +2732,25 @@ var init_federation = __esm({
2391
2732
  }
2392
2733
  });
2393
2734
 
2735
+ // packages/core/dist/utils/math.js
2736
+ function cosineSimilarity(a, b) {
2737
+ if (a.length !== b.length)
2738
+ return 0;
2739
+ let dot = 0, normA = 0, normB = 0;
2740
+ for (let i = 0; i < a.length; i++) {
2741
+ dot += a[i] * b[i];
2742
+ normA += a[i] * a[i];
2743
+ normB += b[i] * b[i];
2744
+ }
2745
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
2746
+ return denom === 0 ? 0 : dot / denom;
2747
+ }
2748
+ var init_math = __esm({
2749
+ "packages/core/dist/utils/math.js"() {
2750
+ "use strict";
2751
+ }
2752
+ });
2753
+
2394
2754
  // packages/core/dist/intelligence/duplicate-detector.js
2395
2755
  var duplicate_detector_exports = {};
2396
2756
  __export(duplicate_detector_exports, {
@@ -2649,9 +3009,9 @@ async function extractTimedTranscriptFromHtml(html) {
2649
3009
  async function extractTimedTranscriptViaTool(videoId) {
2650
3010
  const { execSync: execSync3 } = await import("node:child_process");
2651
3011
  const { readdirSync: readdirSync9, readFileSync: readFileSync19, unlinkSync } = await import("node:fs");
2652
- const { join: join32 } = await import("node:path");
3012
+ const { join: join33 } = await import("node:path");
2653
3013
  const tmpDir = (await import("node:os")).tmpdir();
2654
- const tmpBase = join32(tmpDir, `sv-sub-${videoId}`);
3014
+ const tmpBase = join33(tmpDir, `sv-sub-${videoId}`);
2655
3015
  const url = `https://www.youtube.com/watch?v=${videoId}`;
2656
3016
  for (const lang of ["ko", "en"]) {
2657
3017
  try {
@@ -2662,10 +3022,10 @@ async function extractTimedTranscriptViaTool(videoId) {
2662
3022
  const files = readdirSync9(tmpDir).filter((f) => f.startsWith(`sv-sub-${videoId}`) && f.endsWith(".srv1"));
2663
3023
  if (files.length === 0)
2664
3024
  continue;
2665
- const xml = readFileSync19(join32(tmpDir, files[0]), "utf-8");
3025
+ const xml = readFileSync19(join33(tmpDir, files[0]), "utf-8");
2666
3026
  for (const f of files) {
2667
3027
  try {
2668
- unlinkSync(join32(tmpDir, f));
3028
+ unlinkSync(join33(tmpDir, f));
2669
3029
  } catch {
2670
3030
  }
2671
3031
  }
@@ -2860,18 +3220,18 @@ var init_youtube_extractor = __esm({
2860
3220
  });
2861
3221
 
2862
3222
  // 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";
3223
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync10, existsSync as existsSync10, statSync as statSync3 } from "node:fs";
3224
+ import { join as join11, resolve as resolve8, extname as extname4 } from "node:path";
2865
3225
  function scanFrontmatter(vaultPath, options) {
2866
3226
  const entries = [];
2867
3227
  const includeArchived = options?.includeArchived ?? false;
2868
3228
  function walkDir(dir, rel) {
2869
- if (!existsSync9(dir))
3229
+ if (!existsSync10(dir))
2870
3230
  return;
2871
3231
  for (const name of readdirSync4(dir)) {
2872
3232
  if (name.startsWith(".") || name === "node_modules")
2873
3233
  continue;
2874
- const full = join10(dir, name);
3234
+ const full = join11(dir, name);
2875
3235
  const relPath = rel ? `${rel}/${name}` : name;
2876
3236
  if (statSync3(full).isDirectory()) {
2877
3237
  walkDir(full, relPath);
@@ -2987,7 +3347,7 @@ function archiveFile(fullPath, vaultPath) {
2987
3347
  if (vaultPath && !resolved.startsWith(resolve8(vaultPath))) {
2988
3348
  throw new Error("Path traversal detected: file outside vault");
2989
3349
  }
2990
- if (!existsSync9(resolved))
3350
+ if (!existsSync10(resolved))
2991
3351
  throw new Error(`File not found: ${fullPath}`);
2992
3352
  const content = readFileSync9(resolved, "utf-8");
2993
3353
  if (content.startsWith("---\n")) {
@@ -3008,20 +3368,20 @@ var init_zettelkasten = __esm({
3008
3368
  });
3009
3369
 
3010
3370
  // 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";
3371
+ import { readdirSync as readdirSync5, readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
3372
+ import { join as join12, resolve as resolve9, extname as extname5 } from "node:path";
3013
3373
  function collectVaultTitles(vaultPath, folders = DEFAULT_FOLDERS) {
3014
3374
  const titles = /* @__PURE__ */ new Set();
3015
3375
  const dirs = [folders.fleeting, folders.literature, folders.permanent, folders.wiki, "_drafts"];
3016
3376
  for (const dir of dirs) {
3017
3377
  const fullDir = resolve9(vaultPath, dir);
3018
- if (!existsSync10(fullDir))
3378
+ if (!existsSync11(fullDir))
3019
3379
  continue;
3020
3380
  try {
3021
3381
  const files = readdirSync5(fullDir).filter((f) => extname5(f) === ".md");
3022
3382
  for (const file of files) {
3023
3383
  try {
3024
- const content = readFileSync10(join11(fullDir, file), "utf-8");
3384
+ const content = readFileSync10(join12(fullDir, file), "utf-8");
3025
3385
  const titleMatch = content.match(/^title:\s*"?([^"\n]+)"?\s*$/m);
3026
3386
  if (titleMatch && titleMatch[1].length > 2) {
3027
3387
  titles.add(titleMatch[1].trim());
@@ -3115,8 +3475,8 @@ __export(ingest_pipeline_exports, {
3115
3475
  ingestBatch: () => ingestBatch,
3116
3476
  promoteNote: () => promoteNote
3117
3477
  });
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";
3478
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
3479
+ import { join as join13, resolve as resolve10, basename as basename2 } from "node:path";
3120
3480
  function decodeHtmlEntities(text) {
3121
3481
  return text.replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&quot;/g, '"').replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&nbsp;/g, " ");
3122
3482
  }
@@ -3138,13 +3498,13 @@ function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
3138
3498
  };
3139
3499
  const folder = folderMap[autoStage];
3140
3500
  const dir = resolve10(vaultPath, folder);
3141
- if (!existsSync11(dir))
3142
- mkdirSync11(dir, { recursive: true });
3501
+ if (!existsSync12(dir))
3502
+ mkdirSync12(dir, { recursive: true });
3143
3503
  const now = /* @__PURE__ */ new Date();
3144
3504
  const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
3145
3505
  const slug = title.slice(0, 50).replace(/[^a-zA-Z0-9가-힣\s]/g, "").replace(/\s+/g, "-").toLowerCase();
3146
3506
  const filename = `${timestamp}-${slug}.md`;
3147
- const filePath = join12(folder, filename);
3507
+ const filePath = join13(folder, filename);
3148
3508
  const fullPath = resolve10(vaultPath, filePath);
3149
3509
  if (!fullPath.startsWith(resolve10(vaultPath))) {
3150
3510
  throw new Error("Invalid path");
@@ -3182,7 +3542,7 @@ function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
3182
3542
  try {
3183
3543
  const rawDir = resolve10(vaultPath, folders.fleeting);
3184
3544
  const wikiDir = resolve10(vaultPath, folders.wiki);
3185
- if (existsSync11(rawDir)) {
3545
+ if (existsSync12(rawDir)) {
3186
3546
  compileWiki(rawDir, wikiDir);
3187
3547
  }
3188
3548
  } catch (err) {
@@ -3202,7 +3562,7 @@ function ingestBatch(vaultPath, inputs) {
3202
3562
  }
3203
3563
  function promoteNote(vaultPath, filePath, targetStage, folders = DEFAULT_FOLDERS) {
3204
3564
  const fullPath = resolve10(vaultPath, filePath);
3205
- if (!existsSync11(fullPath))
3565
+ if (!existsSync12(fullPath))
3206
3566
  throw new Error(`File not found: ${filePath}`);
3207
3567
  const content = readFileSync11(fullPath, "utf-8");
3208
3568
  const updated = content.replace(/^type:\s*.+$/m, `type: ${targetStage}`);
@@ -3212,9 +3572,9 @@ function promoteNote(vaultPath, filePath, targetStage, folders = DEFAULT_FOLDERS
3212
3572
  permanent: folders.permanent
3213
3573
  };
3214
3574
  const newDir = resolve10(vaultPath, folderMap[targetStage]);
3215
- if (!existsSync11(newDir))
3216
- mkdirSync11(newDir, { recursive: true });
3217
- const newPath = join12(folderMap[targetStage], basename2(filePath));
3575
+ if (!existsSync12(newDir))
3576
+ mkdirSync12(newDir, { recursive: true });
3577
+ const newPath = join13(folderMap[targetStage], basename2(filePath));
3218
3578
  const newFullPath = resolve10(vaultPath, newPath);
3219
3579
  if (!newFullPath.startsWith(resolve10(vaultPath))) {
3220
3580
  throw new Error("Invalid path");
@@ -3640,14 +4000,14 @@ var init_file_extractors = __esm({
3640
4000
 
3641
4001
  // packages/cli/dist/mcp-clients.js
3642
4002
  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";
4003
+ import { existsSync as existsSync16, mkdirSync as mkdirSync17, readFileSync as readFileSync15, writeFileSync as writeFileSync16 } from "node:fs";
4004
+ import { dirname as dirname4, join as join22 } from "node:path";
4005
+ import { homedir as homedir14 } from "node:os";
3646
4006
  function appData() {
3647
- return process.env.APPDATA ?? join21(homedir13(), "AppData", "Roaming");
4007
+ return process.env.APPDATA ?? join22(homedir14(), "AppData", "Roaming");
3648
4008
  }
3649
4009
  function xdgConfig() {
3650
- return process.env.XDG_CONFIG_HOME ?? join21(homedir13(), ".config");
4010
+ return process.env.XDG_CONFIG_HOME ?? join22(homedir14(), ".config");
3651
4011
  }
3652
4012
  function resolveServeCommand(override) {
3653
4013
  if (override?.command) {
@@ -3660,26 +4020,26 @@ function resolveServeCommand(override) {
3660
4020
  }
3661
4021
  function claudeDesktopPath() {
3662
4022
  if (isWin)
3663
- return join21(appData(), "Claude", "claude_desktop_config.json");
4023
+ return join22(appData(), "Claude", "claude_desktop_config.json");
3664
4024
  if (isMac)
3665
- return join21(homedir13(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
3666
- return join21(xdgConfig(), "Claude", "claude_desktop_config.json");
4025
+ return join22(homedir14(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
4026
+ return join22(xdgConfig(), "Claude", "claude_desktop_config.json");
3667
4027
  }
3668
4028
  function vscodePath() {
3669
4029
  if (isWin)
3670
- return join21(appData(), "Code", "User", "mcp.json");
4030
+ return join22(appData(), "Code", "User", "mcp.json");
3671
4031
  if (isMac)
3672
- return join21(homedir13(), "Library", "Application Support", "Code", "User", "mcp.json");
3673
- return join21(xdgConfig(), "Code", "User", "mcp.json");
4032
+ return join22(homedir14(), "Library", "Application Support", "Code", "User", "mcp.json");
4033
+ return join22(xdgConfig(), "Code", "User", "mcp.json");
3674
4034
  }
3675
4035
  function isDetected(client) {
3676
- return existsSync15(client.detectDir);
4036
+ return existsSync16(client.detectDir);
3677
4037
  }
3678
4038
  function writeClientConfig(client, serve) {
3679
4039
  const path = client.configPath;
3680
4040
  try {
3681
4041
  let json = {};
3682
- if (existsSync15(path)) {
4042
+ if (existsSync16(path)) {
3683
4043
  const raw = readFileSync15(path, "utf-8").trim();
3684
4044
  if (raw)
3685
4045
  json = JSON.parse(raw);
@@ -3689,7 +4049,7 @@ function writeClientConfig(client, serve) {
3689
4049
  json[key] = {};
3690
4050
  const already = Boolean(json[key].stellavault);
3691
4051
  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 });
4052
+ mkdirSync17(dirname4(path), { recursive: true });
3693
4053
  writeFileSync16(path, JSON.stringify(json, null, 2) + "\n", "utf-8");
3694
4054
  return { client: client.label, status: already ? "updated" : "written", path };
3695
4055
  } catch (err) {
@@ -3733,16 +4093,16 @@ var init_mcp_clients = __esm({
3733
4093
  {
3734
4094
  id: "cursor",
3735
4095
  label: "Cursor",
3736
- configPath: join21(homedir13(), ".cursor", "mcp.json"),
3737
- detectDir: join21(homedir13(), ".cursor"),
4096
+ configPath: join22(homedir14(), ".cursor", "mcp.json"),
4097
+ detectDir: join22(homedir14(), ".cursor"),
3738
4098
  serversKey: "mcpServers",
3739
4099
  needsType: false
3740
4100
  },
3741
4101
  {
3742
4102
  id: "windsurf",
3743
4103
  label: "Windsurf",
3744
- configPath: join21(homedir13(), ".codeium", "windsurf", "mcp_config.json"),
3745
- detectDir: join21(homedir13(), ".codeium"),
4104
+ configPath: join22(homedir14(), ".codeium", "windsurf", "mcp_config.json"),
4105
+ detectDir: join22(homedir14(), ".codeium"),
3746
4106
  serversKey: "mcpServers",
3747
4107
  needsType: false
3748
4108
  },
@@ -3766,14 +4126,14 @@ __export(setup_cmd_exports, {
3766
4126
  setupCommand: () => setupCommand
3767
4127
  });
3768
4128
  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";
4129
+ import { existsSync as existsSync17 } from "node:fs";
4130
+ import { join as join23 } from "node:path";
4131
+ import { homedir as homedir15 } from "node:os";
3772
4132
  async function setupCommand(options) {
3773
4133
  console.log("");
3774
4134
  console.log(chalk4.bold(" \u2726 Stellavault \u2014 Connect to your AI clients"));
3775
4135
  console.log(chalk4.dim(" Registering Stellavault as an MCP server.\n"));
3776
- if (!existsSync16(join22(homedir14(), ".stellavault.json"))) {
4136
+ if (!existsSync17(join23(homedir15(), ".stellavault.json"))) {
3777
4137
  console.log(chalk4.yellow(" \u26A0 No config found. Run ") + chalk4.cyan("stellavault init") + chalk4.yellow(" first to index your vault.\n"));
3778
4138
  }
3779
4139
  const serve = resolveServeCommand({ command: options.command, args: options.args });
@@ -3835,9 +4195,9 @@ import ora from "ora";
3835
4195
  import chalk from "chalk";
3836
4196
  import { createHash as createHash7 } from "node:crypto";
3837
4197
  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";
4198
+ import { join as join21 } from "node:path";
4199
+ import { homedir as homedir13 } from "node:os";
4200
+ import { mkdirSync as mkdirSync16 } from "node:fs";
3841
4201
 
3842
4202
  // packages/core/dist/index.js
3843
4203
  init_config();
@@ -3847,13 +4207,21 @@ import Database from "better-sqlite3";
3847
4207
  import * as sqliteVec from "sqlite-vec";
3848
4208
  import { mkdirSync } from "node:fs";
3849
4209
  import { dirname } from "node:path";
4210
+ function loadVecExtension(db) {
4211
+ const loadablePath = sqliteVec.getLoadablePath();
4212
+ if (/\.asar[\\/]/.test(loadablePath)) {
4213
+ db.loadExtension(loadablePath.replace(/\.asar([\\/])/, ".asar.unpacked$1"));
4214
+ } else {
4215
+ sqliteVec.load(db);
4216
+ }
4217
+ }
3850
4218
  function createSqliteVecStore(dbPath, dimensions = 384) {
3851
4219
  let db;
3852
4220
  return {
3853
4221
  async initialize() {
3854
4222
  mkdirSync(dirname(dbPath), { recursive: true });
3855
4223
  db = new Database(dbPath);
3856
- sqliteVec.load(db);
4224
+ loadVecExtension(db);
3857
4225
  db.pragma("journal_mode = WAL");
3858
4226
  db.pragma("foreign_keys = ON");
3859
4227
  createTables(db, dimensions);
@@ -3989,6 +4357,19 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
3989
4357
  const rows = db.prepare("SELECT * FROM documents ORDER BY last_modified DESC").all();
3990
4358
  return rows.map(rowToDocument);
3991
4359
  },
4360
+ async getDocumentsMeta(maxDocs) {
4361
+ const lim = typeof maxDocs === "number" && Number.isFinite(maxDocs) && maxDocs > 0 ? Math.floor(maxDocs) : 0;
4362
+ const sql = "SELECT id, file_path, title, frontmatter, tags, last_modified FROM documents ORDER BY last_modified DESC" + (lim > 0 ? ` LIMIT ${lim}` : "");
4363
+ const rows = db.prepare(sql).all();
4364
+ return rows.map((r) => ({
4365
+ id: r.id,
4366
+ filePath: r.file_path,
4367
+ title: r.title,
4368
+ frontmatter: JSON.parse(r.frontmatter || "{}"),
4369
+ tags: JSON.parse(r.tags || "[]"),
4370
+ lastModified: r.last_modified
4371
+ }));
4372
+ },
3992
4373
  async getTopics() {
3993
4374
  const rows = db.prepare(`
3994
4375
  SELECT je.value as tag, COUNT(DISTINCT d.id) as count
@@ -4015,23 +4396,38 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
4015
4396
  };
4016
4397
  },
4017
4398
  async getDocumentEmbeddings(maxDocs = 1e4) {
4018
- const BATCH_SIZE = 500;
4019
4399
  const result = /* @__PURE__ */ new Map();
4020
- const stmt = db.prepare(`
4021
- SELECT c.document_id, ce.embedding
4400
+ const rows = db.prepare(`
4401
+ SELECT c.document_id AS document_id, ce.embedding AS embedding
4022
4402
  FROM chunks c
4023
4403
  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));
4404
+ WHERE c.id IN (SELECT MIN(id) FROM chunks GROUP BY document_id)
4405
+ LIMIT ?
4406
+ `).all(maxDocs);
4407
+ for (const row of rows) {
4408
+ result.set(row.document_id, bufferToFloat32(row.embedding));
4409
+ }
4410
+ return result;
4411
+ },
4412
+ async getDocumentEmbeddingsByIds(documentIds) {
4413
+ const result = /* @__PURE__ */ new Map();
4414
+ if (documentIds.length === 0)
4415
+ return result;
4416
+ const CHUNK = 800;
4417
+ for (let off = 0; off < documentIds.length; off += CHUNK) {
4418
+ const slice = documentIds.slice(off, off + CHUNK);
4419
+ const ph = slice.map(() => "?").join(",");
4420
+ 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);
4421
+ if (cidRows.length === 0)
4422
+ continue;
4423
+ const docByCid = new Map(cidRows.map((r) => [String(r.cid), r.d]));
4424
+ const cids = cidRows.map((r) => r.cid);
4425
+ const ph2 = cids.map(() => "?").join(",");
4426
+ const embRows = db.prepare(`SELECT chunk_id AS cid, embedding FROM chunk_embeddings WHERE chunk_id IN (${ph2})`).all(...cids);
4427
+ for (const row of embRows) {
4428
+ const docId = docByCid.get(String(row.cid));
4429
+ if (docId)
4430
+ result.set(docId, bufferToFloat32(row.embedding));
4035
4431
  }
4036
4432
  }
4037
4433
  return result;
@@ -4583,10 +4979,10 @@ async function handleGenerateClaudeMd(searchEngine, store, args) {
4583
4979
  }
4584
4980
 
4585
4981
  // 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");
4982
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
4983
+ import { join as join4, resolve as resolve2 } from "node:path";
4984
+ import { homedir as homedir3 } from "node:os";
4985
+ var SNAPSHOT_DIR = join4(homedir3(), ".stellavault", "snapshots");
4590
4986
  function sanitizeName(name) {
4591
4987
  const sanitized = name.replace(/[^a-zA-Z0-9가-힣_-]/g, "");
4592
4988
  if (!sanitized)
@@ -4624,7 +5020,7 @@ var loadSnapshotToolDef = {
4624
5020
  }
4625
5021
  };
4626
5022
  async function handleCreateSnapshot(searchEngine, args) {
4627
- mkdirSync2(SNAPSHOT_DIR, { recursive: true });
5023
+ mkdirSync3(SNAPSHOT_DIR, { recursive: true });
4628
5024
  const results = [];
4629
5025
  for (const query of args.queries) {
4630
5026
  const hits = await searchEngine.search({ query, limit: 5 });
@@ -4659,15 +5055,15 @@ async function handleCreateSnapshot(searchEngine, args) {
4659
5055
  async function handleLoadSnapshot(args) {
4660
5056
  const safeName = sanitizeName(args.name);
4661
5057
  const filePath = ensureWithinDir(SNAPSHOT_DIR, `${safeName}.json`);
4662
- if (!existsSync2(filePath)) {
5058
+ if (!existsSync3(filePath)) {
4663
5059
  return { error: `Snapshot not found: ${args.name}` };
4664
5060
  }
4665
5061
  return JSON.parse(readFileSync3(filePath, "utf-8"));
4666
5062
  }
4667
5063
 
4668
5064
  // 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";
5065
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
5066
+ import { join as join5, resolve as resolve3 } from "node:path";
4671
5067
  var logDecisionToolDef = {
4672
5068
  name: "log-decision",
4673
5069
  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 +5092,8 @@ var findDecisionsToolDef = {
4696
5092
  }
4697
5093
  };
4698
5094
  async function handleLogDecision(vaultPath, args) {
4699
- const decisionsDir = join4(vaultPath, "decisions");
4700
- mkdirSync3(decisionsDir, { recursive: true });
5095
+ const decisionsDir = join5(vaultPath, "decisions");
5096
+ mkdirSync4(decisionsDir, { recursive: true });
4701
5097
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4702
5098
  const slug = args.title.replace(/[^a-zA-Z가-힣0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, 50);
4703
5099
  if (!slug)
@@ -4738,13 +5134,13 @@ ${args.reasoning}
4738
5134
  return { saved: filePath, fileName };
4739
5135
  }
4740
5136
  async function handleFindDecisions(vaultPath, args) {
4741
- const decisionsDir = join4(vaultPath, "decisions");
4742
- if (!existsSync3(decisionsDir))
5137
+ const decisionsDir = join5(vaultPath, "decisions");
5138
+ if (!existsSync4(decisionsDir))
4743
5139
  return { decisions: [], message: "No decisions directory" };
4744
5140
  const files = readdirSync2(decisionsDir).filter((f) => f.endsWith(".md"));
4745
5141
  const query = args.query.toLowerCase();
4746
5142
  const matches = files.map((f) => {
4747
- const content = readFileSync4(join4(decisionsDir, f), "utf-8");
5143
+ const content = readFileSync4(join5(decisionsDir, f), "utf-8");
4748
5144
  const score = content.toLowerCase().includes(query) ? 1 : 0;
4749
5145
  return { file: f, content: content.slice(0, 300), score };
4750
5146
  }).filter((m) => m.score > 0).slice(0, 10);
@@ -4752,11 +5148,11 @@ async function handleFindDecisions(vaultPath, args) {
4752
5148
  }
4753
5149
 
4754
5150
  // packages/core/dist/mcp/tools/export.js
4755
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
5151
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync5 } from "node:fs";
4756
5152
  import { dirname as dirname2, resolve as resolve4 } from "node:path";
4757
- import { homedir as homedir3 } from "node:os";
5153
+ import { homedir as homedir4 } from "node:os";
4758
5154
  var ALLOWED_EXPORT_DIRS = [
4759
- resolve4(homedir3(), ".stellavault"),
5155
+ resolve4(homedir4(), ".stellavault"),
4760
5156
  resolve4(".")
4761
5157
  ];
4762
5158
  function validateExportPath(outputPath) {
@@ -4784,7 +5180,7 @@ async function handleExport(store, args) {
4784
5180
  const stats = await store.getStats();
4785
5181
  const topics = await store.getTopics();
4786
5182
  const safePath = validateExportPath(args.outputPath);
4787
- mkdirSync4(dirname2(safePath), { recursive: true });
5183
+ mkdirSync5(dirname2(safePath), { recursive: true });
4788
5184
  if (args.format === "csv") {
4789
5185
  const header = "id,filePath,title,tags,lastModified,contentHash";
4790
5186
  const rows = docs.map((d) => `"${d.id}","${d.filePath}","${d.title.replace(/"/g, '""')}","${d.tags.join(";")}","${d.lastModified}","${d.contentHash}"`);
@@ -4901,6 +5297,17 @@ function updateStability(currentS, difficulty, currentR) {
4901
5297
  const newS = currentS * (1 + Math.max(0, growth));
4902
5298
  return Math.min(newS, 365);
4903
5299
  }
5300
+ function updateStabilityGraded(currentS, difficulty, currentR, grade) {
5301
+ if (grade === 1) {
5302
+ const postLapse = FSRS_PARAMS.initialStability * 0.2 * Math.pow(difficulty, -FSRS_PARAMS.b);
5303
+ return Math.max(0.1, Math.min(postLapse, currentS));
5304
+ }
5305
+ const good = updateStability(currentS, difficulty, currentR);
5306
+ const baseGrowth = good - currentS;
5307
+ const gradeFactor = grade === 2 ? 0.5 : grade === 4 ? 1.8 : 1;
5308
+ const newS = currentS + baseGrowth * gradeFactor;
5309
+ return Math.min(newS, 365);
5310
+ }
4904
5311
  function estimateInitialStability(contentLength, connectionCount) {
4905
5312
  const base = FSRS_PARAMS.initialStability;
4906
5313
  const sizeBonus = Math.min(contentLength / 1e3, 10) * FSRS_PARAMS.sizeFactor;
@@ -4956,7 +5363,7 @@ var DecayEngine = class {
4956
5363
  if (existing) {
4957
5364
  const elapsed = elapsedDays(existing.last_access, now);
4958
5365
  const currentR = computeRetrievability(existing.stability, elapsed);
4959
- const newS = updateStability(existing.stability, existing.difficulty, currentR);
5366
+ const newS = event.grade ? updateStabilityGraded(existing.stability, existing.difficulty, currentR, event.grade) : updateStability(existing.stability, existing.difficulty, currentR);
4960
5367
  this.db.prepare(`
4961
5368
  UPDATE decay_state SET stability = ?, last_access = ?, retrievability = 1.0, updated_at = ?
4962
5369
  WHERE document_id = ?
@@ -5040,22 +5447,19 @@ var DecayEngine = class {
5040
5447
  * Get documents below decay threshold.
5041
5448
  */
5042
5449
  async getDecaying(threshold = 0.5, limit = 20) {
5043
- await this.computeAll();
5044
5450
  const rows = this.db.prepare(`
5045
5451
  SELECT ds.*, d.title FROM decay_state ds
5046
5452
  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);
5453
+ `).all();
5454
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5051
5455
  return rows.map((r) => ({
5052
5456
  documentId: r.document_id,
5053
5457
  stability: r.stability,
5054
5458
  difficulty: r.difficulty,
5055
5459
  lastAccess: r.last_access,
5056
- retrievability: r.retrievability,
5460
+ retrievability: computeRetrievability(r.stability, elapsedDays(r.last_access, now)),
5057
5461
  title: r.title
5058
- }));
5462
+ })).filter((r) => r.retrievability < threshold).sort((a, b) => a.retrievability - b.retrievability).slice(0, limit);
5059
5463
  }
5060
5464
  /**
5061
5465
  * Design Ref: §B3.3.3 — read-only live retrievability for a set of documents.
@@ -5277,14 +5681,34 @@ function invalidateGapCache(db) {
5277
5681
  } catch {
5278
5682
  }
5279
5683
  }
5684
+ var EMPTY_REPORT = { totalClusters: 0, totalGaps: 0, gaps: [], isolatedNodes: [] };
5685
+ function readAnyCachedGapReport(db) {
5686
+ try {
5687
+ ensureGapCacheTable(db);
5688
+ const row = db.prepare("SELECT * FROM gap_cache WHERE id = 1").get();
5689
+ if (!row || row.version !== CACHE_VERSION)
5690
+ return null;
5691
+ return JSON.parse(row.payload);
5692
+ } catch {
5693
+ return null;
5694
+ }
5695
+ }
5696
+ function triggerBackgroundCompute(store, db) {
5697
+ void computeAndCacheGaps(store, db).catch((err) => {
5698
+ console.warn("[gap-cache] background recompute failed:", err?.message ?? err);
5699
+ });
5700
+ }
5280
5701
  async function getGapReport(store, db, opts = {}) {
5281
5702
  if (!opts.forceRefresh) {
5282
- const cached = readCachedGapReport(db, opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS);
5283
- if (cached)
5284
- return { report: cached, fromCache: true };
5703
+ const fresh = readCachedGapReport(db, opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS);
5704
+ if (fresh)
5705
+ return { report: fresh, fromCache: true, computing: false };
5285
5706
  }
5286
- const fresh = await computeAndCacheGaps(store, db);
5287
- return { report: fresh, fromCache: false };
5707
+ triggerBackgroundCompute(store, db);
5708
+ const stale = readAnyCachedGapReport(db);
5709
+ if (stale)
5710
+ return { report: stale, fromCache: true, computing: true };
5711
+ return { report: EMPTY_REPORT, fromCache: false, computing: true };
5288
5712
  }
5289
5713
 
5290
5714
  // packages/core/dist/mcp/tools/detect-gaps.js
@@ -5309,7 +5733,7 @@ function createDetectGapsTool(store, getDb) {
5309
5733
  handler: async (args) => {
5310
5734
  const minSeverity = args.minSeverity ?? "medium";
5311
5735
  const db = getDb();
5312
- const { report, fromCache } = await getGapReport(store, db, { forceRefresh: args.forceRefresh });
5736
+ const { report, fromCache, computing } = await getGapReport(store, db, { forceRefresh: args.forceRefresh });
5313
5737
  const sevOrder = { high: 0, medium: 1, low: 2 };
5314
5738
  const threshold = sevOrder[minSeverity] ?? 1;
5315
5739
  const filtered = report.gaps.filter((g) => sevOrder[g.severity] <= threshold);
@@ -5327,8 +5751,8 @@ function createDetectGapsTool(store, getDb) {
5327
5751
  suggestedTopic: g.suggestedTopic
5328
5752
  })),
5329
5753
  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"
5754
+ 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.",
5755
+ cacheStatus: computing ? fromCache ? "stale (recomputing in background)" : "computing in background" : fromCache ? "cached" : "fresh"
5332
5756
  }, null, 2)
5333
5757
  }]
5334
5758
  };
@@ -5546,7 +5970,7 @@ function createAskTool(searchEngine, vaultPath) {
5546
5970
  init_wiki_compiler();
5547
5971
  init_config();
5548
5972
  import { resolve as resolve7 } from "node:path";
5549
- import { existsSync as existsSync6 } from "node:fs";
5973
+ import { existsSync as existsSync7 } from "node:fs";
5550
5974
  function createGenerateDraftTool(searchEngine, vaultPath) {
5551
5975
  return {
5552
5976
  name: "generate-draft",
@@ -5589,7 +6013,7 @@ function createGenerateDraftTool(searchEngine, vaultPath) {
5589
6013
  const allDocs = [];
5590
6014
  for (const dir of [folders.fleeting, folders.literature, folders.permanent, folders.wiki]) {
5591
6015
  const fullDir = resolve7(vaultPath, dir);
5592
- if (existsSync6(fullDir)) {
6016
+ if (existsSync7(fullDir)) {
5593
6017
  const docs = scanRawDirectory(fullDir);
5594
6018
  allDocs.push(...docs);
5595
6019
  }
@@ -5645,8 +6069,8 @@ Please write the ${format} draft based on the context above. Save the result to
5645
6069
  }
5646
6070
 
5647
6071
  // 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";
6072
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "node:fs";
6073
+ import { join as join8, resolve as resolvePath } from "node:path";
5650
6074
  function createAgenticGraphTools(store, embedder, vaultPath) {
5651
6075
  let nodeCreationCount = 0;
5652
6076
  let nodeCreationWindowStart = Date.now();
@@ -5717,14 +6141,14 @@ function createAgenticGraphTools(store, embedder, vaultPath) {
5717
6141
 
5718
6142
  ${content}${relatedSection}`;
5719
6143
  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`);
6144
+ const dir = join8(vaultPath, folder);
6145
+ const filePath = join8(dir, `${safeTitle}.md`);
5722
6146
  const resolvedPath = resolvePath(filePath);
5723
6147
  const resolvedVault = resolvePath(vaultPath);
5724
6148
  if (!resolvedPath.startsWith(resolvedVault)) {
5725
6149
  return { content: [{ type: "text", text: "Error: invalid folder path." }] };
5726
6150
  }
5727
- mkdirSync7(dir, { recursive: true });
6151
+ mkdirSync8(dir, { recursive: true });
5728
6152
  writeFileSync6(filePath, fullContent, "utf-8");
5729
6153
  const relatedCount = relatedSection ? relatedSection.split("\n").filter((l) => l.startsWith("- ")).length : 0;
5730
6154
  return {
@@ -5759,7 +6183,7 @@ The note will appear in the graph after next index.`
5759
6183
  return { content: [{ type: "text", text: `Source note "${args.sourceTitle}" not found.` }] };
5760
6184
  }
5761
6185
  const { readFileSync: readFileSync19 } = await import("node:fs");
5762
- const fullPath = join7(vaultPath, source.filePath);
6186
+ const fullPath = join8(vaultPath, source.filePath);
5763
6187
  let existing = "";
5764
6188
  try {
5765
6189
  existing = readFileSync19(fullPath, "utf-8");
@@ -5795,7 +6219,7 @@ function createMcpServer(options) {
5795
6219
  const askTool = createAskTool(searchEngine, vaultPath);
5796
6220
  const generateDraftTool = createGenerateDraftTool(searchEngine, vaultPath);
5797
6221
  const agenticTools = embedder ? createAgenticGraphTools(store, embedder, vaultPath) : [];
5798
- const server = new Server({ name: "stellavault", version: "0.8.3" }, { capabilities: { tools: {} } });
6222
+ const server = new Server({ name: "stellavault", version: "0.8.4" }, { capabilities: { tools: {} } });
5799
6223
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
5800
6224
  tools: [
5801
6225
  searchToolDef,
@@ -5821,6 +6245,14 @@ function createMcpServer(options) {
5821
6245
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
5822
6246
  await ready;
5823
6247
  const { name, arguments: args } = request.params;
6248
+ if (options.onToolCall) {
6249
+ try {
6250
+ const a = args ?? {};
6251
+ const detail = String(a.query ?? a.topic ?? a.title ?? a.documentId ?? a.id ?? "").slice(0, 80);
6252
+ options.onToolCall({ tool: name, detail });
6253
+ } catch {
6254
+ }
6255
+ }
5824
6256
  try {
5825
6257
  let result;
5826
6258
  switch (name) {
@@ -5919,6 +6351,9 @@ function createMcpServer(options) {
5919
6351
  const transport = new StdioServerTransport();
5920
6352
  await server.connect(transport);
5921
6353
  },
6354
+ // T3-3: returns a closable handle ({ port, close }) so an embedding host (the
6355
+ // desktop "Agent Memory" toggle) can stop the server. Backward compatible —
6356
+ // callers that ignore the return value are unaffected.
5922
6357
  async startHttp(port = 3334) {
5923
6358
  const { createServer } = await import("node:http");
5924
6359
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => `sv-${Date.now()}` });
@@ -5944,9 +6379,22 @@ function createMcpServer(options) {
5944
6379
  }
5945
6380
  await transport.handleRequest(req, res);
5946
6381
  });
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`);
6382
+ await new Promise((resolveListen, rejectListen) => {
6383
+ httpServer.once("error", rejectListen);
6384
+ httpServer.listen(port, "127.0.0.1", () => {
6385
+ httpServer.removeListener("error", rejectListen);
6386
+ console.error(`\u{1F50C} MCP HTTP server running at http://127.0.0.1:${port}/mcp`);
6387
+ resolveListen();
6388
+ });
5949
6389
  });
6390
+ return {
6391
+ port,
6392
+ close: () => new Promise((resolveClose) => {
6393
+ httpServer.close(() => resolveClose());
6394
+ void server.close().catch(() => {
6395
+ });
6396
+ })
6397
+ };
5950
6398
  },
5951
6399
  server
5952
6400
  };
@@ -6074,10 +6522,10 @@ function extractPackTags(chunks) {
6074
6522
  }
6075
6523
 
6076
6524
  // packages/core/dist/pack/exporter.js
6077
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "node:fs";
6525
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "node:fs";
6078
6526
  import { dirname as dirname3 } from "node:path";
6079
6527
  function exportPack(pack2, outputPath) {
6080
- mkdirSync8(dirname3(outputPath), { recursive: true });
6528
+ mkdirSync9(dirname3(outputPath), { recursive: true });
6081
6529
  writeFileSync7(outputPath, JSON.stringify(pack2, null, 2), "utf-8");
6082
6530
  }
6083
6531
  function packToSummary(pack2) {
@@ -6300,10 +6748,10 @@ function createKnowledgeRouter(opts) {
6300
6748
  return;
6301
6749
  }
6302
6750
  const { readFileSync: readFileSync19, writeFileSync: writeFileSync23, unlinkSync } = await import("node:fs");
6303
- const { join: join32, resolve: resolve22 } = await import("node:path");
6751
+ const { join: join33, resolve: resolve22 } = await import("node:path");
6304
6752
  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));
6753
+ const keeperPath = resolve22(join33(vaultPath, keeper.filePath));
6754
+ const removedPath = resolve22(join33(vaultPath, removed.filePath));
6307
6755
  const vaultRoot = resolve22(vaultPath);
6308
6756
  if (!keeperPath.startsWith(vaultRoot) || !removedPath.startsWith(vaultRoot)) {
6309
6757
  res.status(400).json({ error: "Invalid file path" });
@@ -6340,8 +6788,8 @@ ${removed.content}`;
6340
6788
  res.status(400).json({ error: "clusterA, clusterB required" });
6341
6789
  return;
6342
6790
  }
6343
- const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync23 } = await import("node:fs");
6344
- const { join: join32, resolve: resolve22 } = await import("node:path");
6791
+ const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync24 } = await import("node:fs");
6792
+ const { join: join33, resolve: resolve22 } = await import("node:path");
6345
6793
  const nameA = clusterA.replace(/\s*\(\d+\)$/, "");
6346
6794
  const nameB = clusterB.replace(/\s*\(\d+\)$/, "");
6347
6795
  const resultsA = await searchEngine.search({ query: nameA, limit: 3 });
@@ -6381,13 +6829,13 @@ ${removed.content}`;
6381
6829
  ""
6382
6830
  ].join("\n");
6383
6831
  const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ");
6384
- const dir = resolve22(join32(vaultPath, "01_Knowledge"));
6832
+ const dir = resolve22(join33(vaultPath, "01_Knowledge"));
6385
6833
  if (!dir.startsWith(resolve22(vaultPath))) {
6386
6834
  res.status(400).json({ error: "Invalid path" });
6387
6835
  return;
6388
6836
  }
6389
- mkdirSync23(dir, { recursive: true });
6390
- const filePath = join32(dir, `${safeTitle}.md`);
6837
+ mkdirSync24(dir, { recursive: true });
6838
+ const filePath = join33(dir, `${safeTitle}.md`);
6391
6839
  writeFileSync23(filePath, content, "utf-8");
6392
6840
  res.json({ success: true, title: safeTitle, path: filePath });
6393
6841
  } catch (err) {
@@ -6402,7 +6850,7 @@ ${removed.content}`;
6402
6850
  import { Router as Router3 } from "express";
6403
6851
  import { createHash as createHash5 } from "node:crypto";
6404
6852
  function createIngestRouter(opts) {
6405
- const { store, vaultPath, requireAuth, assertNotPrivateUrl } = opts;
6853
+ const { store, vaultPath, requireAuth, assertPublicUrl: assertPublicUrl2 } = opts;
6406
6854
  const router = Router3();
6407
6855
  router.post("/ingest", requireAuth, async (req, res) => {
6408
6856
  try {
@@ -6419,6 +6867,14 @@ function createIngestRouter(opts) {
6419
6867
  let autoTitle = title;
6420
6868
  let autoTags = tags ?? [];
6421
6869
  let autoStage = stage ?? "fleeting";
6870
+ if (input.startsWith("http")) {
6871
+ try {
6872
+ await assertPublicUrl2(input);
6873
+ } catch (e) {
6874
+ res.status(400).json({ error: e.message });
6875
+ return;
6876
+ }
6877
+ }
6422
6878
  const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(input);
6423
6879
  if (isYouTube) {
6424
6880
  try {
@@ -6442,12 +6898,6 @@ function createIngestRouter(opts) {
6442
6898
  }
6443
6899
  }
6444
6900
  } 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
6901
  try {
6452
6902
  const resp = await fetch(input, { signal: AbortSignal.timeout(8e3) });
6453
6903
  const html = await resp.text();
@@ -6539,10 +6989,10 @@ function createIngestRouter(opts) {
6539
6989
  }
6540
6990
  try {
6541
6991
  const { writeFileSync: writeFileSync23, unlinkSync } = await import("node:fs");
6542
- const { join: join32 } = await import("node:path");
6992
+ const { join: join33 } = await import("node:path");
6543
6993
  const { tmpdir } = await import("node:os");
6544
6994
  const safeName = (file.originalname ?? "upload").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 100);
6545
- const tmpPath = join32(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
6995
+ const tmpPath = join33(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
6546
6996
  writeFileSync23(tmpPath, file.buffer);
6547
6997
  const { extractFileContent: extractFileContent2 } = await Promise.resolve().then(() => (init_file_extractors(), file_extractors_exports));
6548
6998
  const ext = file.originalname.split(".").pop()?.toLowerCase() ?? "";
@@ -6621,7 +7071,7 @@ function createIngestRouter(opts) {
6621
7071
  return;
6622
7072
  }
6623
7073
  try {
6624
- assertNotPrivateUrl(url);
7074
+ await assertPublicUrl2(url);
6625
7075
  } catch (e) {
6626
7076
  res.status(400).json({ error: e.message });
6627
7077
  return;
@@ -6649,11 +7099,11 @@ ${desc}
6649
7099
  if (content.length > 1e4)
6650
7100
  content = content.slice(0, 1e4) + "\n\n...(truncated)";
6651
7101
  }
6652
- const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync23 } = await import("node:fs");
6653
- const { join: join32 } = await import("node:path");
7102
+ const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync24 } = await import("node:fs");
7103
+ const { join: join33 } = await import("node:path");
6654
7104
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6655
- const clipDir = join32(vaultPath || ".", "06_Research", "clips");
6656
- mkdirSync23(clipDir, { recursive: true });
7105
+ const clipDir = join33(vaultPath || ".", "06_Research", "clips");
7106
+ mkdirSync24(clipDir, { recursive: true });
6657
7107
  const fileName = `${date} ${safeTitle}.md`;
6658
7108
  const md = `---
6659
7109
  title: "${safeTitle}"
@@ -6667,8 +7117,8 @@ tags: [clip${isYT ? ", youtube" : ""}]
6667
7117
  > Source: ${url}
6668
7118
 
6669
7119
  ${content}`;
6670
- writeFileSync23(join32(clipDir, fileName), md, "utf-8");
6671
- res.json({ success: true, fileName, path: join32(clipDir, fileName) });
7120
+ writeFileSync23(join33(clipDir, fileName), md, "utf-8");
7121
+ res.json({ success: true, fileName, path: join33(clipDir, fileName) });
6672
7122
  } catch (err) {
6673
7123
  console.error(err);
6674
7124
  res.status(500).json({ error: "Clip failed" });
@@ -6921,6 +7371,123 @@ function createAnalyticsRouter(opts) {
6921
7371
  return router;
6922
7372
  }
6923
7373
 
7374
+ // packages/core/dist/api/ssrf-guard.js
7375
+ import { lookup } from "node:dns/promises";
7376
+ import { isIP } from "node:net";
7377
+ var defaultResolver = async (host) => {
7378
+ const records = await lookup(host, { all: true });
7379
+ return records.map((r) => r.address);
7380
+ };
7381
+ function ipv4ToInt(ip) {
7382
+ const parts = ip.split(".");
7383
+ if (parts.length !== 4)
7384
+ return null;
7385
+ let value = 0;
7386
+ for (const part of parts) {
7387
+ if (!/^\d{1,3}$/.test(part))
7388
+ return null;
7389
+ const octet = Number(part);
7390
+ if (octet > 255)
7391
+ return null;
7392
+ value = value * 256 + octet;
7393
+ }
7394
+ return value >>> 0;
7395
+ }
7396
+ function isPrivateIpv4(ip) {
7397
+ const value = ipv4ToInt(ip);
7398
+ if (value === null)
7399
+ return false;
7400
+ const a = value >>> 24 & 255;
7401
+ const b = value >>> 16 & 255;
7402
+ if (a === 0 || a === 10 || a === 127)
7403
+ return true;
7404
+ if (a === 169 && b === 254)
7405
+ return true;
7406
+ if (a === 172 && b >= 16 && b <= 31)
7407
+ return true;
7408
+ if (a === 192 && b === 168)
7409
+ return true;
7410
+ if (a === 100 && b >= 64 && b <= 127)
7411
+ return true;
7412
+ return false;
7413
+ }
7414
+ function extractMappedIpv4(ip) {
7415
+ const lower = ip.toLowerCase();
7416
+ const idx = lower.lastIndexOf("::ffff:");
7417
+ if (idx === -1)
7418
+ return null;
7419
+ const tail = lower.slice(idx + "::ffff:".length);
7420
+ if (isIP(tail) === 4)
7421
+ return tail;
7422
+ const hexMatch = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
7423
+ if (hexMatch) {
7424
+ const high = parseInt(hexMatch[1], 16);
7425
+ const low = parseInt(hexMatch[2], 16);
7426
+ return [high >> 8 & 255, high & 255, low >> 8 & 255, low & 255].join(".");
7427
+ }
7428
+ return null;
7429
+ }
7430
+ function isPrivateIp(ip) {
7431
+ const kind = isIP(ip);
7432
+ if (kind === 4)
7433
+ return isPrivateIpv4(ip);
7434
+ if (kind !== 6) {
7435
+ return true;
7436
+ }
7437
+ const lower = ip.toLowerCase();
7438
+ const mapped = extractMappedIpv4(lower);
7439
+ if (mapped !== null)
7440
+ return isPrivateIpv4(mapped);
7441
+ if (lower === "::" || lower === "0:0:0:0:0:0:0:0")
7442
+ return true;
7443
+ if (lower === "::1" || lower === "0:0:0:0:0:0:0:1")
7444
+ return true;
7445
+ const firstHextet = parseInt(lower.split(":")[0] || "0", 16);
7446
+ if (firstHextet >= 65152 && firstHextet <= 65215)
7447
+ return true;
7448
+ if (firstHextet >= 64512 && firstHextet <= 65023)
7449
+ return true;
7450
+ return false;
7451
+ }
7452
+ async function assertPublicUrl(url, resolver = defaultResolver) {
7453
+ let parsed;
7454
+ try {
7455
+ parsed = new URL(url);
7456
+ } catch {
7457
+ throw new Error("Internal or non-public URL not allowed");
7458
+ }
7459
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
7460
+ throw new Error("Only http/https URLs allowed");
7461
+ }
7462
+ let host = parsed.hostname;
7463
+ if (host.startsWith("[") && host.endsWith("]")) {
7464
+ host = host.slice(1, -1);
7465
+ }
7466
+ const lowerHost = host.toLowerCase();
7467
+ const h = lowerHost.replace(/\.$/, "");
7468
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local")) {
7469
+ throw new Error("Internal or non-public URL not allowed");
7470
+ }
7471
+ let addresses;
7472
+ if (isIP(host) !== 0) {
7473
+ addresses = [host];
7474
+ } else {
7475
+ try {
7476
+ addresses = await resolver(host);
7477
+ } catch {
7478
+ throw new Error("Internal or non-public URL not allowed");
7479
+ }
7480
+ }
7481
+ if (addresses.length === 0) {
7482
+ throw new Error("Internal or non-public URL not allowed");
7483
+ }
7484
+ for (const addr of addresses) {
7485
+ if (isPrivateIp(addr)) {
7486
+ throw new Error("Internal or non-public URL not allowed");
7487
+ }
7488
+ }
7489
+ }
7490
+
6924
7491
  // packages/core/dist/api/server.js
6925
7492
  function createApiServer(options) {
6926
7493
  const { store, searchEngine, port = 3333, vaultName = "", vaultPath = "", decayEngine, graphUiPath } = options;
@@ -6976,16 +7543,6 @@ function createApiServer(options) {
6976
7543
  }
6977
7544
  res.json({ token: authToken });
6978
7545
  });
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
7546
  if (graphUiPath) {
6990
7547
  app.use(express.static(graphUiPath, { index: "index.html", extensions: ["html"] }));
6991
7548
  }
@@ -7156,7 +7713,7 @@ function createApiServer(options) {
7156
7713
  res.status(404).json({ error: "Document not found" });
7157
7714
  return;
7158
7715
  }
7159
- const { resolve: resolve22, join: join32 } = await import("node:path");
7716
+ const { resolve: resolve22, join: join33 } = await import("node:path");
7160
7717
  const { writeFileSync: writeFileSync23, readFileSync: readFileSync19 } = await import("node:fs");
7161
7718
  const fullPath = resolve22(vaultPath, doc.filePath);
7162
7719
  if (!fullPath.startsWith(resolve22(vaultPath))) {
@@ -7207,13 +7764,13 @@ function createApiServer(options) {
7207
7764
  return;
7208
7765
  }
7209
7766
  const { resolve: resolve22 } = await import("node:path");
7210
- const { unlinkSync, existsSync: existsSync27 } = await import("node:fs");
7767
+ const { unlinkSync, existsSync: existsSync28 } = await import("node:fs");
7211
7768
  const fullPath = resolve22(vaultPath, doc.filePath);
7212
7769
  if (!fullPath.startsWith(resolve22(vaultPath))) {
7213
7770
  res.status(403).json({ error: "Access denied" });
7214
7771
  return;
7215
7772
  }
7216
- if (existsSync27(fullPath)) {
7773
+ if (existsSync28(fullPath)) {
7217
7774
  unlinkSync(fullPath);
7218
7775
  }
7219
7776
  await store.deleteByDocumentId(id);
@@ -7242,7 +7799,7 @@ function createApiServer(options) {
7242
7799
  res.status(500).json({ error: "Ask failed" });
7243
7800
  }
7244
7801
  });
7245
- app.use("/api", createIngestRouter({ store, vaultPath, requireAuth, assertNotPrivateUrl }));
7802
+ app.use("/api", createIngestRouter({ store, vaultPath, requireAuth, assertPublicUrl }));
7246
7803
  app.get("/api/recent", async (_req, res) => {
7247
7804
  try {
7248
7805
  const docs = await store.getAllDocuments();
@@ -7339,12 +7896,12 @@ function createApiServer(options) {
7339
7896
  const { resolve: resolve22 } = await import("node:path");
7340
7897
  const syncScript = resolve22(process.cwd(), "packages/sync/sync-to-obsidian.mjs");
7341
7898
  const syncDir = resolve22(process.cwd(), "packages/sync");
7342
- const { existsSync: existsSync27 } = await import("node:fs");
7343
- if (!existsSync27(syncScript)) {
7899
+ const { existsSync: existsSync28 } = await import("node:fs");
7900
+ if (!existsSync28(syncScript)) {
7344
7901
  res.json({ success: false, error: "sync script not found" });
7345
7902
  return;
7346
7903
  }
7347
- if (!existsSync27(resolve22(syncDir, ".env"))) {
7904
+ if (!existsSync28(resolve22(syncDir, ".env"))) {
7348
7905
  res.json({ success: false, error: ".env not found" });
7349
7906
  return;
7350
7907
  }
@@ -7393,6 +7950,7 @@ function createApiServer(options) {
7393
7950
  }
7394
7951
 
7395
7952
  // packages/core/dist/index.js
7953
+ init_graph_data();
7396
7954
  init_duplicate_detector();
7397
7955
  init_gap_detector();
7398
7956
 
@@ -7511,6 +8069,7 @@ function cosineSim(a, b) {
7511
8069
  // packages/core/dist/index.js
7512
8070
  init_ask_engine();
7513
8071
  init_wiki_compiler();
8072
+ init_auto_linker();
7514
8073
 
7515
8074
  // packages/core/dist/intelligence/knowledge-lint.js
7516
8075
  init_gap_detector();
@@ -7632,18 +8191,30 @@ async function lintKnowledge(store) {
7632
8191
  init_zettelkasten();
7633
8192
  init_ingest_pipeline();
7634
8193
 
8194
+ // packages/core/dist/intelligence/classify/classify.js
8195
+ init_math();
8196
+
8197
+ // packages/core/dist/intelligence/classify/cluster.js
8198
+ init_math();
8199
+
8200
+ // packages/core/dist/intelligence/classify/discover.js
8201
+ init_math();
8202
+
8203
+ // packages/core/dist/index.js
8204
+ init_file_extractors();
8205
+
7635
8206
  // 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");
8207
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync13, mkdirSync as mkdirSync13 } from "node:fs";
8208
+ import { join as join14 } from "node:path";
8209
+ import { homedir as homedir7 } from "node:os";
8210
+ var VAULTS_FILE = join14(homedir7(), ".stellavault", "vaults.json");
7640
8211
  function loadVaults() {
7641
- if (!existsSync12(VAULTS_FILE))
8212
+ if (!existsSync13(VAULTS_FILE))
7642
8213
  return [];
7643
8214
  return JSON.parse(readFileSync13(VAULTS_FILE, "utf-8"));
7644
8215
  }
7645
8216
  function saveVaults(vaults) {
7646
- mkdirSync12(join13(homedir6(), ".stellavault"), { recursive: true });
8217
+ mkdirSync13(join14(homedir7(), ".stellavault"), { recursive: true });
7647
8218
  writeFileSync12(VAULTS_FILE, JSON.stringify(vaults, null, 2), "utf-8");
7648
8219
  }
7649
8220
  function addVault(id, name, vaultPath, dbPath, shared = false) {
@@ -7674,7 +8245,7 @@ async function searchAllVaults(query, embedder, createStore, options = {}) {
7674
8245
  const embedding = await embedder.embed(query);
7675
8246
  const searches = vaults.map(async (vault2) => {
7676
8247
  try {
7677
- if (!existsSync12(vault2.dbPath))
8248
+ if (!existsSync13(vault2.dbPath))
7678
8249
  return;
7679
8250
  const store = createStore(vault2.dbPath);
7680
8251
  await store.initialize();
@@ -7703,8 +8274,8 @@ async function searchAllVaults(query, embedder, createStore, options = {}) {
7703
8274
 
7704
8275
  // packages/core/dist/capture/voice.js
7705
8276
  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";
8277
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, existsSync as existsSync14 } from "node:fs";
8278
+ import { join as join15, basename as basename4 } from "node:path";
7708
8279
  var ALLOWED_MODELS = ["tiny", "base", "small", "medium", "large"];
7709
8280
  var ALLOWED_LANGUAGES = ["auto", "en", "ko", "ja", "zh", "es", "fr", "de", "it", "pt", "ru", "ar", "hi"];
7710
8281
  function validateModel(model) {
@@ -7727,7 +8298,7 @@ function isWhisperAvailable() {
7727
8298
  }
7728
8299
  async function transcribeAudio(audioPath, options = {}) {
7729
8300
  const { model = "base", language } = options;
7730
- if (!existsSync13(audioPath)) {
8301
+ if (!existsSync14(audioPath)) {
7731
8302
  throw new Error(`Audio file not found: ${audioPath}`);
7732
8303
  }
7733
8304
  if (isWhisperAvailable()) {
@@ -7735,12 +8306,12 @@ async function transcribeAudio(audioPath, options = {}) {
7735
8306
  const args = [audioPath, "--model", safeModel, "--output_format", "txt", "--output_dir", "/tmp/sv-whisper"];
7736
8307
  if (language)
7737
8308
  args.push("--language", validateLanguage(language));
7738
- mkdirSync13("/tmp/sv-whisper", { recursive: true });
8309
+ mkdirSync14("/tmp/sv-whisper", { recursive: true });
7739
8310
  try {
7740
8311
  execFileSync("whisper", args, { stdio: "pipe", timeout: 3e5 });
7741
8312
  const outputName = basename4(audioPath).replace(/\.[^.]+$/, ".txt");
7742
8313
  const { readFileSync: readFileSync19 } = await import("node:fs");
7743
- return readFileSync19(join14("/tmp/sv-whisper", outputName), "utf-8").trim();
8314
+ return readFileSync19(join15("/tmp/sv-whisper", outputName), "utf-8").trim();
7744
8315
  } catch (err) {
7745
8316
  throw new Error(`Whisper failed: ${err instanceof Error ? err.message : err}`);
7746
8317
  }
@@ -7797,9 +8368,9 @@ async function captureVoice(audioPath, options) {
7797
8368
  transcript,
7798
8369
  ""
7799
8370
  ].join("\n");
7800
- const dir = join14(vaultPath, folder);
7801
- mkdirSync13(dir, { recursive: true });
7802
- const filePath = join14(dir, `${date.slice(0, 10)} ${safeTitle}.md`);
8371
+ const dir = join15(vaultPath, folder);
8372
+ mkdirSync14(dir, { recursive: true });
8373
+ const filePath = join15(dir, `${date.slice(0, 10)} ${safeTitle}.md`);
7803
8374
  writeFileSync13(filePath, content, "utf-8");
7804
8375
  return {
7805
8376
  title: safeTitle,
@@ -7822,12 +8393,12 @@ async function captureVoice(audioPath, options) {
7822
8393
 
7823
8394
  // packages/core/dist/cloud/sync.js
7824
8395
  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");
8396
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync15, mkdirSync as mkdirSync15, chmodSync as chmodSync2 } from "node:fs";
8397
+ import { join as join16 } from "node:path";
8398
+ import { homedir as homedir8 } from "node:os";
8399
+ var CLOUD_DIR = join16(homedir8(), ".stellavault", "cloud");
8400
+ var KEY_FILE = join16(CLOUD_DIR, "encryption.key");
8401
+ var SYNC_STATE_FILE = join16(CLOUD_DIR, "sync-state.json");
7831
8402
  function encrypt(data, key) {
7832
8403
  const iv = randomBytes4(16);
7833
8404
  const cipher = createCipheriv("aes-256-gcm", key, iv);
@@ -7841,13 +8412,13 @@ function decrypt(encrypted, key, iv, tag) {
7841
8412
  return Buffer.concat([decipher.update(encrypted), decipher.final()]);
7842
8413
  }
7843
8414
  function getOrCreateEncryptionKey(userKey) {
7844
- mkdirSync14(CLOUD_DIR, { recursive: true });
8415
+ mkdirSync15(CLOUD_DIR, { recursive: true });
7845
8416
  if (userKey) {
7846
8417
  const key2 = createHash6("sha256").update(userKey).digest();
7847
8418
  writeFileSync14(KEY_FILE, key2.toString("hex"), "utf-8");
7848
8419
  return key2;
7849
8420
  }
7850
- if (existsSync14(KEY_FILE)) {
8421
+ if (existsSync15(KEY_FILE)) {
7851
8422
  return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
7852
8423
  }
7853
8424
  const key = randomBytes4(32);
@@ -7897,7 +8468,7 @@ async function s3Get(config, objectKey) {
7897
8468
  async function syncToCloud(dbPath, config) {
7898
8469
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7899
8470
  try {
7900
- if (!existsSync14(dbPath)) {
8471
+ if (!existsSync15(dbPath)) {
7901
8472
  return { action: "upload", dbSize: 0, encryptedSize: 0, timestamp, success: false, error: "DB not found" };
7902
8473
  }
7903
8474
  const dbData = readFileSync14(dbPath);
@@ -7907,7 +8478,7 @@ async function syncToCloud(dbPath, config) {
7907
8478
  const objectKey = `stellavault/index.db.enc`;
7908
8479
  const success = await s3Put(config, objectKey, payload);
7909
8480
  const state = { lastSync: timestamp, dbSize: dbData.length, encryptedSize: payload.length, objectKey };
7910
- mkdirSync14(CLOUD_DIR, { recursive: true });
8481
+ mkdirSync15(CLOUD_DIR, { recursive: true });
7911
8482
  writeFileSync14(SYNC_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
7912
8483
  return { action: "upload", dbSize: dbData.length, encryptedSize: payload.length, timestamp, success };
7913
8484
  } catch (err) {
@@ -7927,7 +8498,7 @@ async function restoreFromCloud(dbPath, config) {
7927
8498
  const tag = payload.subarray(16, 32);
7928
8499
  const encrypted = payload.subarray(32);
7929
8500
  const dbData = decrypt(encrypted, key, iv, tag);
7930
- if (existsSync14(dbPath)) {
8501
+ if (existsSync15(dbPath)) {
7931
8502
  writeFileSync14(dbPath + ".backup", readFileSync14(dbPath));
7932
8503
  }
7933
8504
  writeFileSync14(dbPath, dbData);
@@ -7937,40 +8508,40 @@ async function restoreFromCloud(dbPath, config) {
7937
8508
  }
7938
8509
  }
7939
8510
  function getSyncState() {
7940
- if (!existsSync14(SYNC_STATE_FILE))
8511
+ if (!existsSync15(SYNC_STATE_FILE))
7941
8512
  return null;
7942
8513
  return JSON.parse(readFileSync14(SYNC_STATE_FILE, "utf-8"));
7943
8514
  }
7944
8515
 
7945
8516
  // 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");
8517
+ import { join as join17 } from "node:path";
8518
+ import { homedir as homedir9 } from "node:os";
8519
+ var TEAM_DIR = join17(homedir9(), ".stellavault", "team");
8520
+ var TEAM_FILE = join17(TEAM_DIR, "team.json");
7950
8521
 
7951
8522
  // packages/core/dist/index.js
7952
8523
  init_federation();
7953
8524
 
7954
8525
  // 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");
8526
+ import { join as join18 } from "node:path";
8527
+ import { homedir as homedir10 } from "node:os";
8528
+ var TRUST_FILE = join18(homedir10(), ".stellavault", "federation", "trust.json");
7958
8529
 
7959
8530
  // packages/core/dist/index.js
7960
8531
  init_sharing();
7961
8532
 
7962
8533
  // 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");
8534
+ import { join as join19 } from "node:path";
8535
+ import { homedir as homedir11 } from "node:os";
8536
+ var REP_FILE = join19(homedir11(), ".stellavault", "federation", "reputation.json");
7966
8537
 
7967
8538
  // packages/core/dist/index.js
7968
8539
  init_privacy();
7969
8540
 
7970
8541
  // 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");
8542
+ import { join as join20 } from "node:path";
8543
+ import { homedir as homedir12 } from "node:os";
8544
+ var CREDITS_FILE = join20(homedir12(), ".stellavault", "federation", "credits.json");
7974
8545
 
7975
8546
  // packages/core/dist/index.js
7976
8547
  init_retry();
@@ -8006,15 +8577,15 @@ function createKnowledgeHub(config, options = {}) {
8006
8577
  // B2.2 — cross-lingual/synonym groups
8007
8578
  });
8008
8579
  const mcpServer = createMcpServer({ store, searchEngine, vaultPath: config.vaultPath, ready: options.ready });
8009
- return { store, embedder, searchEngine, mcpServer, config };
8580
+ return { store, embedder, searchEngine, mcpServer, config, getDecayEngine };
8010
8581
  }
8011
8582
 
8012
8583
  // packages/cli/dist/commands/index-cmd.js
8013
8584
  function getVaultDbPath(vaultPath) {
8014
8585
  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`);
8586
+ const dir = join21(homedir13(), ".stellavault", "vaults");
8587
+ mkdirSync16(dir, { recursive: true });
8588
+ return join21(dir, `${hash}.db`);
8018
8589
  }
8019
8590
  function resolveDbPath(vault2, configDbPath) {
8020
8591
  const envDbPath = process.env.STELLAVAULT_DB_PATH?.trim();
@@ -8305,19 +8876,19 @@ async function statusCommand(_opts, cmd) {
8305
8876
  import chalk6 from "chalk";
8306
8877
  import { spawn } from "node:child_process";
8307
8878
  import { resolve as resolve11, dirname as dirname5 } from "node:path";
8308
- import { existsSync as existsSync17 } from "node:fs";
8879
+ import { existsSync as existsSync18 } from "node:fs";
8309
8880
  import { fileURLToPath } from "node:url";
8310
8881
  function locateBundledGraphUi() {
8311
8882
  try {
8312
8883
  const here = dirname5(fileURLToPath(import.meta.url));
8313
8884
  const bundled = resolve11(here, "graph-ui");
8314
- if (existsSync17(resolve11(bundled, "index.html")))
8885
+ if (existsSync18(resolve11(bundled, "index.html")))
8315
8886
  return bundled;
8316
8887
  const monorepoGraphDist = resolve11(here, "..", "..", "..", "graph", "dist");
8317
- if (existsSync17(resolve11(monorepoGraphDist, "index.html")))
8888
+ if (existsSync18(resolve11(monorepoGraphDist, "index.html")))
8318
8889
  return monorepoGraphDist;
8319
8890
  const singleFileBundle = resolve11(here, "..", "dist", "graph-ui");
8320
- if (existsSync17(resolve11(singleFileBundle, "index.html")))
8891
+ if (existsSync18(resolve11(singleFileBundle, "index.html")))
8321
8892
  return singleFileBundle;
8322
8893
  } catch {
8323
8894
  }
@@ -8368,7 +8939,7 @@ async function graphCommand() {
8368
8939
  return;
8369
8940
  }
8370
8941
  const devGraphDir = resolve11(process.cwd(), "packages/graph");
8371
- const hasDevGraph = existsSync17(resolve11(devGraphDir, "package.json"));
8942
+ const hasDevGraph = existsSync18(resolve11(devGraphDir, "package.json"));
8372
8943
  if (hasDevGraph) {
8373
8944
  console.error(chalk6.dim(" \u{1F680} Starting Vite dev server..."));
8374
8945
  const vite = spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["vite", "--host"], {
@@ -8430,10 +9001,10 @@ async function cardCommand(options) {
8430
9001
 
8431
9002
  // packages/cli/dist/commands/pack-cmd.js
8432
9003
  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");
9004
+ import { resolve as resolve13, join as join24 } from "node:path";
9005
+ import { readdirSync as readdirSync6, existsSync as existsSync19, readFileSync as readFileSync16, mkdirSync as mkdirSync18 } from "node:fs";
9006
+ import { homedir as homedir16 } from "node:os";
9007
+ var PACKS_DIR = join24(homedir16(), ".stellavault", "packs");
8437
9008
  async function packCreateCommand(name, options) {
8438
9009
  const config = loadConfig();
8439
9010
  const hub = createKnowledgeHub(config);
@@ -8449,8 +9020,8 @@ async function packCreateCommand(name, options) {
8449
9020
  description: options.description,
8450
9021
  limit: options.limit ? parseInt(options.limit) : 100
8451
9022
  });
8452
- mkdirSync17(PACKS_DIR, { recursive: true });
8453
- const outPath = join23(PACKS_DIR, `${name}.sv-pack`);
9023
+ mkdirSync18(PACKS_DIR, { recursive: true });
9024
+ const outPath = join24(PACKS_DIR, `${name}.sv-pack`);
8454
9025
  exportPack(pack2, outPath);
8455
9026
  console.error(chalk8.green(`\u2705 Pack created: ${name}`));
8456
9027
  console.error(` \u{1F4E6} ${pack2.chunks.length} chunks`);
@@ -8461,8 +9032,8 @@ async function packCreateCommand(name, options) {
8461
9032
  await hub.store.close();
8462
9033
  }
8463
9034
  async function packExportCommand(name, options) {
8464
- const srcPath = join23(PACKS_DIR, `${name}.sv-pack`);
8465
- if (!existsSync18(srcPath)) {
9035
+ const srcPath = join24(PACKS_DIR, `${name}.sv-pack`);
9036
+ if (!existsSync19(srcPath)) {
8466
9037
  console.error(chalk8.red(`\u274C Pack not found: ${name}`));
8467
9038
  process.exit(1);
8468
9039
  }
@@ -8474,7 +9045,7 @@ async function packExportCommand(name, options) {
8474
9045
  }
8475
9046
  async function packImportCommand(filePath) {
8476
9047
  const absPath = resolve13(process.cwd(), filePath);
8477
- if (!existsSync18(absPath)) {
9048
+ if (!existsSync19(absPath)) {
8478
9049
  console.error(chalk8.red(`\u274C File not found: ${absPath}`));
8479
9050
  process.exit(1);
8480
9051
  }
@@ -8493,7 +9064,7 @@ async function packImportCommand(filePath) {
8493
9064
  await hub.store.close();
8494
9065
  }
8495
9066
  async function packListCommand() {
8496
- mkdirSync17(PACKS_DIR, { recursive: true });
9067
+ mkdirSync18(PACKS_DIR, { recursive: true });
8497
9068
  const files = readdirSync6(PACKS_DIR).filter((f) => f.endsWith(".sv-pack"));
8498
9069
  if (files.length === 0) {
8499
9070
  console.error(chalk8.dim("No packs found. Create one: stellavault pack create <name> --from-search <query>"));
@@ -8503,7 +9074,7 @@ async function packListCommand() {
8503
9074
  `));
8504
9075
  for (const file of files) {
8505
9076
  try {
8506
- const pack2 = JSON.parse(readFileSync16(join23(PACKS_DIR, file), "utf-8"));
9077
+ const pack2 = JSON.parse(readFileSync16(join24(PACKS_DIR, file), "utf-8"));
8507
9078
  console.error(` ${chalk8.bold(pack2.name)} v${pack2.version} \u2014 ${pack2.chunks.length} chunks (${pack2.license})`);
8508
9079
  } catch {
8509
9080
  console.error(` ${file} (invalid)`);
@@ -8511,8 +9082,8 @@ async function packListCommand() {
8511
9082
  }
8512
9083
  }
8513
9084
  async function packInfoCommand(name) {
8514
- const filePath = join23(PACKS_DIR, `${name}.sv-pack`);
8515
- if (!existsSync18(filePath)) {
9085
+ const filePath = join24(PACKS_DIR, `${name}.sv-pack`);
9086
+ if (!existsSync19(filePath)) {
8516
9087
  console.error(chalk8.red(`\u274C Pack not found: ${name}`));
8517
9088
  process.exit(1);
8518
9089
  }
@@ -8570,24 +9141,24 @@ async function decayCommand(_opts, cmd) {
8570
9141
  import chalk10 from "chalk";
8571
9142
  import { spawn as spawn2 } from "node:child_process";
8572
9143
  import { resolve as resolve14 } from "node:path";
8573
- import { existsSync as existsSync19 } from "node:fs";
9144
+ import { existsSync as existsSync20 } from "node:fs";
8574
9145
  async function syncCommand(options) {
8575
9146
  const syncDir = resolve14(process.cwd(), "packages/sync");
8576
9147
  const syncScript = resolve14(syncDir, "sync-to-obsidian.mjs");
8577
- if (!existsSync19(syncScript)) {
9148
+ if (!existsSync20(syncScript)) {
8578
9149
  console.error(chalk10.red("\u274C packages/sync/sync-to-obsidian.mjs not found"));
8579
9150
  console.error(chalk10.dim(" Run from project root: cd notion-obsidian-sync"));
8580
9151
  process.exit(1);
8581
9152
  }
8582
9153
  const envFile = resolve14(syncDir, ".env");
8583
- if (!existsSync19(envFile)) {
9154
+ if (!existsSync20(envFile)) {
8584
9155
  console.error(chalk10.red("\u274C packages/sync/.env not found"));
8585
9156
  console.error(chalk10.dim(" Copy .env.example \u2192 .env and set NOTION_TOKEN"));
8586
9157
  process.exit(1);
8587
9158
  }
8588
9159
  if (options.upload) {
8589
9160
  const uploadScript = resolve14(syncDir, "upload-pdca-to-notion.mjs");
8590
- if (!existsSync19(uploadScript)) {
9161
+ if (!existsSync20(uploadScript)) {
8591
9162
  console.error(chalk10.red("\u274C upload-pdca-to-notion.mjs not found"));
8592
9163
  process.exit(1);
8593
9164
  }
@@ -8839,8 +9410,8 @@ async function gapsCommand() {
8839
9410
 
8840
9411
  // packages/cli/dist/commands/clip-cmd.js
8841
9412
  import chalk14 from "chalk";
8842
- import { writeFileSync as writeFileSync18, mkdirSync as mkdirSync18 } from "node:fs";
8843
- import { join as join24 } from "node:path";
9413
+ import { writeFileSync as writeFileSync18, mkdirSync as mkdirSync19 } from "node:fs";
9414
+ import { join as join25 } from "node:path";
8844
9415
  async function clipCommand(url, options) {
8845
9416
  if (!url) {
8846
9417
  console.error(chalk14.red("\u274C Please provide a URL: stellavault clip <url>"));
@@ -8853,9 +9424,15 @@ async function clipCommand(url, options) {
8853
9424
  process.exit(1);
8854
9425
  }
8855
9426
  const folder = options.folder ?? "06_Research/clips";
8856
- const targetDir = join24(vaultPath, folder);
8857
- mkdirSync18(targetDir, { recursive: true });
9427
+ const targetDir = join25(vaultPath, folder);
9428
+ mkdirSync19(targetDir, { recursive: true });
8858
9429
  console.error(chalk14.dim(`\u{1F4CE} Clipping: ${url}`));
9430
+ try {
9431
+ await assertPublicUrl(url);
9432
+ } catch {
9433
+ console.error(chalk14.yellow("Private/local or non-public URLs are not allowed for security."));
9434
+ process.exit(1);
9435
+ }
8859
9436
  try {
8860
9437
  const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(url);
8861
9438
  let title;
@@ -8872,7 +9449,7 @@ async function clipCommand(url, options) {
8872
9449
  const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
8873
9450
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8874
9451
  const fileName = `${date} ${safeTitle}.md`;
8875
- const filePath = join24(targetDir, fileName);
9452
+ const filePath = join25(targetDir, fileName);
8876
9453
  const md = [
8877
9454
  "---",
8878
9455
  `title: "${safeTitle}"`,
@@ -9089,14 +9666,14 @@ async function digestCommand(options) {
9089
9666
  \u{1F9E0} Health: R=${report.averageR} | Decaying ${report.decayingCount} | Critical ${report.criticalCount}`);
9090
9667
  console.log(chalk16.dim("\n\u2550".repeat(50)));
9091
9668
  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");
9669
+ const { writeFileSync: writeFileSync23, mkdirSync: mkdirSync24, existsSync: existsSync28 } = await import("node:fs");
9670
+ const { join: join33, resolve: resolve22 } = await import("node:path");
9094
9671
  const outputDir = resolve22(config.vaultPath, "_stellavault/digests");
9095
- if (!existsSync27(outputDir))
9096
- mkdirSync23(outputDir, { recursive: true });
9672
+ if (!existsSync28(outputDir))
9673
+ mkdirSync24(outputDir, { recursive: true });
9097
9674
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9098
9675
  const filename = `digest-${date}.md`;
9099
- const outputPath = join32(outputDir, filename);
9676
+ const outputPath = join33(outputDir, filename);
9100
9677
  const pieData = (typeStats.length > 0 ? typeStats : [{ type: "note", cnt: 1 }]).map((t2) => ` "${t2.type}" : ${t2.cnt}`).join("\n");
9101
9678
  const timelineData = dailyActivity.map((d) => ` ${d.day.slice(5)} : ${d.cnt}`).join("\n");
9102
9679
  const md = [
@@ -9143,9 +9720,9 @@ Visual digest saved: ${filename}`));
9143
9720
 
9144
9721
  // packages/cli/dist/commands/init-cmd.js
9145
9722
  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";
9723
+ import { existsSync as existsSync21, mkdirSync as mkdirSync20, writeFileSync as writeFileSync19 } from "node:fs";
9724
+ import { join as join26 } from "node:path";
9725
+ import { homedir as homedir17 } from "node:os";
9149
9726
  import ora2 from "ora";
9150
9727
  import chalk17 from "chalk";
9151
9728
  function ask(rl, question, defaultVal) {
@@ -9171,8 +9748,8 @@ async function initCommand() {
9171
9748
  console.log(chalk17.yellow(" Please enter your vault path."));
9172
9749
  continue;
9173
9750
  }
9174
- const resolved = input.replace(/^~/, homedir16());
9175
- if (!existsSync20(resolved)) {
9751
+ const resolved = input.replace(/^~/, homedir17());
9752
+ if (!existsSync21(resolved)) {
9176
9753
  console.log(chalk17.yellow(` Path not found: ${resolved}`));
9177
9754
  continue;
9178
9755
  }
@@ -9180,9 +9757,9 @@ async function initCommand() {
9180
9757
  }
9181
9758
  console.log(chalk17.green(` \u2713 Vault: ${vaultPath}
9182
9759
  `));
9183
- const configDir = join25(homedir16(), ".stellavault");
9184
- mkdirSync19(configDir, { recursive: true });
9185
- const dbPath = join25(configDir, "index.db");
9760
+ const configDir = join26(homedir17(), ".stellavault");
9761
+ mkdirSync20(configDir, { recursive: true });
9762
+ const dbPath = join26(configDir, "index.db");
9186
9763
  const configData = {
9187
9764
  vaultPath,
9188
9765
  dbPath,
@@ -9191,7 +9768,7 @@ async function initCommand() {
9191
9768
  search: { defaultLimit: 10, rrfK: 60 },
9192
9769
  mcp: { mode: "stdio", port: 3333 }
9193
9770
  };
9194
- writeFileSync19(join25(homedir16(), ".stellavault.json"), JSON.stringify(configData, null, 2), "utf-8");
9771
+ writeFileSync19(join26(homedir17(), ".stellavault.json"), JSON.stringify(configData, null, 2), "utf-8");
9195
9772
  console.log(chalk17.dim(` Config saved: ~/.stellavault.json`));
9196
9773
  console.log("");
9197
9774
  console.log(chalk17.cyan(" Step 2/3") + " \u2014 Indexing your vault");
@@ -9215,8 +9792,8 @@ async function initCommand() {
9215
9792
  spinner.succeed(chalk17.green(` Indexed ${result.indexed} docs, ${result.totalChunks} chunks (${(result.elapsedMs / 1e3).toFixed(1)}s)`));
9216
9793
  if (result.indexed === 0) {
9217
9794
  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 });
9795
+ const rawDir = join26(vaultPath, "raw");
9796
+ mkdirSync20(rawDir, { recursive: true });
9220
9797
  const samples = [
9221
9798
  {
9222
9799
  file: "welcome-to-stellavault.md",
@@ -9289,7 +9866,7 @@ Andrej Karpathy's approach: every session auto-compiles into structured knowledg
9289
9866
  }
9290
9867
  ];
9291
9868
  for (const s of samples) {
9292
- writeFileSync19(join25(rawDir, s.file), s.content, "utf-8");
9869
+ writeFileSync19(join26(rawDir, s.file), s.content, "utf-8");
9293
9870
  }
9294
9871
  const reSpinner = ora2({ text: " Indexing sample notes...", indent: 2 }).start();
9295
9872
  const reResult = await indexVault(vaultPath, {
@@ -9950,8 +10527,8 @@ import chalk26 from "chalk";
9950
10527
  // packages/core/dist/intelligence/draft-generator.js
9951
10528
  init_wiki_compiler();
9952
10529
  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";
10530
+ import { writeFileSync as writeFileSync20, mkdirSync as mkdirSync21, existsSync as existsSync22 } from "node:fs";
10531
+ import { join as join27, resolve as resolve16, basename as basename5, extname as extname7 } from "node:path";
9955
10532
  function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
9956
10533
  const { topic, format = "blog", maxSections = 8, blueprint } = options;
9957
10534
  const rawDir = resolve16(vaultPath, folders.fleeting);
@@ -9959,7 +10536,7 @@ function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
9959
10536
  const litDir = resolve16(vaultPath, folders.literature);
9960
10537
  const allDocs = [];
9961
10538
  for (const dir of [rawDir, wikiDir, litDir]) {
9962
- if (existsSync21(dir)) {
10539
+ if (existsSync22(dir)) {
9963
10540
  allDocs.push(...scanRawDirectory(dir));
9964
10541
  }
9965
10542
  }
@@ -10027,12 +10604,12 @@ function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
10027
10604
  }
10028
10605
  const wordCount = body.split(/\s+/).filter(Boolean).length;
10029
10606
  const draftsDir = resolve16(vaultPath, "_drafts");
10030
- if (!existsSync21(draftsDir))
10031
- mkdirSync20(draftsDir, { recursive: true });
10607
+ if (!existsSync22(draftsDir))
10608
+ mkdirSync21(draftsDir, { recursive: true });
10032
10609
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
10033
10610
  const slug = (topic ?? "knowledge").replace(/[^a-zA-Z0-9가-힣\s]/g, "").replace(/\s+/g, "-").toLowerCase().slice(0, 40);
10034
10611
  const filename = `${timestamp}-${slug}.md`;
10035
- const filePath = join26("_drafts", filename);
10612
+ const filePath = join27("_drafts", filename);
10036
10613
  const fullPath = resolve16(vaultPath, filePath);
10037
10614
  writeFileSync20(fullPath, body, "utf-8");
10038
10615
  return {
@@ -10284,8 +10861,8 @@ ${aiContent.text}
10284
10861
 
10285
10862
  // packages/cli/dist/commands/session-cmd.js
10286
10863
  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";
10864
+ import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync22, existsSync as existsSync23, appendFileSync } from "node:fs";
10865
+ import { resolve as resolve17, join as join28 } from "node:path";
10289
10866
  async function sessionSaveCommand(options) {
10290
10867
  const config = loadConfig();
10291
10868
  if (!config.vaultPath) {
@@ -10294,12 +10871,12 @@ async function sessionSaveCommand(options) {
10294
10871
  }
10295
10872
  const folders = config.folders;
10296
10873
  const logDir = resolve17(config.vaultPath, folders.fleeting, "_daily-logs");
10297
- if (!existsSync22(logDir))
10298
- mkdirSync21(logDir, { recursive: true });
10874
+ if (!existsSync23(logDir))
10875
+ mkdirSync22(logDir, { recursive: true });
10299
10876
  const now = /* @__PURE__ */ new Date();
10300
10877
  const dateStr = now.toISOString().split("T")[0];
10301
10878
  const timeStr = now.toTimeString().split(" ")[0];
10302
- const logFile = join27(logDir, `daily-log-${dateStr}.md`);
10879
+ const logFile = join28(logDir, `daily-log-${dateStr}.md`);
10303
10880
  let summary = options.summary ?? "";
10304
10881
  if (!summary && !process.stdin.isTTY) {
10305
10882
  const chunks = [];
@@ -10338,7 +10915,7 @@ async function sessionSaveCommand(options) {
10338
10915
  entry.push("### Action Items", options.actions, "");
10339
10916
  }
10340
10917
  entry.push("---", "");
10341
- if (!existsSync22(logFile)) {
10918
+ if (!existsSync23(logFile)) {
10342
10919
  const header = [
10343
10920
  "---",
10344
10921
  `title: "Daily Log \u2014 ${dateStr}"`,
@@ -10370,8 +10947,8 @@ async function sessionSaveCommand(options) {
10370
10947
 
10371
10948
  // packages/cli/dist/commands/flush-cmd.js
10372
10949
  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";
10950
+ import { readdirSync as readdirSync7, readFileSync as readFileSync17, existsSync as existsSync24 } from "node:fs";
10951
+ import { resolve as resolve18, join as join29 } from "node:path";
10375
10952
  async function flushCommand() {
10376
10953
  const config = loadConfig();
10377
10954
  if (!config.vaultPath) {
@@ -10380,7 +10957,7 @@ async function flushCommand() {
10380
10957
  }
10381
10958
  const folders = config.folders;
10382
10959
  const logDir = resolve18(config.vaultPath, folders.fleeting, "_daily-logs");
10383
- if (!existsSync23(logDir)) {
10960
+ if (!existsSync24(logDir)) {
10384
10961
  console.log(chalk28.yellow("No daily logs found. Use `stellavault session-save` or let Claude Code hooks capture sessions."));
10385
10962
  return;
10386
10963
  }
@@ -10393,7 +10970,7 @@ async function flushCommand() {
10393
10970
  let totalSessions = 0;
10394
10971
  const allContent = [];
10395
10972
  for (const file of logFiles) {
10396
- const content = readFileSync17(join28(logDir, file), "utf-8");
10973
+ const content = readFileSync17(join29(logDir, file), "utf-8");
10397
10974
  const sessions = content.split(/^## Session/m).slice(1);
10398
10975
  totalSessions += sessions.length;
10399
10976
  allContent.push(content);
@@ -10521,8 +11098,8 @@ async function lintCommand() {
10521
11098
 
10522
11099
  // packages/cli/dist/commands/fleeting-cmd.js
10523
11100
  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";
11101
+ import { writeFileSync as writeFileSync22, mkdirSync as mkdirSync23, existsSync as existsSync25 } from "node:fs";
11102
+ import { join as join30, resolve as resolve19 } from "node:path";
10526
11103
  async function fleetingCommand(text, options) {
10527
11104
  if (!text || text.trim().length < 2) {
10528
11105
  console.error(chalk31.yellow('Usage: stellavault fleeting "your idea here" [--tags tag1,tag2]'));
@@ -10530,13 +11107,13 @@ async function fleetingCommand(text, options) {
10530
11107
  }
10531
11108
  const config = loadConfig();
10532
11109
  const rawDir = resolve19(config.vaultPath, "raw");
10533
- if (!existsSync24(rawDir))
10534
- mkdirSync22(rawDir, { recursive: true });
11110
+ if (!existsSync25(rawDir))
11111
+ mkdirSync23(rawDir, { recursive: true });
10535
11112
  const now = /* @__PURE__ */ new Date();
10536
11113
  const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
10537
11114
  const slug = text.slice(0, 40).replace(/[^a-zA-Z0-9가-힣\s]/g, "").replace(/\s+/g, "-").toLowerCase();
10538
11115
  const filename = `${timestamp}-${slug}.md`;
10539
- const filePath = join29(rawDir, filename);
11116
+ const filePath = join30(rawDir, filename);
10540
11117
  if (!resolve19(filePath).startsWith(resolve19(rawDir))) {
10541
11118
  console.error(chalk31.red("Invalid file path"));
10542
11119
  process.exit(1);
@@ -10563,15 +11140,15 @@ async function fleetingCommand(text, options) {
10563
11140
 
10564
11141
  // packages/cli/dist/commands/ingest-cmd.js
10565
11142
  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";
11143
+ import { readFileSync as readFileSync18, existsSync as existsSync26, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
11144
+ import { extname as extname8, resolve as resolve20, join as join31 } from "node:path";
10568
11145
  async function ingestCommand(input, options) {
10569
11146
  if (!input) {
10570
11147
  console.error(chalk32.yellow("Usage: stellavault ingest <url|file|text|folder/> [--tags t1,t2]"));
10571
11148
  process.exit(1);
10572
11149
  }
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));
11150
+ if (existsSync26(input) && statSync5(input).isDirectory()) {
11151
+ 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
11152
  if (files.length === 0) {
10576
11153
  console.error(chalk32.yellow(`No supported files found in ${input}`));
10577
11154
  process.exit(1);
@@ -10616,13 +11193,10 @@ async function ingestSingleFile(input, options) {
10616
11193
  if (/^https?:\/\//.test(input)) {
10617
11194
  const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(input);
10618
11195
  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
- }
11196
+ await assertPublicUrl(input);
10625
11197
  } catch {
11198
+ console.error(chalk32.yellow("Private/local or non-public URLs are not allowed for security."));
11199
+ process.exit(1);
10626
11200
  }
10627
11201
  if (isYouTube) {
10628
11202
  try {
@@ -10668,7 +11242,7 @@ async function ingestSingleFile(input, options) {
10668
11242
  source: input
10669
11243
  };
10670
11244
  }
10671
- } else if (existsSync25(input)) {
11245
+ } else if (existsSync26(input)) {
10672
11246
  const ext = extname8(input).toLowerCase();
10673
11247
  const binaryExts = /* @__PURE__ */ new Set([".pdf", ".docx", ".pptx", ".xlsx", ".xls"]);
10674
11248
  const structuredExts = /* @__PURE__ */ new Set([".json", ".csv", ".xml", ".html", ".htm", ".yaml", ".yml", ".rtf"]);
@@ -10826,9 +11400,9 @@ async function autopilotCommand(options) {
10826
11400
 
10827
11401
  // packages/cli/dist/commands/doctor-cmd.js
10828
11402
  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";
11403
+ import { existsSync as existsSync27, statSync as statSync6 } from "node:fs";
11404
+ import { join as join32 } from "node:path";
11405
+ import { homedir as homedir18 } from "node:os";
10832
11406
  async function doctorCommand() {
10833
11407
  console.log("");
10834
11408
  console.log(chalk34.bold(" \u{1FA7A} Stellavault Doctor\n"));
@@ -10841,10 +11415,10 @@ async function doctorCommand() {
10841
11415
  fix: nodeVersion2 < 20 ? "Download Node.js 20+: https://nodejs.org" : void 0
10842
11416
  });
10843
11417
  const configPaths = [
10844
- join31(process.cwd(), ".stellavault.json"),
10845
- join31(homedir17(), ".stellavault.json")
11418
+ join32(process.cwd(), ".stellavault.json"),
11419
+ join32(homedir18(), ".stellavault.json")
10846
11420
  ];
10847
- const configPath = configPaths.find((p) => existsSync26(p));
11421
+ const configPath = configPaths.find((p) => existsSync27(p));
10848
11422
  checks.push({
10849
11423
  name: "Config file",
10850
11424
  pass: !!configPath,
@@ -10869,7 +11443,7 @@ async function doctorCommand() {
10869
11443
  }
10870
11444
  }
10871
11445
  if (vaultPath) {
10872
- const vaultExists = existsSync26(vaultPath);
11446
+ const vaultExists = existsSync27(vaultPath);
10873
11447
  checks.push({
10874
11448
  name: "Vault path",
10875
11449
  pass: vaultExists,
@@ -10898,7 +11472,7 @@ async function doctorCommand() {
10898
11472
  }
10899
11473
  }
10900
11474
  if (dbPath) {
10901
- const dbExists = existsSync26(dbPath);
11475
+ const dbExists = existsSync27(dbPath);
10902
11476
  checks.push({
10903
11477
  name: "Database",
10904
11478
  pass: dbExists,
@@ -10913,20 +11487,20 @@ async function doctorCommand() {
10913
11487
  fix: "Re-run: stellavault init"
10914
11488
  });
10915
11489
  }
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"));
11490
+ const cacheDir = join32(homedir18(), ".cache", "onnxruntime");
11491
+ const hfCache = join32(homedir18(), ".cache", "huggingface");
11492
+ const xenovaCache = join32(homedir18(), ".cache", "xenova");
11493
+ const modelCached = existsSync27(cacheDir) || existsSync27(hfCache) || existsSync27(xenovaCache) || existsSync27(join32(homedir18(), ".cache", "transformers"));
10920
11494
  checks.push({
10921
11495
  name: "Embedding model cached",
10922
11496
  pass: modelCached,
10923
11497
  detail: modelCached ? "local model files found" : "not downloaded yet",
10924
11498
  fix: !modelCached ? "Will download automatically on first index (~30MB)" : void 0
10925
11499
  });
10926
- const testDir = join31(homedir17(), ".stellavault");
11500
+ const testDir = join32(homedir18(), ".stellavault");
10927
11501
  try {
10928
- const { mkdirSync: mkdirSync23 } = await import("node:fs");
10929
- mkdirSync23(testDir, { recursive: true });
11502
+ const { mkdirSync: mkdirSync24 } = await import("node:fs");
11503
+ mkdirSync24(testDir, { recursive: true });
10930
11504
  checks.push({ name: "Write permission (~/.stellavault)", pass: true, detail: "OK" });
10931
11505
  } catch {
10932
11506
  checks.push({
@@ -10976,7 +11550,7 @@ if (nodeVersion < 20) {
10976
11550
  process.exit(1);
10977
11551
  }
10978
11552
  var program = new Command();
10979
- var SV_VERSION = true ? "0.8.3" : "0.0.0-dev";
11553
+ var SV_VERSION = true ? "0.8.5" : "0.0.0-dev";
10980
11554
  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
11555
  program.command("init").description("Interactive setup wizard \u2014 get started in 3 minutes").action(initCommand);
10982
11556
  program.command("doctor").description("Diagnose setup issues (config, vault, DB, model, Node version)").action(doctorCommand);