stellavault 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/graph-ui/assets/{camera_utils-BK2vNdvf.js → camera_utils-DBFmYlaE.js} +1 -1
- package/dist/graph-ui/assets/{hands-yrSjE20U.js → hands-CBFUH2Er.js} +1 -1
- package/dist/graph-ui/assets/{index-4LS6c1x8.js → index-BNGAzxHC.js} +50 -50
- package/dist/graph-ui/index.html +1 -1
- package/dist/stellavault.js +746 -264
- package/package.json +1 -1
package/dist/stellavault.js
CHANGED
|
@@ -4,12 +4,6 @@ const require = createRequire(import.meta.url);
|
|
|
4
4
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
-
}) : x)(function(x) {
|
|
10
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
-
});
|
|
13
7
|
var __esm = (fn, res) => function __init() {
|
|
14
8
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
9
|
};
|
|
@@ -88,17 +82,33 @@ import { createHash } from "node:crypto";
|
|
|
88
82
|
import matter from "gray-matter";
|
|
89
83
|
function scanVault(vaultPath) {
|
|
90
84
|
const documents = [];
|
|
91
|
-
|
|
85
|
+
const skipped = [];
|
|
92
86
|
const mdFiles = findMdFiles(vaultPath);
|
|
93
87
|
for (const filePath of mdFiles) {
|
|
88
|
+
const rel = relative(vaultPath, filePath).replace(/\\/g, "/");
|
|
94
89
|
try {
|
|
90
|
+
const stat = statSync(filePath);
|
|
91
|
+
if (stat.size === 0) {
|
|
92
|
+
skipped.push({ path: rel, reason: "empty" });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
96
|
+
skipped.push({ path: rel, reason: "too-large", detail: `${stat.size}B` });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
95
99
|
const doc = parseDocument(vaultPath, filePath);
|
|
100
|
+
if (!doc.content || doc.content.trim().length === 0) {
|
|
101
|
+
skipped.push({ path: rel, reason: "empty", detail: "no content after frontmatter" });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
96
104
|
documents.push(doc);
|
|
97
|
-
} catch {
|
|
98
|
-
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const msg = err?.message ?? String(err);
|
|
107
|
+
const reason = /ENOENT|EACCES|EPERM/.test(msg) ? "unreadable" : "parse-error";
|
|
108
|
+
skipped.push({ path: rel, reason, detail: msg.slice(0, 200) });
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
|
-
return { documents, scannedFiles: mdFiles.length, skippedFiles };
|
|
111
|
+
return { documents, scannedFiles: mdFiles.length, skippedFiles: skipped.length, skipped };
|
|
102
112
|
}
|
|
103
113
|
function findMdFiles(dir, files = []) {
|
|
104
114
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -209,9 +219,11 @@ function extractTags(frontmatter, content) {
|
|
|
209
219
|
}
|
|
210
220
|
return [...tags];
|
|
211
221
|
}
|
|
222
|
+
var MAX_FILE_BYTES;
|
|
212
223
|
var init_scanner = __esm({
|
|
213
224
|
"packages/core/dist/indexer/scanner.js"() {
|
|
214
225
|
"use strict";
|
|
226
|
+
MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
215
227
|
}
|
|
216
228
|
});
|
|
217
229
|
|
|
@@ -419,28 +431,60 @@ var init_retry = __esm({
|
|
|
419
431
|
});
|
|
420
432
|
|
|
421
433
|
// packages/core/dist/indexer/local-embedder.js
|
|
434
|
+
function getPipeline(modelName) {
|
|
435
|
+
let p = pipelineCache.get(modelName);
|
|
436
|
+
if (p)
|
|
437
|
+
return p;
|
|
438
|
+
p = (async () => {
|
|
439
|
+
const { pipeline: createPipeline } = await import("@xenova/transformers");
|
|
440
|
+
return createPipeline("feature-extraction", `Xenova/${modelName}`, { quantized: true });
|
|
441
|
+
})();
|
|
442
|
+
pipelineCache.set(modelName, p);
|
|
443
|
+
return p;
|
|
444
|
+
}
|
|
422
445
|
function createLocalEmbedder(modelName = "nomic-embed-text-v1.5") {
|
|
423
446
|
let pipeline;
|
|
424
447
|
let dims = modelName.includes("MiniLM") ? 384 : 768;
|
|
448
|
+
const profile = process.env.STELLAVAULT_PROFILE_MEMORY === "1";
|
|
449
|
+
let callCount = 0;
|
|
425
450
|
return {
|
|
426
451
|
async initialize() {
|
|
427
|
-
|
|
428
|
-
pipeline = await createPipeline("feature-extraction", `Xenova/${modelName}`, {
|
|
429
|
-
quantized: true
|
|
430
|
-
});
|
|
452
|
+
pipeline = await getPipeline(modelName);
|
|
431
453
|
},
|
|
432
454
|
async embed(text) {
|
|
433
|
-
|
|
434
|
-
|
|
455
|
+
let output = await pipeline(text, { pooling: "mean", normalize: true });
|
|
456
|
+
const result = Array.from(output.data).slice(0, dims);
|
|
457
|
+
try {
|
|
458
|
+
output.dispose?.();
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
output = null;
|
|
462
|
+
return result;
|
|
435
463
|
},
|
|
436
|
-
async embedBatch(texts, batchSize =
|
|
464
|
+
async embedBatch(texts, batchSize = 16) {
|
|
437
465
|
const results = [];
|
|
466
|
+
let processed = 0;
|
|
438
467
|
for (let i = 0; i < texts.length; i += batchSize) {
|
|
439
468
|
const batch = texts.slice(i, i + batchSize);
|
|
440
|
-
|
|
469
|
+
let output = await pipeline(batch, { pooling: "mean", normalize: true });
|
|
441
470
|
const flat = output.data;
|
|
442
471
|
for (let j = 0; j < batch.length; j++) {
|
|
443
|
-
results.push(Array.from(flat.
|
|
472
|
+
results.push(Array.from(flat.subarray(j * dims, (j + 1) * dims)));
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
output.dispose?.();
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
output = null;
|
|
479
|
+
processed += batch.length;
|
|
480
|
+
callCount += batch.length;
|
|
481
|
+
if (processed % 256 === 0 && typeof globalThis.gc === "function") {
|
|
482
|
+
globalThis.gc();
|
|
483
|
+
}
|
|
484
|
+
if (profile && callCount % 100 < batchSize) {
|
|
485
|
+
const rss = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
486
|
+
const heap = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
487
|
+
console.error(`[profile-memory] embedded=${callCount} rss=${rss}MB heap=${heap}MB`);
|
|
444
488
|
}
|
|
445
489
|
}
|
|
446
490
|
return results;
|
|
@@ -453,9 +497,11 @@ function createLocalEmbedder(modelName = "nomic-embed-text-v1.5") {
|
|
|
453
497
|
}
|
|
454
498
|
};
|
|
455
499
|
}
|
|
500
|
+
var pipelineCache;
|
|
456
501
|
var init_local_embedder = __esm({
|
|
457
502
|
"packages/core/dist/indexer/local-embedder.js"() {
|
|
458
503
|
"use strict";
|
|
504
|
+
pipelineCache = /* @__PURE__ */ new Map();
|
|
459
505
|
}
|
|
460
506
|
});
|
|
461
507
|
|
|
@@ -525,13 +571,15 @@ __export(indexer_exports, {
|
|
|
525
571
|
async function indexVault(vaultPath, options) {
|
|
526
572
|
const start = Date.now();
|
|
527
573
|
const { store, embedder, chunkOptions, onProgress } = options;
|
|
528
|
-
const
|
|
574
|
+
const scan = scanVault(vaultPath);
|
|
575
|
+
const { documents } = scan;
|
|
529
576
|
const existingDocs = await store.getAllDocuments();
|
|
530
577
|
const existingMap = new Map(existingDocs.map((d) => [d.id, d.contentHash]));
|
|
531
578
|
let indexed = 0;
|
|
532
579
|
let skipped = 0;
|
|
533
580
|
let failed = 0;
|
|
534
581
|
let totalChunks = 0;
|
|
582
|
+
const failedFiles = [];
|
|
535
583
|
const scannedIds = /* @__PURE__ */ new Set();
|
|
536
584
|
for (let i = 0; i < documents.length; i++) {
|
|
537
585
|
const doc = documents[i];
|
|
@@ -555,6 +603,7 @@ async function indexVault(vaultPath, options) {
|
|
|
555
603
|
totalChunks += chunks.length;
|
|
556
604
|
} catch (err) {
|
|
557
605
|
failed++;
|
|
606
|
+
failedFiles.push({ path: doc.filePath, error: err?.message ?? String(err) });
|
|
558
607
|
console.error(errors.indexingFailed(doc.filePath, err).format());
|
|
559
608
|
}
|
|
560
609
|
}
|
|
@@ -571,7 +620,10 @@ async function indexVault(vaultPath, options) {
|
|
|
571
620
|
deleted,
|
|
572
621
|
failed,
|
|
573
622
|
totalChunks,
|
|
574
|
-
elapsedMs: Date.now() - start
|
|
623
|
+
elapsedMs: Date.now() - start,
|
|
624
|
+
totalFiles: scan.scannedFiles,
|
|
625
|
+
skippedFiles: scan.skipped,
|
|
626
|
+
failedFiles
|
|
575
627
|
};
|
|
576
628
|
}
|
|
577
629
|
var init_indexer = __esm({
|
|
@@ -645,50 +697,31 @@ async function buildGraphData(store, options = {}) {
|
|
|
645
697
|
]);
|
|
646
698
|
const edges = [];
|
|
647
699
|
const edgeCounts = /* @__PURE__ */ new Map();
|
|
648
|
-
const USE_HNSW_THRESHOLD = 200;
|
|
649
700
|
const docsWithVecs = docs.filter((d) => embeddings.has(d.id));
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
const normalizedVecs = /* @__PURE__ */ new Map();
|
|
668
|
-
for (const doc of docsWithVecs) {
|
|
669
|
-
normalizedVecs.set(doc.id, normalizeVector([...embeddings.get(doc.id)]));
|
|
670
|
-
}
|
|
671
|
-
const docIds = [...normalizedVecs.keys()];
|
|
672
|
-
const vecArray = docIds.map((id) => normalizedVecs.get(id));
|
|
673
|
-
const n = docIds.length;
|
|
674
|
-
const neighbors = Array.from({ length: n }, () => []);
|
|
675
|
-
for (let i = 0; i < n; i++) {
|
|
676
|
-
for (let j = i + 1; j < n; j++) {
|
|
677
|
-
const sim = dotProduct(vecArray[i], vecArray[j]);
|
|
678
|
-
if (sim >= edgeThreshold) {
|
|
679
|
-
neighbors[i].push({ peer: j, sim });
|
|
680
|
-
neighbors[j].push({ peer: i, sim });
|
|
681
|
-
}
|
|
701
|
+
const normalizedVecs = /* @__PURE__ */ new Map();
|
|
702
|
+
for (const doc of docsWithVecs) {
|
|
703
|
+
normalizedVecs.set(doc.id, normalizeVector([...embeddings.get(doc.id)]));
|
|
704
|
+
}
|
|
705
|
+
const docIds = [...normalizedVecs.keys()];
|
|
706
|
+
const vecArray = docIds.map((id) => normalizedVecs.get(id));
|
|
707
|
+
const n = docIds.length;
|
|
708
|
+
const neighbors = Array.from({ length: n }, () => []);
|
|
709
|
+
for (let i = 0; i < n; i++) {
|
|
710
|
+
for (let j = i + 1; j < n; j++) {
|
|
711
|
+
const sim = dotProduct(vecArray[i], vecArray[j]);
|
|
712
|
+
if (sim >= edgeThreshold) {
|
|
713
|
+
neighbors[i].push({ peer: j, sim });
|
|
714
|
+
neighbors[j].push({ peer: i, sim });
|
|
682
715
|
}
|
|
683
716
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
717
|
+
}
|
|
718
|
+
for (let i = 0; i < n; i++) {
|
|
719
|
+
neighbors[i].sort((a, b) => b.sim - a.sim);
|
|
720
|
+
for (const { peer: j, sim } of neighbors[i].slice(0, maxEdgesPerNode)) {
|
|
721
|
+
const edgeKey = i < j ? `${i}:${j}` : `${j}:${i}`;
|
|
722
|
+
if (!edgeCounts.has(edgeKey)) {
|
|
723
|
+
edges.push({ source: docIds[i], target: docIds[j], weight: sim });
|
|
724
|
+
edgeCounts.set(edgeKey, 1);
|
|
692
725
|
}
|
|
693
726
|
}
|
|
694
727
|
}
|
|
@@ -722,16 +755,16 @@ async function buildGraphData(store, options = {}) {
|
|
|
722
755
|
nodeCount: folderCounts.get(i) ?? 0
|
|
723
756
|
}));
|
|
724
757
|
} else {
|
|
725
|
-
const
|
|
726
|
-
const vectors =
|
|
727
|
-
const k = Math.min(Math.max(5, Math.round(Math.sqrt(
|
|
758
|
+
const docIds2 = docs.filter((d) => embeddings.has(d.id)).map((d) => d.id);
|
|
759
|
+
const vectors = docIds2.map((id) => embeddings.get(id));
|
|
760
|
+
const k = Math.min(Math.max(5, Math.round(Math.sqrt(docIds2.length / 5))), 10);
|
|
728
761
|
const assignments = kMeans(vectors, k);
|
|
729
762
|
const clusterDocInfos = /* @__PURE__ */ new Map();
|
|
730
|
-
for (let i = 0; i <
|
|
763
|
+
for (let i = 0; i < docIds2.length; i++) {
|
|
731
764
|
const cId = assignments[i];
|
|
732
765
|
if (!clusterDocInfos.has(cId))
|
|
733
766
|
clusterDocInfos.set(cId, []);
|
|
734
|
-
const doc = docs.find((d) => d.id ===
|
|
767
|
+
const doc = docs.find((d) => d.id === docIds2[i]);
|
|
735
768
|
if (doc)
|
|
736
769
|
clusterDocInfos.get(cId).push({ id: doc.id, title: doc.title });
|
|
737
770
|
}
|
|
@@ -752,8 +785,8 @@ async function buildGraphData(store, options = {}) {
|
|
|
752
785
|
});
|
|
753
786
|
}
|
|
754
787
|
assignmentMap = /* @__PURE__ */ new Map();
|
|
755
|
-
for (let i = 0; i <
|
|
756
|
-
assignmentMap.set(
|
|
788
|
+
for (let i = 0; i < docIds2.length; i++) {
|
|
789
|
+
assignmentMap.set(docIds2[i], assignments[i]);
|
|
757
790
|
}
|
|
758
791
|
}
|
|
759
792
|
const connectionCounts = /* @__PURE__ */ new Map();
|
|
@@ -1272,36 +1305,34 @@ var init_wiki_compiler = __esm({
|
|
|
1272
1305
|
});
|
|
1273
1306
|
|
|
1274
1307
|
// packages/core/dist/federation/identity.js
|
|
1275
|
-
import {
|
|
1276
|
-
import {
|
|
1277
|
-
import { join as join8 } from "node:path";
|
|
1308
|
+
import { createHash as createHash3, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";
|
|
1309
|
+
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
1278
1310
|
import { homedir as homedir4 } from "node:os";
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1290
|
-
const secretKey = randomBytes(32);
|
|
1291
|
-
const publicKey = createHash3("sha256").update(secretKey).digest();
|
|
1292
|
-
const peerId = createHash3("sha256").update(publicKey).digest("hex").slice(0, 16);
|
|
1293
|
-
const identity = {
|
|
1311
|
+
import { join as join8 } from "node:path";
|
|
1312
|
+
function derivePeerId(publicKey) {
|
|
1313
|
+
return createHash3("sha256").update(publicKey).digest("hex").slice(0, 16);
|
|
1314
|
+
}
|
|
1315
|
+
function generateIdentity(displayName) {
|
|
1316
|
+
const { publicKey: pkObj, privateKey: skObj } = generateKeyPairSync("ed25519");
|
|
1317
|
+
const publicKey = pkObj.export({ type: "spki", format: "der" });
|
|
1318
|
+
const secretKey = skObj.export({ type: "pkcs8", format: "der" });
|
|
1319
|
+
const peerId = derivePeerId(publicKey);
|
|
1320
|
+
return {
|
|
1294
1321
|
peerId,
|
|
1295
1322
|
publicKey,
|
|
1296
1323
|
secretKey,
|
|
1297
1324
|
displayName: displayName ?? `node-${peerId.slice(0, 6)}`,
|
|
1298
1325
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1299
1326
|
};
|
|
1327
|
+
}
|
|
1328
|
+
function persistIdentity(identity) {
|
|
1300
1329
|
mkdirSync9(IDENTITY_DIR, { recursive: true });
|
|
1301
1330
|
const content = JSON.stringify({
|
|
1331
|
+
version: IDENTITY_VERSION,
|
|
1332
|
+
algorithm: IDENTITY_ALGORITHM,
|
|
1302
1333
|
peerId: identity.peerId,
|
|
1303
|
-
publicKey: publicKey.toString("hex"),
|
|
1304
|
-
secretKey: secretKey.toString("hex"),
|
|
1334
|
+
publicKey: identity.publicKey.toString("hex"),
|
|
1335
|
+
secretKey: identity.secretKey.toString("hex"),
|
|
1305
1336
|
displayName: identity.displayName,
|
|
1306
1337
|
createdAt: identity.createdAt
|
|
1307
1338
|
}, null, 2);
|
|
@@ -1310,36 +1341,103 @@ function getOrCreateIdentity(displayName) {
|
|
|
1310
1341
|
chmodSync(IDENTITY_FILE, 384);
|
|
1311
1342
|
} catch {
|
|
1312
1343
|
}
|
|
1344
|
+
}
|
|
1345
|
+
function getOrCreateIdentity(displayName) {
|
|
1346
|
+
if (existsSync7(IDENTITY_FILE)) {
|
|
1347
|
+
const raw = JSON.parse(readFileSync7(IDENTITY_FILE, "utf-8"));
|
|
1348
|
+
const isV2 = raw.version === IDENTITY_VERSION && raw.algorithm === IDENTITY_ALGORITHM;
|
|
1349
|
+
if (isV2) {
|
|
1350
|
+
return {
|
|
1351
|
+
peerId: String(raw.peerId),
|
|
1352
|
+
publicKey: Buffer.from(String(raw.publicKey), "hex"),
|
|
1353
|
+
secretKey: Buffer.from(String(raw.secretKey), "hex"),
|
|
1354
|
+
displayName: String(raw.displayName),
|
|
1355
|
+
createdAt: String(raw.createdAt)
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const bakPath = IDENTITY_FILE.replace(/\.json$/, ".v1.bak.json");
|
|
1359
|
+
writeFileSync8(bakPath, JSON.stringify(raw, null, 2), { encoding: "utf-8", mode: 384 });
|
|
1360
|
+
console.warn(`[federation] Identity migrated to v2 (Ed25519). v1 backup: ${bakPath}`);
|
|
1361
|
+
}
|
|
1362
|
+
const identity = generateIdentity(displayName);
|
|
1363
|
+
persistIdentity(identity);
|
|
1313
1364
|
return identity;
|
|
1314
1365
|
}
|
|
1315
|
-
|
|
1366
|
+
function signMessage(secretKeyDer, message) {
|
|
1367
|
+
const keyObj = createPrivateKey({ key: secretKeyDer, format: "der", type: "pkcs8" });
|
|
1368
|
+
return sign(null, message, keyObj);
|
|
1369
|
+
}
|
|
1370
|
+
function verifySignature(publicKeyDer, message, signature) {
|
|
1371
|
+
try {
|
|
1372
|
+
const keyObj = createPublicKey({ key: publicKeyDer, format: "der", type: "spki" });
|
|
1373
|
+
return verify(null, message, keyObj, signature);
|
|
1374
|
+
} catch {
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function peerIdFromPublicKey(publicKey) {
|
|
1379
|
+
return derivePeerId(publicKey);
|
|
1380
|
+
}
|
|
1381
|
+
var IDENTITY_DIR, IDENTITY_FILE, IDENTITY_VERSION, IDENTITY_ALGORITHM;
|
|
1316
1382
|
var init_identity = __esm({
|
|
1317
1383
|
"packages/core/dist/federation/identity.js"() {
|
|
1318
1384
|
"use strict";
|
|
1319
1385
|
IDENTITY_DIR = join8(homedir4(), ".stellavault", "federation");
|
|
1320
1386
|
IDENTITY_FILE = join8(IDENTITY_DIR, "identity.json");
|
|
1387
|
+
IDENTITY_VERSION = 2;
|
|
1388
|
+
IDENTITY_ALGORITHM = "ed25519";
|
|
1321
1389
|
}
|
|
1322
1390
|
});
|
|
1323
1391
|
|
|
1324
1392
|
// packages/core/dist/federation/node.js
|
|
1325
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
1393
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
|
|
1326
1394
|
import { EventEmitter } from "node:events";
|
|
1327
|
-
var FEDERATION_TOPIC, FederationNode;
|
|
1395
|
+
var FEDERATION_TOPIC, REPLAY_CACHE_LIMIT, DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_RATE_LIMIT_PER_SEC, DEFAULT_RATE_LIMIT_BURST, FederationNode;
|
|
1328
1396
|
var init_node = __esm({
|
|
1329
1397
|
"packages/core/dist/federation/node.js"() {
|
|
1330
1398
|
"use strict";
|
|
1331
1399
|
init_identity();
|
|
1332
1400
|
FEDERATION_TOPIC = createHash4("sha256").update("stellavault-federation-v1").digest();
|
|
1401
|
+
REPLAY_CACHE_LIMIT = 1e4;
|
|
1402
|
+
DEFAULT_HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
1403
|
+
DEFAULT_RATE_LIMIT_PER_SEC = 50;
|
|
1404
|
+
DEFAULT_RATE_LIMIT_BURST = 100;
|
|
1333
1405
|
FederationNode = class extends EventEmitter {
|
|
1334
1406
|
swarm = null;
|
|
1335
1407
|
identity;
|
|
1336
1408
|
peers = /* @__PURE__ */ new Map();
|
|
1409
|
+
connStates = /* @__PURE__ */ new WeakMap();
|
|
1410
|
+
replayCache = /* @__PURE__ */ new Set();
|
|
1411
|
+
replayQueue = [];
|
|
1337
1412
|
running = false;
|
|
1338
1413
|
documentCount = 0;
|
|
1339
1414
|
topTopics = [];
|
|
1340
|
-
constructor
|
|
1415
|
+
// Configurable per-instance — see constructor options.
|
|
1416
|
+
handshakeTimeoutMs;
|
|
1417
|
+
rateLimitPerSec;
|
|
1418
|
+
rateLimitBurst;
|
|
1419
|
+
/**
|
|
1420
|
+
* @param init Either a displayName string (legacy positional form) or an
|
|
1421
|
+
* options object. Pass `{ identity }` to skip the on-disk identity
|
|
1422
|
+
* load — useful for tests that need many ephemeral peers.
|
|
1423
|
+
* `handshakeTimeoutMs` drops connections stuck in handshake
|
|
1424
|
+
* (set to 0 to disable; default 30s).
|
|
1425
|
+
* `rateLimitPerSec` / `rateLimitBurst` cap per-peer envelope
|
|
1426
|
+
* processing rate (set rateLimitPerSec=0 to disable).
|
|
1427
|
+
*/
|
|
1428
|
+
constructor(init) {
|
|
1341
1429
|
super();
|
|
1342
|
-
|
|
1430
|
+
if (init && typeof init === "object") {
|
|
1431
|
+
this.identity = init.identity ?? getOrCreateIdentity(init.displayName);
|
|
1432
|
+
this.handshakeTimeoutMs = init.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
1433
|
+
this.rateLimitPerSec = init.rateLimitPerSec ?? DEFAULT_RATE_LIMIT_PER_SEC;
|
|
1434
|
+
this.rateLimitBurst = init.rateLimitBurst ?? DEFAULT_RATE_LIMIT_BURST;
|
|
1435
|
+
} else {
|
|
1436
|
+
this.identity = getOrCreateIdentity(init);
|
|
1437
|
+
this.handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
1438
|
+
this.rateLimitPerSec = DEFAULT_RATE_LIMIT_PER_SEC;
|
|
1439
|
+
this.rateLimitBurst = DEFAULT_RATE_LIMIT_BURST;
|
|
1440
|
+
}
|
|
1343
1441
|
}
|
|
1344
1442
|
get peerId() {
|
|
1345
1443
|
return this.identity.peerId;
|
|
@@ -1357,7 +1455,6 @@ var init_node = __esm({
|
|
|
1357
1455
|
this.documentCount = documentCount;
|
|
1358
1456
|
this.topTopics = topTopics.slice(0, 5);
|
|
1359
1457
|
}
|
|
1360
|
-
// Design Ref: §4 — join()
|
|
1361
1458
|
async join() {
|
|
1362
1459
|
if (this.running)
|
|
1363
1460
|
return;
|
|
@@ -1371,7 +1468,6 @@ var init_node = __esm({
|
|
|
1371
1468
|
this.running = true;
|
|
1372
1469
|
this.emit("joined", { peerId: this.peerId, topic: FEDERATION_TOPIC.toString("hex").slice(0, 16) });
|
|
1373
1470
|
}
|
|
1374
|
-
// Design Ref: §4 — joinDirect() 수동 IP 폴백
|
|
1375
1471
|
async joinDirect(host, port) {
|
|
1376
1472
|
const net = await import("node:net");
|
|
1377
1473
|
const conn = net.connect(port, host);
|
|
@@ -1391,7 +1487,7 @@ var init_node = __esm({
|
|
|
1391
1487
|
return;
|
|
1392
1488
|
for (const [, peer] of this.peers) {
|
|
1393
1489
|
try {
|
|
1394
|
-
this.
|
|
1490
|
+
this.sendSigned(peer.conn, { type: "leave", peerId: this.peerId });
|
|
1395
1491
|
peer.conn.end();
|
|
1396
1492
|
} catch {
|
|
1397
1493
|
}
|
|
@@ -1405,30 +1501,109 @@ var init_node = __esm({
|
|
|
1405
1501
|
getPeers() {
|
|
1406
1502
|
return [...this.peers.values()].map((p) => p.info);
|
|
1407
1503
|
}
|
|
1408
|
-
// 피어에게 검색 쿼리 전송 (FederatedSearch에서 사용)
|
|
1409
1504
|
sendSearchQuery(peerId, queryId, embedding, limit) {
|
|
1410
1505
|
const peer = this.peers.get(peerId);
|
|
1411
1506
|
if (!peer)
|
|
1412
1507
|
return;
|
|
1413
|
-
this.
|
|
1508
|
+
this.sendSigned(peer.conn, { type: "search_query", queryId, embedding, limit });
|
|
1414
1509
|
}
|
|
1415
|
-
// 피어에게 검색 결과 응답 (FederatedSearch에서 사용)
|
|
1416
1510
|
sendSearchResult(peerId, queryId, results) {
|
|
1417
1511
|
const peer = this.peers.get(peerId);
|
|
1418
1512
|
if (!peer)
|
|
1419
1513
|
return;
|
|
1420
|
-
this.
|
|
1514
|
+
this.sendSigned(peer.conn, { type: "search_result", queryId, results });
|
|
1421
1515
|
}
|
|
1422
1516
|
// --- Private ---
|
|
1517
|
+
/**
|
|
1518
|
+
* Stable JSON for signing. Sorts keys recursively so nested objects
|
|
1519
|
+
* produced by different JS runtimes (or by future code paths that build
|
|
1520
|
+
* objects in a different insertion order) still hash identically on
|
|
1521
|
+
* both sides. RFC 8785-style canonicalization, scoped to what
|
|
1522
|
+
* FederationMessage / SignedEnvelope actually carry. Keys whose value
|
|
1523
|
+
* is `undefined` are omitted (matches JSON.stringify semantics), so
|
|
1524
|
+
* sender and receiver agree on optional fields like `publicKeyHex`.
|
|
1525
|
+
*/
|
|
1526
|
+
canonicalize(value) {
|
|
1527
|
+
if (value === null || typeof value !== "object")
|
|
1528
|
+
return JSON.stringify(value);
|
|
1529
|
+
if (Array.isArray(value)) {
|
|
1530
|
+
return "[" + value.map((v) => this.canonicalize(v)).join(",") + "]";
|
|
1531
|
+
}
|
|
1532
|
+
const obj = value;
|
|
1533
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
1534
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + this.canonicalize(obj[k])).join(",") + "}";
|
|
1535
|
+
}
|
|
1536
|
+
sendSigned(conn, payload, includePublicKey = false) {
|
|
1537
|
+
try {
|
|
1538
|
+
const envBase = {
|
|
1539
|
+
payload,
|
|
1540
|
+
peerId: this.peerId,
|
|
1541
|
+
nonce: randomBytes2(16).toString("hex"),
|
|
1542
|
+
...includePublicKey ? { publicKeyHex: this.identity.publicKey.toString("hex") } : {}
|
|
1543
|
+
};
|
|
1544
|
+
const canonical = Buffer.from(this.canonicalize(envBase), "utf-8");
|
|
1545
|
+
const sig = signMessage(this.identity.secretKey, canonical);
|
|
1546
|
+
const envelope = { ...envBase, signature: sig.toString("hex") };
|
|
1547
|
+
conn.write(JSON.stringify(envelope) + "\n");
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
rememberNonce(nonceHex) {
|
|
1552
|
+
if (this.replayCache.has(nonceHex))
|
|
1553
|
+
return false;
|
|
1554
|
+
this.replayCache.add(nonceHex);
|
|
1555
|
+
this.replayQueue.push(nonceHex);
|
|
1556
|
+
if (this.replayQueue.length > REPLAY_CACHE_LIMIT) {
|
|
1557
|
+
const oldest = this.replayQueue.shift();
|
|
1558
|
+
if (oldest)
|
|
1559
|
+
this.replayCache.delete(oldest);
|
|
1560
|
+
}
|
|
1561
|
+
return true;
|
|
1562
|
+
}
|
|
1423
1563
|
handleConnection(conn) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1564
|
+
const state = {
|
|
1565
|
+
conn,
|
|
1566
|
+
ourNonce: randomBytes2(32),
|
|
1567
|
+
helloSent: false,
|
|
1568
|
+
weResponded: false,
|
|
1569
|
+
peerVerified: false,
|
|
1570
|
+
peerPublicKey: null,
|
|
1571
|
+
peerId: null,
|
|
1572
|
+
pendingPeerInfo: null,
|
|
1573
|
+
ready: false,
|
|
1574
|
+
handshakeTimer: null,
|
|
1575
|
+
rlTokens: this.rateLimitBurst,
|
|
1576
|
+
rlLastRefill: Date.now()
|
|
1577
|
+
};
|
|
1578
|
+
this.connStates.set(conn, state);
|
|
1579
|
+
if (this.handshakeTimeoutMs > 0) {
|
|
1580
|
+
state.handshakeTimer = setTimeout(() => {
|
|
1581
|
+
if (state.ready)
|
|
1582
|
+
return;
|
|
1583
|
+
try {
|
|
1584
|
+
conn.end();
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
this.emit("handshake_timeout", { peerId: state.peerId });
|
|
1588
|
+
}, this.handshakeTimeoutMs);
|
|
1589
|
+
const timer = state.handshakeTimer;
|
|
1590
|
+
timer.unref?.();
|
|
1591
|
+
}
|
|
1592
|
+
this.sendSigned(
|
|
1593
|
+
conn,
|
|
1594
|
+
{
|
|
1595
|
+
type: "hello",
|
|
1596
|
+
peerId: this.peerId,
|
|
1597
|
+
displayName: this.identity.displayName,
|
|
1598
|
+
version: "0.2.0",
|
|
1599
|
+
documentCount: this.documentCount,
|
|
1600
|
+
topTopics: this.topTopics,
|
|
1601
|
+
nonce: state.ourNonce.toString("hex")
|
|
1602
|
+
},
|
|
1603
|
+
/* includePublicKey */
|
|
1604
|
+
true
|
|
1605
|
+
);
|
|
1606
|
+
state.helloSent = true;
|
|
1432
1607
|
let buffer = "";
|
|
1433
1608
|
const MAX_BUFFER = 1024 * 1024;
|
|
1434
1609
|
const MAX_MESSAGE = 64 * 1024;
|
|
@@ -1447,107 +1622,214 @@ var init_node = __esm({
|
|
|
1447
1622
|
continue;
|
|
1448
1623
|
if (line.length > MAX_MESSAGE)
|
|
1449
1624
|
continue;
|
|
1450
|
-
|
|
1451
|
-
const msg = JSON.parse(line);
|
|
1452
|
-
this.handleMessage(conn, msg);
|
|
1453
|
-
} catch {
|
|
1454
|
-
}
|
|
1625
|
+
this.dispatchLine(conn, line);
|
|
1455
1626
|
}
|
|
1456
1627
|
});
|
|
1457
1628
|
conn.on("close", () => {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1629
|
+
const st = this.connStates.get(conn);
|
|
1630
|
+
if (st?.handshakeTimer)
|
|
1631
|
+
clearTimeout(st.handshakeTimer);
|
|
1632
|
+
this.connStates.delete(conn);
|
|
1633
|
+
if (st?.peerId && this.peers.get(st.peerId)?.conn === conn) {
|
|
1634
|
+
this.peers.delete(st.peerId);
|
|
1635
|
+
this.emit("peer_left", { peerId: st.peerId });
|
|
1464
1636
|
}
|
|
1465
1637
|
});
|
|
1466
1638
|
conn.on("error", () => {
|
|
1467
1639
|
});
|
|
1468
1640
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
if (
|
|
1477
|
-
return
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
if (
|
|
1641
|
+
/**
|
|
1642
|
+
* Token-bucket admission control. Returns false when the inbound rate
|
|
1643
|
+
* exceeds the configured limit; the caller should drop the envelope
|
|
1644
|
+
* silently (we don't disconnect — the peer might be momentarily bursty,
|
|
1645
|
+
* not malicious, and a real attacker would just reconnect).
|
|
1646
|
+
*/
|
|
1647
|
+
admitEnvelope(state) {
|
|
1648
|
+
if (this.rateLimitPerSec <= 0)
|
|
1649
|
+
return true;
|
|
1650
|
+
const now = Date.now();
|
|
1651
|
+
const elapsed = (now - state.rlLastRefill) / 1e3;
|
|
1652
|
+
if (elapsed > 0) {
|
|
1653
|
+
state.rlTokens = Math.min(this.rateLimitBurst, state.rlTokens + elapsed * this.rateLimitPerSec);
|
|
1654
|
+
state.rlLastRefill = now;
|
|
1655
|
+
}
|
|
1656
|
+
if (state.rlTokens < 1) {
|
|
1657
|
+
this.emit("rate_limited", { peerId: state.peerId });
|
|
1481
1658
|
return false;
|
|
1659
|
+
}
|
|
1660
|
+
state.rlTokens -= 1;
|
|
1482
1661
|
return true;
|
|
1483
1662
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1663
|
+
dispatchLine(conn, line) {
|
|
1664
|
+
const state = this.connStates.get(conn);
|
|
1665
|
+
if (!state)
|
|
1666
|
+
return;
|
|
1667
|
+
if (!this.admitEnvelope(state))
|
|
1668
|
+
return;
|
|
1669
|
+
let envelope;
|
|
1670
|
+
try {
|
|
1671
|
+
envelope = JSON.parse(line);
|
|
1672
|
+
} catch {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
if (!envelope || typeof envelope !== "object")
|
|
1676
|
+
return;
|
|
1677
|
+
if (typeof envelope.peerId !== "string")
|
|
1678
|
+
return;
|
|
1679
|
+
if (typeof envelope.signature !== "string")
|
|
1680
|
+
return;
|
|
1681
|
+
if (typeof envelope.nonce !== "string")
|
|
1682
|
+
return;
|
|
1683
|
+
if (envelope.nonce.length < 16 || envelope.nonce.length > 128)
|
|
1684
|
+
return;
|
|
1685
|
+
if (!envelope.payload || typeof envelope.payload !== "object")
|
|
1686
|
+
return;
|
|
1687
|
+
if (!this.rememberNonce(envelope.nonce))
|
|
1688
|
+
return;
|
|
1689
|
+
let publicKey = null;
|
|
1690
|
+
if (!state.peerPublicKey) {
|
|
1691
|
+
if (envelope.payload && envelope.payload.type !== "hello")
|
|
1692
|
+
return;
|
|
1693
|
+
if (!envelope.publicKeyHex || typeof envelope.publicKeyHex !== "string")
|
|
1694
|
+
return;
|
|
1695
|
+
try {
|
|
1696
|
+
publicKey = Buffer.from(envelope.publicKeyHex, "hex");
|
|
1697
|
+
} catch {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const derived = peerIdFromPublicKey(publicKey);
|
|
1701
|
+
if (derived !== envelope.peerId)
|
|
1702
|
+
return;
|
|
1703
|
+
} else {
|
|
1704
|
+
if (envelope.peerId !== state.peerId)
|
|
1705
|
+
return;
|
|
1706
|
+
publicKey = state.peerPublicKey;
|
|
1707
|
+
}
|
|
1708
|
+
const { signature, ...envBase } = envelope;
|
|
1709
|
+
const canonical = Buffer.from(this.canonicalize(envBase), "utf-8");
|
|
1710
|
+
let sigBuf;
|
|
1711
|
+
try {
|
|
1712
|
+
sigBuf = Buffer.from(signature, "hex");
|
|
1713
|
+
} catch {
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (sigBuf.length !== 64)
|
|
1717
|
+
return;
|
|
1718
|
+
if (!verifySignature(publicKey, canonical, sigBuf))
|
|
1486
1719
|
return;
|
|
1720
|
+
this.handleMessage(conn, state, envelope.payload, publicKey);
|
|
1721
|
+
}
|
|
1722
|
+
handleMessage(conn, state, msg, publicKey) {
|
|
1487
1723
|
switch (msg.type) {
|
|
1488
|
-
case "
|
|
1489
|
-
|
|
1490
|
-
|
|
1724
|
+
case "hello": {
|
|
1725
|
+
if (state.peerPublicKey)
|
|
1726
|
+
return;
|
|
1727
|
+
if (typeof msg.peerId !== "string" || typeof msg.nonce !== "string")
|
|
1728
|
+
return;
|
|
1729
|
+
let nonceBuf;
|
|
1730
|
+
try {
|
|
1731
|
+
nonceBuf = Buffer.from(msg.nonce, "hex");
|
|
1732
|
+
} catch {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (nonceBuf.length !== 32)
|
|
1736
|
+
return;
|
|
1737
|
+
if (!this.rememberNonce(msg.nonce))
|
|
1738
|
+
return;
|
|
1739
|
+
state.peerPublicKey = publicKey;
|
|
1740
|
+
state.peerId = msg.peerId;
|
|
1741
|
+
state.pendingPeerInfo = {
|
|
1491
1742
|
peerId: msg.peerId,
|
|
1492
|
-
displayName:
|
|
1743
|
+
displayName: (msg.displayName ?? "").slice(0, 50),
|
|
1493
1744
|
documentCount: Math.min(msg.documentCount ?? 0, 1e6),
|
|
1494
|
-
// 합리적 상한
|
|
1495
1745
|
topTopics: (msg.topTopics ?? []).slice(0, 10),
|
|
1496
1746
|
joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1497
1747
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1498
1748
|
};
|
|
1499
|
-
this.
|
|
1500
|
-
this.
|
|
1749
|
+
const response = signMessage(this.identity.secretKey, nonceBuf);
|
|
1750
|
+
this.sendSigned(conn, {
|
|
1751
|
+
type: "challenge_response",
|
|
1752
|
+
signedNonce: response.toString("hex")
|
|
1753
|
+
});
|
|
1754
|
+
state.weResponded = true;
|
|
1755
|
+
this.maybeMarkReady(conn, state);
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
case "challenge_response": {
|
|
1759
|
+
if (state.peerVerified)
|
|
1760
|
+
return;
|
|
1761
|
+
if (typeof msg.signedNonce !== "string")
|
|
1762
|
+
return;
|
|
1763
|
+
let sigBuf;
|
|
1764
|
+
try {
|
|
1765
|
+
sigBuf = Buffer.from(msg.signedNonce, "hex");
|
|
1766
|
+
} catch {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (sigBuf.length !== 64)
|
|
1770
|
+
return;
|
|
1771
|
+
if (!verifySignature(publicKey, state.ourNonce, sigBuf))
|
|
1772
|
+
return;
|
|
1773
|
+
state.peerVerified = true;
|
|
1774
|
+
this.maybeMarkReady(conn, state);
|
|
1501
1775
|
break;
|
|
1502
1776
|
}
|
|
1503
1777
|
case "search_query": {
|
|
1778
|
+
if (!state.ready)
|
|
1779
|
+
return;
|
|
1504
1780
|
this.emit("search_request", {
|
|
1505
|
-
peerId:
|
|
1506
|
-
// queryId를 추적용으로 사용
|
|
1781
|
+
peerId: state.peerId,
|
|
1507
1782
|
queryId: msg.queryId,
|
|
1508
1783
|
embedding: msg.embedding,
|
|
1509
1784
|
limit: msg.limit,
|
|
1510
|
-
|
|
1511
|
-
respondTo: (() => {
|
|
1512
|
-
for (const [pid, peer] of this.peers) {
|
|
1513
|
-
if (peer.conn === conn)
|
|
1514
|
-
return pid;
|
|
1515
|
-
}
|
|
1516
|
-
return null;
|
|
1517
|
-
})()
|
|
1785
|
+
respondTo: state.peerId
|
|
1518
1786
|
});
|
|
1519
1787
|
break;
|
|
1520
1788
|
}
|
|
1521
1789
|
case "search_result": {
|
|
1790
|
+
if (!state.ready)
|
|
1791
|
+
return;
|
|
1522
1792
|
this.emit("search_response", {
|
|
1523
1793
|
queryId: msg.queryId,
|
|
1524
1794
|
results: msg.results,
|
|
1525
|
-
peerId:
|
|
1526
|
-
for (const [pid, peer] of this.peers) {
|
|
1527
|
-
if (peer.conn === conn)
|
|
1528
|
-
return pid;
|
|
1529
|
-
}
|
|
1530
|
-
return "unknown";
|
|
1531
|
-
})()
|
|
1795
|
+
peerId: state.peerId ?? "unknown"
|
|
1532
1796
|
});
|
|
1533
1797
|
break;
|
|
1534
1798
|
}
|
|
1535
1799
|
case "leave": {
|
|
1536
|
-
|
|
1537
|
-
|
|
1800
|
+
if (!state.ready)
|
|
1801
|
+
return;
|
|
1802
|
+
if (msg.peerId !== state.peerId)
|
|
1803
|
+
return;
|
|
1804
|
+
const registered = this.peers.get(msg.peerId);
|
|
1805
|
+
if (registered && registered.conn === conn) {
|
|
1538
1806
|
this.peers.delete(msg.peerId);
|
|
1539
1807
|
this.emit("peer_left", { peerId: msg.peerId });
|
|
1540
1808
|
}
|
|
1541
1809
|
break;
|
|
1542
1810
|
}
|
|
1811
|
+
case "challenge":
|
|
1812
|
+
case "ready":
|
|
1813
|
+
break;
|
|
1814
|
+
case "handshake":
|
|
1815
|
+
console.warn(`[federation] Rejected v1 handshake from peerId=${state.peerId ?? "unknown"}. Peer must upgrade to v2 (Ed25519).`);
|
|
1816
|
+
break;
|
|
1543
1817
|
}
|
|
1544
1818
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1819
|
+
maybeMarkReady(conn, state) {
|
|
1820
|
+
if (state.ready)
|
|
1821
|
+
return;
|
|
1822
|
+
if (!state.weResponded || !state.peerVerified)
|
|
1823
|
+
return;
|
|
1824
|
+
if (!state.peerId || !state.pendingPeerInfo)
|
|
1825
|
+
return;
|
|
1826
|
+
state.ready = true;
|
|
1827
|
+
if (state.handshakeTimer) {
|
|
1828
|
+
clearTimeout(state.handshakeTimer);
|
|
1829
|
+
state.handshakeTimer = null;
|
|
1550
1830
|
}
|
|
1831
|
+
this.peers.set(state.peerId, { info: state.pendingPeerInfo, conn });
|
|
1832
|
+
this.emit("peer_joined", state.pendingPeerInfo);
|
|
1551
1833
|
}
|
|
1552
1834
|
};
|
|
1553
1835
|
}
|
|
@@ -1727,10 +2009,10 @@ var init_sharing = __esm({
|
|
|
1727
2009
|
/\b\d{6}[-]\d{7}\b/
|
|
1728
2010
|
];
|
|
1729
2011
|
DEFAULT_CONFIG2 = {
|
|
1730
|
-
defaultLevel:
|
|
1731
|
-
// 기본:
|
|
1732
|
-
myNodeLevel:
|
|
1733
|
-
// 내 노드 기본:
|
|
2012
|
+
defaultLevel: 1,
|
|
2013
|
+
// 기본: 제목+유사도까지만 (스니펫 안 보냄)
|
|
2014
|
+
myNodeLevel: 0,
|
|
2015
|
+
// 내 노드 기본: 수신 전용 (명시적 set-level 필요)
|
|
1734
2016
|
rules: [
|
|
1735
2017
|
// 기본 규칙
|
|
1736
2018
|
{ pattern: "public", type: "tag", level: 4 },
|
|
@@ -1817,6 +2099,11 @@ var init_search = __esm({
|
|
|
1817
2099
|
this.node.on("search_request", async (req) => {
|
|
1818
2100
|
if (!req.respondTo)
|
|
1819
2101
|
return;
|
|
2102
|
+
const cfg = loadSharingConfig();
|
|
2103
|
+
if (cfg.myNodeLevel === 0) {
|
|
2104
|
+
this.node.sendSearchResult(req.respondTo, req.queryId, []);
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
1820
2107
|
try {
|
|
1821
2108
|
const allStores = [this.store, ...this.additionalStores];
|
|
1822
2109
|
const allScored = await Promise.all(allStores.map((s) => s.searchSemantic(req.embedding, req.limit).catch(() => [])));
|
|
@@ -1852,8 +2139,13 @@ var federation_exports = {};
|
|
|
1852
2139
|
__export(federation_exports, {
|
|
1853
2140
|
FederatedSearch: () => FederatedSearch,
|
|
1854
2141
|
FederationNode: () => FederationNode,
|
|
1855
|
-
getOrCreateIdentity: () => getOrCreateIdentity
|
|
2142
|
+
getOrCreateIdentity: () => getOrCreateIdentity,
|
|
2143
|
+
isFederationExperimentalEnabled: () => isFederationExperimentalEnabled
|
|
1856
2144
|
});
|
|
2145
|
+
function isFederationExperimentalEnabled() {
|
|
2146
|
+
const v = (process.env.STELLAVAULT_FEDERATION_EXPERIMENTAL ?? "").trim().toLowerCase();
|
|
2147
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
2148
|
+
}
|
|
1857
2149
|
var init_federation = __esm({
|
|
1858
2150
|
"packages/core/dist/federation/index.js"() {
|
|
1859
2151
|
"use strict";
|
|
@@ -3116,7 +3408,8 @@ import { Command } from "commander";
|
|
|
3116
3408
|
// packages/cli/dist/commands/index-cmd.js
|
|
3117
3409
|
import ora from "ora";
|
|
3118
3410
|
import chalk from "chalk";
|
|
3119
|
-
import { createHash as
|
|
3411
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
3412
|
+
import { writeFileSync as writeFileSync15 } from "node:fs";
|
|
3120
3413
|
import { join as join20 } from "node:path";
|
|
3121
3414
|
import { homedir as homedir12 } from "node:os";
|
|
3122
3415
|
import { mkdirSync as mkdirSync15 } from "node:fs";
|
|
@@ -3187,9 +3480,8 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
|
|
|
3187
3480
|
const rows = db.prepare(`
|
|
3188
3481
|
SELECT chunk_id, distance
|
|
3189
3482
|
FROM chunk_embeddings
|
|
3190
|
-
WHERE embedding MATCH ?
|
|
3483
|
+
WHERE embedding MATCH ? AND k = ?
|
|
3191
3484
|
ORDER BY distance
|
|
3192
|
-
LIMIT ?
|
|
3193
3485
|
`).all(float32Buffer(embedding), limit);
|
|
3194
3486
|
return rows.map((r) => ({
|
|
3195
3487
|
chunkId: r.chunk_id,
|
|
@@ -3276,15 +3568,16 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
|
|
|
3276
3568
|
return result;
|
|
3277
3569
|
},
|
|
3278
3570
|
async findDocumentNeighbors(embedding, limit) {
|
|
3571
|
+
const knnK = Math.max(limit * 3, 30);
|
|
3279
3572
|
const rows = db.prepare(`
|
|
3280
3573
|
SELECT c.document_id, MIN(ce.distance) as distance
|
|
3281
3574
|
FROM chunk_embeddings ce
|
|
3282
3575
|
JOIN chunks c ON c.id = ce.chunk_id
|
|
3283
|
-
WHERE ce.embedding MATCH ?
|
|
3576
|
+
WHERE ce.embedding MATCH ? AND k = ?
|
|
3284
3577
|
GROUP BY c.document_id
|
|
3285
3578
|
ORDER BY distance
|
|
3286
3579
|
LIMIT ?
|
|
3287
|
-
`).all(float32Buffer(embedding), limit * 2);
|
|
3580
|
+
`).all(float32Buffer(embedding), knnK, limit * 2);
|
|
3288
3581
|
return rows.slice(0, limit).map((r) => ({
|
|
3289
3582
|
documentId: r.document_id,
|
|
3290
3583
|
similarity: 1 / (1 + r.distance)
|
|
@@ -4631,7 +4924,7 @@ Please write the ${format} draft based on the context above. Save the result to
|
|
|
4631
4924
|
|
|
4632
4925
|
// packages/core/dist/mcp/tools/agentic-graph.js
|
|
4633
4926
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "node:fs";
|
|
4634
|
-
import { join as join7 } from "node:path";
|
|
4927
|
+
import { join as join7, resolve as resolvePath } from "node:path";
|
|
4635
4928
|
function createAgenticGraphTools(store, embedder, vaultPath) {
|
|
4636
4929
|
let nodeCreationCount = 0;
|
|
4637
4930
|
let nodeCreationWindowStart = Date.now();
|
|
@@ -4704,8 +4997,8 @@ ${content}${relatedSection}`;
|
|
|
4704
4997
|
const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
4705
4998
|
const dir = join7(vaultPath, folder);
|
|
4706
4999
|
const filePath = join7(dir, `${safeTitle}.md`);
|
|
4707
|
-
const resolvedPath =
|
|
4708
|
-
const resolvedVault =
|
|
5000
|
+
const resolvedPath = resolvePath(filePath);
|
|
5001
|
+
const resolvedVault = resolvePath(vaultPath);
|
|
4709
5002
|
if (!resolvedPath.startsWith(resolvedVault)) {
|
|
4710
5003
|
return { content: [{ type: "text", text: "Error: invalid folder path." }] };
|
|
4711
5004
|
}
|
|
@@ -4771,6 +5064,7 @@ The note will appear in the graph after next index.`
|
|
|
4771
5064
|
// packages/core/dist/mcp/server.js
|
|
4772
5065
|
function createMcpServer(options) {
|
|
4773
5066
|
const { store, searchEngine, embedder, vaultPath = "", decayEngine } = options;
|
|
5067
|
+
const ready = options.ready ?? Promise.resolve();
|
|
4774
5068
|
const learningPathTool = createLearningPathTool(store);
|
|
4775
5069
|
const detectGapsTool = createDetectGapsTool(store);
|
|
4776
5070
|
const getEvolutionTool = createGetEvolutionTool(store);
|
|
@@ -4802,6 +5096,7 @@ function createMcpServer(options) {
|
|
|
4802
5096
|
]
|
|
4803
5097
|
}));
|
|
4804
5098
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5099
|
+
await ready;
|
|
4805
5100
|
const { name, arguments: args } = request.params;
|
|
4806
5101
|
try {
|
|
4807
5102
|
let result;
|
|
@@ -4905,8 +5200,18 @@ function createMcpServer(options) {
|
|
|
4905
5200
|
const { createServer } = await import("node:http");
|
|
4906
5201
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => `sv-${Date.now()}` });
|
|
4907
5202
|
await server.connect(transport);
|
|
5203
|
+
const allowedOrigins = options.corsOrigins ?? ["http://localhost", "http://127.0.0.1"];
|
|
5204
|
+
const allowWildcard = allowedOrigins.includes("*");
|
|
4908
5205
|
const httpServer = createServer(async (req, res) => {
|
|
4909
|
-
|
|
5206
|
+
const origin = req.headers.origin;
|
|
5207
|
+
if (origin) {
|
|
5208
|
+
if (allowWildcard) {
|
|
5209
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
5210
|
+
} else if (allowedOrigins.some((allowed) => origin === allowed || origin.startsWith(allowed + ":"))) {
|
|
5211
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
5212
|
+
res.setHeader("Vary", "Origin");
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
4910
5215
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4911
5216
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
4912
5217
|
if (req.method === "OPTIONS") {
|
|
@@ -5119,11 +5424,11 @@ Author: ${pack2.author}`,
|
|
|
5119
5424
|
init_graph_data();
|
|
5120
5425
|
import express from "express";
|
|
5121
5426
|
import cors from "cors";
|
|
5122
|
-
import { randomBytes as
|
|
5427
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
5123
5428
|
|
|
5124
5429
|
// packages/core/dist/api/routes/federation.js
|
|
5125
5430
|
import { Router } from "express";
|
|
5126
|
-
function createFederationRouter(store) {
|
|
5431
|
+
function createFederationRouter(store, requireAuth) {
|
|
5127
5432
|
const router = Router();
|
|
5128
5433
|
let federationNode = null;
|
|
5129
5434
|
let federationAvailable = null;
|
|
@@ -5166,7 +5471,15 @@ function createFederationRouter(store) {
|
|
|
5166
5471
|
res.json({ available: true, active: false, peerCount: 0, peers: [], displayName: null, peerId: null });
|
|
5167
5472
|
}
|
|
5168
5473
|
});
|
|
5169
|
-
router.post("/join", async (req, res) => {
|
|
5474
|
+
router.post("/join", requireAuth, async (req, res) => {
|
|
5475
|
+
const { isFederationExperimentalEnabled: isFederationExperimentalEnabled2 } = await Promise.resolve().then(() => (init_federation(), federation_exports));
|
|
5476
|
+
if (!isFederationExperimentalEnabled2()) {
|
|
5477
|
+
return res.status(503).json({
|
|
5478
|
+
success: false,
|
|
5479
|
+
error: "federation-experimental-disabled",
|
|
5480
|
+
message: "Federation is experimental and disabled by default. Set STELLAVAULT_FEDERATION_EXPERIMENTAL=1 in the environment to enable."
|
|
5481
|
+
});
|
|
5482
|
+
}
|
|
5170
5483
|
const available = await probeFederationAvailable();
|
|
5171
5484
|
if (!available) {
|
|
5172
5485
|
return res.status(501).json({
|
|
@@ -5208,7 +5521,7 @@ function createFederationRouter(store) {
|
|
|
5208
5521
|
res.status(500).json({ success: false, error: err instanceof Error ? err.message : "Federation join failed" });
|
|
5209
5522
|
}
|
|
5210
5523
|
});
|
|
5211
|
-
router.post("/leave", async (_req, res) => {
|
|
5524
|
+
router.post("/leave", requireAuth, async (_req, res) => {
|
|
5212
5525
|
if (!federationNode || !federationNode.isRunning) {
|
|
5213
5526
|
return res.json({ success: true, active: false, message: "Not active" });
|
|
5214
5527
|
}
|
|
@@ -5263,7 +5576,7 @@ function createKnowledgeRouter(opts) {
|
|
|
5263
5576
|
res.status(404).json({ error: "Document not found" });
|
|
5264
5577
|
return;
|
|
5265
5578
|
}
|
|
5266
|
-
const { readFileSync: readFileSync18, writeFileSync:
|
|
5579
|
+
const { readFileSync: readFileSync18, writeFileSync: writeFileSync22, unlinkSync } = await import("node:fs");
|
|
5267
5580
|
const { join: join30, resolve: resolve22 } = await import("node:path");
|
|
5268
5581
|
const [keeper, removed] = docA.content.length >= docB.content.length ? [docA, docB] : [docB, docA];
|
|
5269
5582
|
const keeperPath = resolve22(join30(vaultPath, keeper.filePath));
|
|
@@ -5281,7 +5594,7 @@ function createKnowledgeRouter(opts) {
|
|
|
5281
5594
|
> Merged from: ${removed.title} (${removed.filePath})
|
|
5282
5595
|
|
|
5283
5596
|
${removed.content}`;
|
|
5284
|
-
|
|
5597
|
+
writeFileSync22(keeperPath, keeperContent + appendix, "utf-8");
|
|
5285
5598
|
try {
|
|
5286
5599
|
unlinkSync(removedPath);
|
|
5287
5600
|
} catch {
|
|
@@ -5304,7 +5617,7 @@ ${removed.content}`;
|
|
|
5304
5617
|
res.status(400).json({ error: "clusterA, clusterB required" });
|
|
5305
5618
|
return;
|
|
5306
5619
|
}
|
|
5307
|
-
const { writeFileSync:
|
|
5620
|
+
const { writeFileSync: writeFileSync22, mkdirSync: mkdirSync22 } = await import("node:fs");
|
|
5308
5621
|
const { join: join30, resolve: resolve22 } = await import("node:path");
|
|
5309
5622
|
const nameA = clusterA.replace(/\s*\(\d+\)$/, "");
|
|
5310
5623
|
const nameB = clusterB.replace(/\s*\(\d+\)$/, "");
|
|
@@ -5352,7 +5665,7 @@ ${removed.content}`;
|
|
|
5352
5665
|
}
|
|
5353
5666
|
mkdirSync22(dir, { recursive: true });
|
|
5354
5667
|
const filePath = join30(dir, `${safeTitle}.md`);
|
|
5355
|
-
|
|
5668
|
+
writeFileSync22(filePath, content, "utf-8");
|
|
5356
5669
|
res.json({ success: true, title: safeTitle, path: filePath });
|
|
5357
5670
|
} catch (err) {
|
|
5358
5671
|
console.error(err);
|
|
@@ -5364,6 +5677,7 @@ ${removed.content}`;
|
|
|
5364
5677
|
|
|
5365
5678
|
// packages/core/dist/api/routes/ingest.js
|
|
5366
5679
|
import { Router as Router3 } from "express";
|
|
5680
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
5367
5681
|
function createIngestRouter(opts) {
|
|
5368
5682
|
const { store, vaultPath, requireAuth, assertNotPrivateUrl } = opts;
|
|
5369
5683
|
const router = Router3();
|
|
@@ -5434,7 +5748,7 @@ function createIngestRouter(opts) {
|
|
|
5434
5748
|
});
|
|
5435
5749
|
try {
|
|
5436
5750
|
const doc = {
|
|
5437
|
-
id:
|
|
5751
|
+
id: createHash5("sha256").update(result.savedTo).digest("hex").slice(0, 16),
|
|
5438
5752
|
filePath: result.savedTo,
|
|
5439
5753
|
title: result.title,
|
|
5440
5754
|
content,
|
|
@@ -5501,12 +5815,12 @@ function createIngestRouter(opts) {
|
|
|
5501
5815
|
return;
|
|
5502
5816
|
}
|
|
5503
5817
|
try {
|
|
5504
|
-
const { writeFileSync:
|
|
5818
|
+
const { writeFileSync: writeFileSync22, unlinkSync } = await import("node:fs");
|
|
5505
5819
|
const { join: join30 } = await import("node:path");
|
|
5506
5820
|
const { tmpdir } = await import("node:os");
|
|
5507
5821
|
const safeName = (file.originalname ?? "upload").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 100);
|
|
5508
5822
|
const tmpPath = join30(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
|
|
5509
|
-
|
|
5823
|
+
writeFileSync22(tmpPath, file.buffer);
|
|
5510
5824
|
const { extractFileContent: extractFileContent2 } = await Promise.resolve().then(() => (init_file_extractors(), file_extractors_exports));
|
|
5511
5825
|
const ext = file.originalname.split(".").pop()?.toLowerCase() ?? "";
|
|
5512
5826
|
const binaryExts = /* @__PURE__ */ new Set(["pdf", "docx", "pptx", "xlsx", "xls"]);
|
|
@@ -5542,7 +5856,7 @@ function createIngestRouter(opts) {
|
|
|
5542
5856
|
});
|
|
5543
5857
|
try {
|
|
5544
5858
|
const doc = {
|
|
5545
|
-
id:
|
|
5859
|
+
id: createHash5("sha256").update(result.savedTo).digest("hex").slice(0, 16),
|
|
5546
5860
|
filePath: result.savedTo,
|
|
5547
5861
|
title: result.title,
|
|
5548
5862
|
content,
|
|
@@ -5612,7 +5926,7 @@ ${desc}
|
|
|
5612
5926
|
if (content.length > 1e4)
|
|
5613
5927
|
content = content.slice(0, 1e4) + "\n\n...(truncated)";
|
|
5614
5928
|
}
|
|
5615
|
-
const { writeFileSync:
|
|
5929
|
+
const { writeFileSync: writeFileSync22, mkdirSync: mkdirSync22 } = await import("node:fs");
|
|
5616
5930
|
const { join: join30 } = await import("node:path");
|
|
5617
5931
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5618
5932
|
const clipDir = join30(vaultPath || ".", "06_Research", "clips");
|
|
@@ -5630,7 +5944,7 @@ tags: [clip${isYT ? ", youtube" : ""}]
|
|
|
5630
5944
|
> Source: ${url}
|
|
5631
5945
|
|
|
5632
5946
|
${content}`;
|
|
5633
|
-
|
|
5947
|
+
writeFileSync22(join30(clipDir, fileName), md, "utf-8");
|
|
5634
5948
|
res.json({ success: true, fileName, path: join30(clipDir, fileName) });
|
|
5635
5949
|
} catch (err) {
|
|
5636
5950
|
console.error(err);
|
|
@@ -5888,7 +6202,7 @@ function createAnalyticsRouter(opts) {
|
|
|
5888
6202
|
function createApiServer(options) {
|
|
5889
6203
|
const { store, searchEngine, port = 3333, vaultName = "", vaultPath = "", decayEngine, graphUiPath } = options;
|
|
5890
6204
|
const app = express();
|
|
5891
|
-
const authToken =
|
|
6205
|
+
const authToken = randomBytes3(32).toString("hex");
|
|
5892
6206
|
app.use((_req, res, next) => {
|
|
5893
6207
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
5894
6208
|
res.setHeader("X-Frame-Options", "DENY");
|
|
@@ -5929,11 +6243,14 @@ function createApiServer(options) {
|
|
|
5929
6243
|
const token = req.headers["x-stellavault-token"];
|
|
5930
6244
|
if (token === authToken)
|
|
5931
6245
|
return next();
|
|
5932
|
-
|
|
5933
|
-
return next();
|
|
5934
|
-
res.status(403).json({ error: "Invalid or missing auth token. GET /api/token first." });
|
|
6246
|
+
res.status(403).json({ error: "Invalid or missing auth token. Send X-Stellavault-Token header." });
|
|
5935
6247
|
}
|
|
5936
|
-
app.get("/api/token", (
|
|
6248
|
+
app.get("/api/token", (req, res) => {
|
|
6249
|
+
const origin = req.headers.origin;
|
|
6250
|
+
if (!origin || !allowedOrigins.includes(origin)) {
|
|
6251
|
+
res.status(403).json({ error: "Token endpoint requires a same-origin browser request." });
|
|
6252
|
+
return;
|
|
6253
|
+
}
|
|
5937
6254
|
res.json({ token: authToken });
|
|
5938
6255
|
});
|
|
5939
6256
|
function assertNotPrivateUrl(url) {
|
|
@@ -6117,7 +6434,7 @@ function createApiServer(options) {
|
|
|
6117
6434
|
return;
|
|
6118
6435
|
}
|
|
6119
6436
|
const { resolve: resolve22, join: join30 } = await import("node:path");
|
|
6120
|
-
const { writeFileSync:
|
|
6437
|
+
const { writeFileSync: writeFileSync22, readFileSync: readFileSync18 } = await import("node:fs");
|
|
6121
6438
|
const fullPath = resolve22(vaultPath, doc.filePath);
|
|
6122
6439
|
if (!fullPath.startsWith(resolve22(vaultPath))) {
|
|
6123
6440
|
res.status(403).json({ error: "Access denied" });
|
|
@@ -6144,7 +6461,7 @@ function createApiServer(options) {
|
|
|
6144
6461
|
updated = content;
|
|
6145
6462
|
}
|
|
6146
6463
|
}
|
|
6147
|
-
|
|
6464
|
+
writeFileSync22(fullPath, updated, "utf-8");
|
|
6148
6465
|
await store.upsertDocument({
|
|
6149
6466
|
...doc,
|
|
6150
6467
|
title: title ?? doc.title,
|
|
@@ -6333,7 +6650,7 @@ function createApiServer(options) {
|
|
|
6333
6650
|
app.get("/api/sync/status", (_req, res) => {
|
|
6334
6651
|
res.json(syncState);
|
|
6335
6652
|
});
|
|
6336
|
-
app.use("/api/federate", createFederationRouter(store));
|
|
6653
|
+
app.use("/api/federate", createFederationRouter(store, requireAuth));
|
|
6337
6654
|
if (graphUiPath) {
|
|
6338
6655
|
app.get(/^(?!\/api\/)(?!.*\.[a-z0-9]+$).*$/i, (_req, res) => {
|
|
6339
6656
|
res.sendFile(`${graphUiPath}/index.html`);
|
|
@@ -6781,7 +7098,7 @@ async function captureVoice(audioPath, options) {
|
|
|
6781
7098
|
}
|
|
6782
7099
|
|
|
6783
7100
|
// packages/core/dist/cloud/sync.js
|
|
6784
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
7101
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, createHash as createHash6 } from "node:crypto";
|
|
6785
7102
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync14, chmodSync as chmodSync2 } from "node:fs";
|
|
6786
7103
|
import { join as join15 } from "node:path";
|
|
6787
7104
|
import { homedir as homedir7 } from "node:os";
|
|
@@ -6789,7 +7106,7 @@ var CLOUD_DIR = join15(homedir7(), ".stellavault", "cloud");
|
|
|
6789
7106
|
var KEY_FILE = join15(CLOUD_DIR, "encryption.key");
|
|
6790
7107
|
var SYNC_STATE_FILE = join15(CLOUD_DIR, "sync-state.json");
|
|
6791
7108
|
function encrypt(data, key) {
|
|
6792
|
-
const iv =
|
|
7109
|
+
const iv = randomBytes4(16);
|
|
6793
7110
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
6794
7111
|
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
6795
7112
|
const tag = cipher.getAuthTag();
|
|
@@ -6803,14 +7120,14 @@ function decrypt(encrypted, key, iv, tag) {
|
|
|
6803
7120
|
function getOrCreateEncryptionKey(userKey) {
|
|
6804
7121
|
mkdirSync14(CLOUD_DIR, { recursive: true });
|
|
6805
7122
|
if (userKey) {
|
|
6806
|
-
const key2 =
|
|
7123
|
+
const key2 = createHash6("sha256").update(userKey).digest();
|
|
6807
7124
|
writeFileSync14(KEY_FILE, key2.toString("hex"), "utf-8");
|
|
6808
7125
|
return key2;
|
|
6809
7126
|
}
|
|
6810
7127
|
if (existsSync14(KEY_FILE)) {
|
|
6811
7128
|
return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
|
|
6812
7129
|
}
|
|
6813
|
-
const key =
|
|
7130
|
+
const key = randomBytes4(32);
|
|
6814
7131
|
writeFileSync14(KEY_FILE, key.toString("hex"), { encoding: "utf-8", mode: 384 });
|
|
6815
7132
|
try {
|
|
6816
7133
|
chmodSync2(KEY_FILE, 384);
|
|
@@ -6837,7 +7154,7 @@ async function s3Put(config, objectKey, data, contentType = "application/octet-s
|
|
|
6837
7154
|
"Content-Type": contentType,
|
|
6838
7155
|
"Content-Length": String(data.length),
|
|
6839
7156
|
"x-amz-date": date,
|
|
6840
|
-
"x-amz-content-sha256":
|
|
7157
|
+
"x-amz-content-sha256": createHash6("sha256").update(data).digest("hex"),
|
|
6841
7158
|
// R2는 Bearer token 지원
|
|
6842
7159
|
"Authorization": `Bearer ${secretAccessKey}`
|
|
6843
7160
|
},
|
|
@@ -6936,23 +7253,25 @@ var CREDITS_FILE = join19(homedir11(), ".stellavault", "federation", "credits.js
|
|
|
6936
7253
|
init_retry();
|
|
6937
7254
|
init_math();
|
|
6938
7255
|
init_indexer();
|
|
6939
|
-
function createKnowledgeHub(config) {
|
|
7256
|
+
function createKnowledgeHub(config, options = {}) {
|
|
6940
7257
|
const embedder = createLocalEmbedder(config.embedding.localModel);
|
|
6941
7258
|
const dims = embedder.dimensions;
|
|
6942
7259
|
const store = createSqliteVecStore(config.dbPath, dims);
|
|
6943
7260
|
const searchEngine = createSearchEngine({ store, embedder, rrfK: config.search.rrfK });
|
|
6944
|
-
const mcpServer = createMcpServer({ store, searchEngine, vaultPath: config.vaultPath });
|
|
7261
|
+
const mcpServer = createMcpServer({ store, searchEngine, vaultPath: config.vaultPath, ready: options.ready });
|
|
6945
7262
|
return { store, embedder, searchEngine, mcpServer, config };
|
|
6946
7263
|
}
|
|
6947
7264
|
|
|
6948
7265
|
// packages/cli/dist/commands/index-cmd.js
|
|
6949
7266
|
function getVaultDbPath(vaultPath) {
|
|
6950
|
-
const hash =
|
|
7267
|
+
const hash = createHash7("sha256").update(vaultPath).digest("hex").slice(0, 8);
|
|
6951
7268
|
const dir = join20(homedir12(), ".stellavault", "vaults");
|
|
6952
7269
|
mkdirSync15(dir, { recursive: true });
|
|
6953
7270
|
return join20(dir, `${hash}.db`);
|
|
6954
7271
|
}
|
|
6955
|
-
async function indexCommand(vaultPath) {
|
|
7272
|
+
async function indexCommand(vaultPath, opts = {}) {
|
|
7273
|
+
if (opts.profileMemory)
|
|
7274
|
+
process.env.STELLAVAULT_PROFILE_MEMORY = "1";
|
|
6956
7275
|
const config = loadConfig();
|
|
6957
7276
|
const vault2 = vaultPath ?? config.vaultPath;
|
|
6958
7277
|
if (!vault2) {
|
|
@@ -6969,28 +7288,85 @@ async function indexCommand(vaultPath) {
|
|
|
6969
7288
|
} catch {
|
|
6970
7289
|
}
|
|
6971
7290
|
}
|
|
6972
|
-
const
|
|
7291
|
+
const spinnerEnabled = !opts.noSpinner && !opts.verbose && process.stderr.isTTY;
|
|
7292
|
+
const spinner = ora({ text: "Initializing...", isEnabled: spinnerEnabled }).start();
|
|
6973
7293
|
const store = createSqliteVecStore(dbPath);
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
spinner.text = "Starting indexing...";
|
|
6979
|
-
const result = await indexVault(vault2, {
|
|
6980
|
-
store,
|
|
6981
|
-
embedder,
|
|
6982
|
-
chunkOptions: config.chunking,
|
|
6983
|
-
onProgress(current, total, doc) {
|
|
6984
|
-
spinner.text = `[${current}/${total}] ${doc.title}`;
|
|
7294
|
+
const cleanupSpinner = () => {
|
|
7295
|
+
try {
|
|
7296
|
+
spinner.stop();
|
|
7297
|
+
} catch {
|
|
6985
7298
|
}
|
|
7299
|
+
};
|
|
7300
|
+
process.once("uncaughtException", cleanupSpinner);
|
|
7301
|
+
process.once("SIGINT", () => {
|
|
7302
|
+
cleanupSpinner();
|
|
7303
|
+
process.exit(130);
|
|
6986
7304
|
});
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
7305
|
+
process.once("SIGTERM", () => {
|
|
7306
|
+
cleanupSpinner();
|
|
7307
|
+
process.exit(143);
|
|
7308
|
+
});
|
|
7309
|
+
try {
|
|
7310
|
+
await store.initialize();
|
|
7311
|
+
spinner.text = "Loading embedding model...";
|
|
7312
|
+
const embedder = createLocalEmbedder(config.embedding.localModel);
|
|
7313
|
+
await embedder.initialize();
|
|
7314
|
+
spinner.text = "Starting indexing...";
|
|
7315
|
+
const result = await indexVault(vault2, {
|
|
7316
|
+
store,
|
|
7317
|
+
embedder,
|
|
7318
|
+
chunkOptions: config.chunking,
|
|
7319
|
+
onProgress(current, total, doc) {
|
|
7320
|
+
const mb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
7321
|
+
if (spinnerEnabled) {
|
|
7322
|
+
spinner.text = `[${current}/${total}] ${doc.title} (${mb}MB)`;
|
|
7323
|
+
} else if (opts.verbose || current % 50 === 0 || current === total) {
|
|
7324
|
+
console.error(`[${current}/${total}] ${doc.title} (${mb}MB)`);
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
});
|
|
7328
|
+
await store.close();
|
|
7329
|
+
spinner.stop();
|
|
7330
|
+
const reasonCount = {
|
|
7331
|
+
"empty": 0,
|
|
7332
|
+
"parse-error": 0,
|
|
7333
|
+
"binary": 0,
|
|
7334
|
+
"too-large": 0,
|
|
7335
|
+
"unreadable": 0
|
|
7336
|
+
};
|
|
7337
|
+
for (const s of result.skippedFiles)
|
|
7338
|
+
reasonCount[s.reason]++;
|
|
7339
|
+
console.log("");
|
|
7340
|
+
console.log(chalk.green("\u2705 Indexing complete"));
|
|
7341
|
+
console.log(` \u{1F4C1} Files: ${result.totalFiles} total`);
|
|
7342
|
+
console.log(` \u{1F4C4} Indexed: ${result.indexed} | \u23ED\uFE0F Unchanged: ${result.skipped} | \u{1F5D1}\uFE0F Deleted: ${result.deleted}${result.failed ? ` | \u274C Failed: ${result.failed}` : ""}`);
|
|
7343
|
+
if (result.skippedFiles.length > 0) {
|
|
7344
|
+
const parts = Object.entries(reasonCount).filter(([, c]) => c > 0).map(([r, c]) => `${r}=${c}`).join(", ");
|
|
7345
|
+
console.log(` \u26A0\uFE0F Skipped: ${result.skippedFiles.length} (${parts})`);
|
|
7346
|
+
}
|
|
7347
|
+
console.log(` \u{1F9E9} Chunks: ${result.totalChunks} | \u23F1 ${(result.elapsedMs / 1e3).toFixed(1)}s`);
|
|
7348
|
+
console.log(` \u{1F4BE} DB: ${dbPath}`);
|
|
7349
|
+
if (opts.logSkipped) {
|
|
7350
|
+
writeFileSync15(opts.logSkipped, JSON.stringify({ skipped: result.skippedFiles, failed: result.failedFiles }, null, 2));
|
|
7351
|
+
console.log(chalk.dim(` \u{1F4CB} Skip log: ${opts.logSkipped}`));
|
|
7352
|
+
}
|
|
7353
|
+
} catch (err) {
|
|
7354
|
+
spinner.fail(chalk.red("Indexing failed"));
|
|
7355
|
+
const e = err;
|
|
7356
|
+
console.error(chalk.red(`
|
|
7357
|
+
${e.message ?? err}`));
|
|
7358
|
+
if (e.stack)
|
|
7359
|
+
console.error(chalk.dim(e.stack.split("\n").slice(1, 5).join("\n")));
|
|
7360
|
+
if ((e.message ?? "").match(/heap|out of memory|allocation failed/i)) {
|
|
7361
|
+
console.error(chalk.yellow("\n \u{1F4A1} Hint: large vault detected. Retry with a larger Node heap:"));
|
|
7362
|
+
console.error(chalk.yellow(' NODE_OPTIONS="--max-old-space-size=8192 --expose-gc" stellavault index <path>'));
|
|
7363
|
+
}
|
|
7364
|
+
try {
|
|
7365
|
+
await store.close();
|
|
7366
|
+
} catch {
|
|
7367
|
+
}
|
|
7368
|
+
process.exit(1);
|
|
7369
|
+
}
|
|
6994
7370
|
}
|
|
6995
7371
|
|
|
6996
7372
|
// packages/cli/dist/commands/search-cmd.js
|
|
@@ -7038,14 +7414,29 @@ async function searchCommand(query, options, cmd) {
|
|
|
7038
7414
|
import chalk3 from "chalk";
|
|
7039
7415
|
async function serveCommand() {
|
|
7040
7416
|
const config = loadConfig();
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
console.error(
|
|
7417
|
+
let resolveReady;
|
|
7418
|
+
const ready = new Promise((r) => {
|
|
7419
|
+
resolveReady = r;
|
|
7420
|
+
});
|
|
7421
|
+
const hub = createKnowledgeHub(config, { ready });
|
|
7422
|
+
console.error(chalk3.green("\u{1F680} MCP Server running (stdio mode) \u2014 index loading in background"));
|
|
7047
7423
|
console.error(chalk3.dim("\u{1F4A1} Claude Code: claude mcp add stellavault -- stellavault serve"));
|
|
7048
|
-
|
|
7424
|
+
const serverPromise = hub.mcpServer.startStdio();
|
|
7425
|
+
(async () => {
|
|
7426
|
+
try {
|
|
7427
|
+
const t0 = Date.now();
|
|
7428
|
+
await hub.store.initialize();
|
|
7429
|
+
await hub.embedder.initialize();
|
|
7430
|
+
const stats = await hub.store.getStats();
|
|
7431
|
+
const elapsed = Date.now() - t0;
|
|
7432
|
+
console.error(`\u{1F4DA} ${stats.documentCount} documents | ${stats.chunkCount} chunks (ready in ${elapsed}ms)`);
|
|
7433
|
+
resolveReady();
|
|
7434
|
+
} catch (err) {
|
|
7435
|
+
console.error(chalk3.red("\u274C Index load failed: " + err.message));
|
|
7436
|
+
resolveReady();
|
|
7437
|
+
}
|
|
7438
|
+
})();
|
|
7439
|
+
await serverPromise;
|
|
7049
7440
|
}
|
|
7050
7441
|
|
|
7051
7442
|
// packages/cli/dist/commands/status-cmd.js
|
|
@@ -7059,14 +7450,34 @@ async function statusCommand(_opts, cmd) {
|
|
|
7059
7450
|
const stats = await store.getStats();
|
|
7060
7451
|
const topics = await store.getTopics();
|
|
7061
7452
|
await store.close();
|
|
7453
|
+
let totalFiles = null;
|
|
7454
|
+
let skippedFiles = null;
|
|
7455
|
+
if (config.vaultPath) {
|
|
7456
|
+
try {
|
|
7457
|
+
const scan = scanVault(config.vaultPath);
|
|
7458
|
+
totalFiles = scan.scannedFiles;
|
|
7459
|
+
skippedFiles = scan.skippedFiles;
|
|
7460
|
+
} catch {
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7062
7463
|
if (jsonMode) {
|
|
7063
|
-
console.log(JSON.stringify({
|
|
7464
|
+
console.log(JSON.stringify({
|
|
7465
|
+
...stats,
|
|
7466
|
+
totalFiles,
|
|
7467
|
+
skippedFiles,
|
|
7468
|
+
vaultPath: config.vaultPath,
|
|
7469
|
+
dbPath: config.dbPath,
|
|
7470
|
+
topics: topics.slice(0, 20)
|
|
7471
|
+
}, null, 2));
|
|
7064
7472
|
return;
|
|
7065
7473
|
}
|
|
7066
7474
|
console.log("");
|
|
7067
7475
|
console.log(chalk4.bold("\u{1F4CA} Stellavault Status"));
|
|
7068
7476
|
console.log("\u2500".repeat(40));
|
|
7069
|
-
console.log(` \u{1F4C4} Documents: ${stats.documentCount}`);
|
|
7477
|
+
console.log(` \u{1F4C4} Documents: ${stats.documentCount}${totalFiles != null ? ` / ${totalFiles} files (${Math.round(stats.documentCount / totalFiles * 100)}%)` : ""}`);
|
|
7478
|
+
if (skippedFiles != null && skippedFiles > 0) {
|
|
7479
|
+
console.log(` \u26A0\uFE0F Skipped: ${skippedFiles} (run: stellavault index --log-skipped skipped.json)`);
|
|
7480
|
+
}
|
|
7070
7481
|
console.log(` \u{1F9E9} Chunks: ${stats.chunkCount}`);
|
|
7071
7482
|
console.log(` \u{1F550} Last indexed: ${stats.lastIndexed ?? "never"}`);
|
|
7072
7483
|
console.log(` \u{1F4BE} DB: ${config.dbPath}`);
|
|
@@ -7186,7 +7597,7 @@ async function openBrowser(url) {
|
|
|
7186
7597
|
|
|
7187
7598
|
// packages/cli/dist/commands/card-cmd.js
|
|
7188
7599
|
import chalk6 from "chalk";
|
|
7189
|
-
import { writeFileSync as
|
|
7600
|
+
import { writeFileSync as writeFileSync16 } from "node:fs";
|
|
7190
7601
|
import { resolve as resolve12 } from "node:path";
|
|
7191
7602
|
async function cardCommand(options) {
|
|
7192
7603
|
const output = options.output ?? "knowledge-card.svg";
|
|
@@ -7197,7 +7608,7 @@ async function cardCommand(options) {
|
|
|
7197
7608
|
if (!res.ok)
|
|
7198
7609
|
throw new Error(`API error: ${res.status}. Is 'stellavault graph' running?`);
|
|
7199
7610
|
const svg = await res.text();
|
|
7200
|
-
|
|
7611
|
+
writeFileSync16(outPath, svg, "utf-8");
|
|
7201
7612
|
console.error(chalk6.green(`\u2705 Profile card saved: ${outPath}`));
|
|
7202
7613
|
console.error(chalk6.dim(" Embed in GitHub README:"));
|
|
7203
7614
|
console.error(chalk6.dim(` `));
|
|
@@ -7248,8 +7659,8 @@ async function packExportCommand(name, options) {
|
|
|
7248
7659
|
}
|
|
7249
7660
|
const outPath = resolve13(process.cwd(), options.output ?? `${name}.sv-pack`);
|
|
7250
7661
|
const content = readFileSync15(srcPath, "utf-8");
|
|
7251
|
-
const { writeFileSync:
|
|
7252
|
-
|
|
7662
|
+
const { writeFileSync: writeFileSync22 } = await import("node:fs");
|
|
7663
|
+
writeFileSync22(outPath, content);
|
|
7253
7664
|
console.error(chalk7.green(`\u2705 Exported: ${outPath}`));
|
|
7254
7665
|
}
|
|
7255
7666
|
async function packImportCommand(filePath) {
|
|
@@ -7406,11 +7817,29 @@ function runScript(scriptPath, cwd) {
|
|
|
7406
7817
|
// packages/cli/dist/commands/review-cmd.js
|
|
7407
7818
|
import chalk10 from "chalk";
|
|
7408
7819
|
import { createInterface } from "node:readline";
|
|
7820
|
+
function globToRegex(glob) {
|
|
7821
|
+
const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]*").replace(/\?/g, ".");
|
|
7822
|
+
return new RegExp("^" + esc + "$");
|
|
7823
|
+
}
|
|
7824
|
+
function seededRotate(arr, seed) {
|
|
7825
|
+
if (!seed)
|
|
7826
|
+
return arr;
|
|
7827
|
+
let h = 2166136261;
|
|
7828
|
+
for (let i = 0; i < seed.length; i++) {
|
|
7829
|
+
h ^= seed.charCodeAt(i);
|
|
7830
|
+
h = Math.imul(h, 16777619);
|
|
7831
|
+
}
|
|
7832
|
+
const offset = Math.abs(h) % Math.max(1, arr.length);
|
|
7833
|
+
return arr.slice(offset).concat(arr.slice(0, offset));
|
|
7834
|
+
}
|
|
7409
7835
|
async function reviewCommand(options) {
|
|
7410
7836
|
const config = loadConfig();
|
|
7411
7837
|
const hub = createKnowledgeHub(config);
|
|
7412
7838
|
const count = parseInt(options.count ?? "5", 10);
|
|
7413
|
-
|
|
7839
|
+
const minAgeDays = parseInt(options.minAge ?? "0", 10);
|
|
7840
|
+
const excludeRe = options.exclude ? globToRegex(options.exclude) : null;
|
|
7841
|
+
if (!options.json)
|
|
7842
|
+
console.error(chalk10.dim("\u23F3 Initializing..."));
|
|
7414
7843
|
await hub.store.initialize();
|
|
7415
7844
|
const db = hub.store.getDb();
|
|
7416
7845
|
if (!db) {
|
|
@@ -7418,7 +7847,41 @@ async function reviewCommand(options) {
|
|
|
7418
7847
|
process.exit(1);
|
|
7419
7848
|
}
|
|
7420
7849
|
const decayEngine = new DecayEngine(db);
|
|
7421
|
-
|
|
7850
|
+
let pool = await decayEngine.getDecaying(0.6, Math.max(count * 5, 50));
|
|
7851
|
+
if (excludeRe || minAgeDays > 0) {
|
|
7852
|
+
pool = pool.filter((d) => {
|
|
7853
|
+
const doc = db.prepare("SELECT file_path FROM documents WHERE id = ?").get(d.documentId);
|
|
7854
|
+
const fp = doc?.file_path ?? "";
|
|
7855
|
+
if (excludeRe && excludeRe.test(fp))
|
|
7856
|
+
return false;
|
|
7857
|
+
if (minAgeDays > 0) {
|
|
7858
|
+
const ageDays = (Date.now() - new Date(d.lastAccess).getTime()) / 864e5;
|
|
7859
|
+
if (ageDays < minAgeDays)
|
|
7860
|
+
return false;
|
|
7861
|
+
}
|
|
7862
|
+
return true;
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7865
|
+
if (options.seed)
|
|
7866
|
+
pool = seededRotate(pool, options.seed);
|
|
7867
|
+
const decaying = pool.slice(0, count);
|
|
7868
|
+
if (options.json) {
|
|
7869
|
+
const out = decaying.map((d) => {
|
|
7870
|
+
const doc = db.prepare("SELECT file_path FROM documents WHERE id = ?").get(d.documentId);
|
|
7871
|
+
const ageDays = Math.round((Date.now() - new Date(d.lastAccess).getTime()) / 864e5);
|
|
7872
|
+
return {
|
|
7873
|
+
documentId: d.documentId,
|
|
7874
|
+
title: d.title,
|
|
7875
|
+
filePath: doc?.file_path ?? null,
|
|
7876
|
+
retrievability: d.retrievability,
|
|
7877
|
+
lastAccess: d.lastAccess,
|
|
7878
|
+
ageDays,
|
|
7879
|
+
reviewScore: d.retrievability * 0.7 + Math.min(1, ageDays / 30) * 0.3
|
|
7880
|
+
};
|
|
7881
|
+
});
|
|
7882
|
+
console.log(JSON.stringify(out, null, 2));
|
|
7883
|
+
return;
|
|
7884
|
+
}
|
|
7422
7885
|
if (decaying.length === 0) {
|
|
7423
7886
|
console.log(chalk10.green("\n\u2728 All knowledge is healthy! No notes to review."));
|
|
7424
7887
|
return;
|
|
@@ -7567,7 +8030,7 @@ async function gapsCommand() {
|
|
|
7567
8030
|
|
|
7568
8031
|
// packages/cli/dist/commands/clip-cmd.js
|
|
7569
8032
|
import chalk13 from "chalk";
|
|
7570
|
-
import { writeFileSync as
|
|
8033
|
+
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync17 } from "node:fs";
|
|
7571
8034
|
import { join as join22 } from "node:path";
|
|
7572
8035
|
async function clipCommand(url, options) {
|
|
7573
8036
|
if (!url) {
|
|
@@ -7616,7 +8079,7 @@ async function clipCommand(url, options) {
|
|
|
7616
8079
|
"",
|
|
7617
8080
|
content
|
|
7618
8081
|
].join("\n");
|
|
7619
|
-
|
|
8082
|
+
writeFileSync17(filePath, md, "utf-8");
|
|
7620
8083
|
console.log(chalk13.green(`\u2705 Saved: ${fileName}`));
|
|
7621
8084
|
console.log(chalk13.dim(` \u2192 ${filePath}`));
|
|
7622
8085
|
console.log(chalk13.dim(" \u{1F4A1} Run stellavault index to make it searchable"));
|
|
@@ -7817,7 +8280,7 @@ async function digestCommand(options) {
|
|
|
7817
8280
|
\u{1F9E0} Health: R=${report.averageR} | Decaying ${report.decayingCount} | Critical ${report.criticalCount}`);
|
|
7818
8281
|
console.log(chalk15.dim("\n\u2550".repeat(50)));
|
|
7819
8282
|
if (options.visual) {
|
|
7820
|
-
const { writeFileSync:
|
|
8283
|
+
const { writeFileSync: writeFileSync22, mkdirSync: mkdirSync22, existsSync: existsSync25 } = await import("node:fs");
|
|
7821
8284
|
const { join: join30, resolve: resolve22 } = await import("node:path");
|
|
7822
8285
|
const outputDir = resolve22(config.vaultPath, "_stellavault/digests");
|
|
7823
8286
|
if (!existsSync25(outputDir))
|
|
@@ -7862,7 +8325,7 @@ async function digestCommand(options) {
|
|
|
7862
8325
|
"---",
|
|
7863
8326
|
`*Generated by \`stellavault digest --visual\` on ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
7864
8327
|
].filter(Boolean).join("\n");
|
|
7865
|
-
|
|
8328
|
+
writeFileSync22(outputPath, md, "utf-8");
|
|
7866
8329
|
console.log(chalk15.green(`
|
|
7867
8330
|
Visual digest saved: ${filename}`));
|
|
7868
8331
|
console.log(chalk15.dim(`Open in Obsidian to see Mermaid charts.`));
|
|
@@ -7871,7 +8334,7 @@ Visual digest saved: ${filename}`));
|
|
|
7871
8334
|
|
|
7872
8335
|
// packages/cli/dist/commands/init-cmd.js
|
|
7873
8336
|
import { createInterface as createInterface2 } from "node:readline";
|
|
7874
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync18, writeFileSync as
|
|
8337
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync18, writeFileSync as writeFileSync18 } from "node:fs";
|
|
7875
8338
|
import { join as join23 } from "node:path";
|
|
7876
8339
|
import { homedir as homedir14 } from "node:os";
|
|
7877
8340
|
import ora2 from "ora";
|
|
@@ -7919,7 +8382,7 @@ async function initCommand() {
|
|
|
7919
8382
|
search: { defaultLimit: 10, rrfK: 60 },
|
|
7920
8383
|
mcp: { mode: "stdio", port: 3333 }
|
|
7921
8384
|
};
|
|
7922
|
-
|
|
8385
|
+
writeFileSync18(join23(homedir14(), ".stellavault.json"), JSON.stringify(configData, null, 2), "utf-8");
|
|
7923
8386
|
console.log(chalk16.dim(` Config saved: ~/.stellavault.json`));
|
|
7924
8387
|
console.log("");
|
|
7925
8388
|
console.log(chalk16.cyan(" Step 2/3") + " \u2014 Indexing your vault");
|
|
@@ -8017,7 +8480,7 @@ Andrej Karpathy's approach: every session auto-compiles into structured knowledg
|
|
|
8017
8480
|
}
|
|
8018
8481
|
];
|
|
8019
8482
|
for (const s of samples) {
|
|
8020
|
-
|
|
8483
|
+
writeFileSync18(join23(rawDir, s.file), s.content, "utf-8");
|
|
8021
8484
|
}
|
|
8022
8485
|
const reSpinner = ora2({ text: " Indexing sample notes...", indent: 2 }).start();
|
|
8023
8486
|
const reResult = await indexVault(vaultPath, {
|
|
@@ -8191,10 +8654,22 @@ async function contradictionsCommand(_opts, cmd) {
|
|
|
8191
8654
|
import { createInterface as createInterface3 } from "node:readline";
|
|
8192
8655
|
import chalk19 from "chalk";
|
|
8193
8656
|
async function federateJoinCommand(options) {
|
|
8657
|
+
if (!isFederationExperimentalEnabled()) {
|
|
8658
|
+
console.log("");
|
|
8659
|
+
console.log(chalk19.red(" \u2726 Federation is experimental and disabled by default."));
|
|
8660
|
+
console.log("");
|
|
8661
|
+
console.log(chalk19.dim(" Enable it by setting an environment variable:"));
|
|
8662
|
+
console.log(chalk19.dim(' PowerShell: $env:STELLAVAULT_FEDERATION_EXPERIMENTAL = "1"'));
|
|
8663
|
+
console.log(chalk19.dim(" bash/zsh: export STELLAVAULT_FEDERATION_EXPERIMENTAL=1"));
|
|
8664
|
+
console.log("");
|
|
8665
|
+
console.log(chalk19.dim(" Then re-run `stellavault federate join`."));
|
|
8666
|
+
console.log("");
|
|
8667
|
+
process.exit(2);
|
|
8668
|
+
}
|
|
8194
8669
|
const config = loadConfig();
|
|
8195
8670
|
const identity = getOrCreateIdentity(options.name);
|
|
8196
8671
|
console.log("");
|
|
8197
|
-
console.log(chalk19.bold(" \u2726 Stellavault Federation"));
|
|
8672
|
+
console.log(chalk19.bold(" \u2726 Stellavault Federation") + chalk19.yellow(" (experimental)"));
|
|
8198
8673
|
console.log(chalk19.dim(` Node: ${identity.displayName} (${identity.peerId})`));
|
|
8199
8674
|
console.log("");
|
|
8200
8675
|
const store = createSqliteVecStore(config.dbPath);
|
|
@@ -8207,6 +8682,13 @@ async function federateJoinCommand(options) {
|
|
|
8207
8682
|
node.setLocalStats(stats.documentCount, topics.slice(0, 5).map((t2) => t2.topic));
|
|
8208
8683
|
const search = new FederatedSearch(node, store, embedder);
|
|
8209
8684
|
search.startResponder();
|
|
8685
|
+
const sharingCfg = loadSharingConfig();
|
|
8686
|
+
if (sharingCfg.myNodeLevel === 0) {
|
|
8687
|
+
console.log(chalk19.yellow(" \u26A0 Receive-only mode (my node level = 0)."));
|
|
8688
|
+
console.log(chalk19.dim(" Run `set-level 1` or higher in the federation prompt to share."));
|
|
8689
|
+
} else {
|
|
8690
|
+
console.log(chalk19.dim(` Sharing level: ${sharingCfg.myNodeLevel} (set-level <0-4> to change)`));
|
|
8691
|
+
}
|
|
8210
8692
|
node.on("joined", (info) => {
|
|
8211
8693
|
console.log(chalk19.green(` \u2726 Joined federation network`));
|
|
8212
8694
|
console.log(chalk19.dim(` Topic: ${info.topic}`));
|
|
@@ -8654,7 +9136,7 @@ import chalk25 from "chalk";
|
|
|
8654
9136
|
// packages/core/dist/intelligence/draft-generator.js
|
|
8655
9137
|
init_wiki_compiler();
|
|
8656
9138
|
init_config();
|
|
8657
|
-
import { writeFileSync as
|
|
9139
|
+
import { writeFileSync as writeFileSync19, mkdirSync as mkdirSync19, existsSync as existsSync19 } from "node:fs";
|
|
8658
9140
|
import { join as join24, resolve as resolve16, basename as basename5, extname as extname7 } from "node:path";
|
|
8659
9141
|
function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
|
|
8660
9142
|
const { topic, format = "blog", maxSections = 8, blueprint } = options;
|
|
@@ -8738,7 +9220,7 @@ function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
|
|
|
8738
9220
|
const filename = `${timestamp}-${slug}.md`;
|
|
8739
9221
|
const filePath = join24("_drafts", filename);
|
|
8740
9222
|
const fullPath = resolve16(vaultPath, filePath);
|
|
8741
|
-
|
|
9223
|
+
writeFileSync19(fullPath, body, "utf-8");
|
|
8742
9224
|
return {
|
|
8743
9225
|
title: draftTitle,
|
|
8744
9226
|
filePath,
|
|
@@ -8934,7 +9416,7 @@ async function draftCommand(topic, options) {
|
|
|
8934
9416
|
}
|
|
8935
9417
|
}
|
|
8936
9418
|
async function enhanceWithAI(vaultPath, draftPath, topic, format) {
|
|
8937
|
-
const { readFileSync: readFileSync18, writeFileSync:
|
|
9419
|
+
const { readFileSync: readFileSync18, writeFileSync: writeFileSync22 } = await import("node:fs");
|
|
8938
9420
|
const { resolve: resolve22 } = await import("node:path");
|
|
8939
9421
|
const fullPath = resolve22(vaultPath, draftPath);
|
|
8940
9422
|
const scaffold = readFileSync18(fullPath, "utf-8");
|
|
@@ -8979,7 +9461,7 @@ ${aiContent.text}
|
|
|
8979
9461
|
---
|
|
8980
9462
|
*Generated by \`stellavault draft --ai\` using Claude API at ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
8981
9463
|
`;
|
|
8982
|
-
|
|
9464
|
+
writeFileSync22(fullPath, enhanced, "utf-8");
|
|
8983
9465
|
}
|
|
8984
9466
|
} catch (err) {
|
|
8985
9467
|
console.error(chalk25.yellow(` AI enhancement failed: ${err instanceof Error ? err.message : "unknown"}. Keeping rule-based draft.`));
|
|
@@ -8988,7 +9470,7 @@ ${aiContent.text}
|
|
|
8988
9470
|
|
|
8989
9471
|
// packages/cli/dist/commands/session-cmd.js
|
|
8990
9472
|
import chalk26 from "chalk";
|
|
8991
|
-
import { writeFileSync as
|
|
9473
|
+
import { writeFileSync as writeFileSync20, mkdirSync as mkdirSync20, existsSync as existsSync20, appendFileSync } from "node:fs";
|
|
8992
9474
|
import { resolve as resolve17, join as join25 } from "node:path";
|
|
8993
9475
|
async function sessionSaveCommand(options) {
|
|
8994
9476
|
const config = loadConfig();
|
|
@@ -9054,7 +9536,7 @@ async function sessionSaveCommand(options) {
|
|
|
9054
9536
|
`# Daily Log \u2014 ${dateStr}`,
|
|
9055
9537
|
""
|
|
9056
9538
|
].join("\n");
|
|
9057
|
-
|
|
9539
|
+
writeFileSync20(logFile, header + entry.join("\n"), "utf-8");
|
|
9058
9540
|
} else {
|
|
9059
9541
|
appendFileSync(logFile, entry.join("\n"), "utf-8");
|
|
9060
9542
|
}
|
|
@@ -9225,7 +9707,7 @@ async function lintCommand() {
|
|
|
9225
9707
|
|
|
9226
9708
|
// packages/cli/dist/commands/fleeting-cmd.js
|
|
9227
9709
|
import chalk30 from "chalk";
|
|
9228
|
-
import { writeFileSync as
|
|
9710
|
+
import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync21, existsSync as existsSync22 } from "node:fs";
|
|
9229
9711
|
import { join as join27, resolve as resolve19 } from "node:path";
|
|
9230
9712
|
async function fleetingCommand(text, options) {
|
|
9231
9713
|
if (!text || text.trim().length < 2) {
|
|
@@ -9259,7 +9741,7 @@ async function fleetingCommand(text, options) {
|
|
|
9259
9741
|
"---",
|
|
9260
9742
|
`*Captured via \`stellavault fleeting\` at ${now.toLocaleString("ko-KR")}*`
|
|
9261
9743
|
].join("\n");
|
|
9262
|
-
|
|
9744
|
+
writeFileSync21(filePath, content, "utf-8");
|
|
9263
9745
|
console.log(chalk30.green(`Captured: ${filename}`));
|
|
9264
9746
|
console.log(chalk30.dim(`Location: raw/${filename}`));
|
|
9265
9747
|
console.log(chalk30.dim("Run `stellavault compile` to process into wiki."));
|
|
@@ -9680,25 +10162,25 @@ if (nodeVersion < 20) {
|
|
|
9680
10162
|
process.exit(1);
|
|
9681
10163
|
}
|
|
9682
10164
|
var program = new Command();
|
|
9683
|
-
var SV_VERSION = true ? "0.7.
|
|
10165
|
+
var SV_VERSION = true ? "0.7.4" : "0.0.0-dev";
|
|
9684
10166
|
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");
|
|
9685
10167
|
program.command("init").description("Interactive setup wizard \u2014 get started in 3 minutes").action(initCommand);
|
|
9686
10168
|
program.command("doctor").description("Diagnose setup issues (config, vault, DB, model, Node version)").action(doctorCommand);
|
|
9687
|
-
program.command("index [vault-path]").description("Index your vault (vectorize all documents for search)").action(indexCommand);
|
|
10169
|
+
program.command("index [vault-path]").description("Index your vault (vectorize all documents for search)").option("--no-spinner", "Disable spinner (for CI / log redirection)").option("-v, --verbose", "Verbose progress logs instead of spinner").option("--log-skipped <file>", "Write skipped/failed file list as JSON").option("--profile-memory", "Log RSS/heap every ~100 embeddings (diagnoses memory leaks)").action((vaultPath, opts) => indexCommand(vaultPath, opts));
|
|
9688
10170
|
program.command("status").description("Show index status (document count, last indexed, DB size)").action(statusCommand);
|
|
9689
10171
|
program.command("search <query>").description("Search your knowledge base (hybrid BM25 + vector)").option("-l, --limit <n>", "Max results", "5").action(searchCommand);
|
|
9690
10172
|
program.command("ask <question>").description("Ask a question \u2014 search, compose answer, optionally save").option("-s, --save", "Save answer as a new note in your vault").option("-q, --quotes", "Show direct quotes from sources").action((question, opts) => askCommand(question, opts));
|
|
9691
10173
|
program.command("ingest <input>").description("Ingest any input (URL, file, text, PDF, YouTube) into your vault").option("-t, --tags <tags>", "Comma-separated tags").option("-s, --stage <stage>", "Note stage: fleeting, literature, permanent", "fleeting").option("--title <title>", "Override title").action((input, opts) => ingestCommand(input, opts));
|
|
9692
10174
|
program.command("clip <url>").description("Clip a web page or YouTube video into your vault").option("-f, --folder <path>", "Vault subfolder for clips", "06_Research/clips").action(clipCommand);
|
|
9693
10175
|
program.command("graph").description("Launch the 3D knowledge graph in your browser").action(graphCommand);
|
|
9694
|
-
program.command("serve").description("Start MCP server (for Claude Code / Claude Desktop)").action(serveCommand);
|
|
10176
|
+
program.command("serve").alias("mcp").description("Start MCP server (for Claude Code / Claude Desktop). Alias: mcp").action(serveCommand);
|
|
9695
10177
|
program.command("decay").description("Memory decay report \u2014 find notes you are forgetting").action(decayCommand);
|
|
9696
10178
|
program.command("brief").description("Daily knowledge briefing (decay + gaps + activity)").action(briefCommand);
|
|
9697
10179
|
program.command("digest").description("Weekly knowledge activity report").option("-d, --days <n>", "Period in days", "7").option("-v, --visual", "Save as .md with Mermaid charts for Obsidian").action(digestCommand);
|
|
9698
10180
|
program.command("gaps").description("Detect knowledge gaps (weak connections between clusters)").action(gapsCommand);
|
|
9699
10181
|
program.command("duplicates").description("Find duplicate or near-identical notes").option("-t, --threshold <n>", "Similarity threshold (0\u20131)", "0.88").action(duplicatesCommand);
|
|
9700
10182
|
program.command("contradictions").description("Detect contradicting statements across your notes").action(contradictionsCommand);
|
|
9701
|
-
program.command("review").description("Daily review \u2014 resurface fading notes for spaced repetition").option("-n, --count <n>", "Number of notes to review", "5").action(reviewCommand);
|
|
10183
|
+
program.command("review").description("Daily review \u2014 resurface fading notes for spaced repetition").option("-n, --count <n>", "Number of notes to review", "5").option("--json", "Output as JSON (non-interactive, for automation)").option("--seed <value>", "Deterministic seed for rotation (e.g. day-of-year)").option("--exclude <glob>", 'Exclude paths matching glob (e.g. "Templates/**")').option("--min-age <days>", "Only notes older than N days", "0").action(reviewCommand);
|
|
9702
10184
|
program.command("learn").description("AI learning path \u2014 personalized recommendations based on decay + gaps").action(learnCommand);
|
|
9703
10185
|
program.command("lint").description("Knowledge health check \u2014 gaps, duplicates, contradictions, stale notes").action(() => lintCommand());
|
|
9704
10186
|
program.command("draft [topic]").description("Generate a draft from your knowledge (blog/report/outline/instagram/thread/script)").option("--format <type>", "Output format: blog, report, outline, instagram, thread, script").option("--ai", "Use Claude API for AI-enhanced draft (requires ANTHROPIC_API_KEY)").option("--blueprint <spec>", 'Chapter structure: "Ch1:tag1,tag2; Ch2:tag3"').action((topic, opts) => draftCommand(topic, opts));
|