stellavault 0.7.2 → 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 +24 -2
- 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 +735 -220
- 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,12 +697,10 @@ async function buildGraphData(store, options = {}) {
|
|
|
645
697
|
]);
|
|
646
698
|
const edges = [];
|
|
647
699
|
const edgeCounts = /* @__PURE__ */ new Map();
|
|
700
|
+
const docsWithVecs = docs.filter((d) => embeddings.has(d.id));
|
|
648
701
|
const normalizedVecs = /* @__PURE__ */ new Map();
|
|
649
|
-
for (const doc of
|
|
650
|
-
|
|
651
|
-
if (!vec)
|
|
652
|
-
continue;
|
|
653
|
-
normalizedVecs.set(doc.id, normalizeVector([...vec]));
|
|
702
|
+
for (const doc of docsWithVecs) {
|
|
703
|
+
normalizedVecs.set(doc.id, normalizeVector([...embeddings.get(doc.id)]));
|
|
654
704
|
}
|
|
655
705
|
const docIds = [...normalizedVecs.keys()];
|
|
656
706
|
const vecArray = docIds.map((id) => normalizedVecs.get(id));
|
|
@@ -1255,36 +1305,34 @@ var init_wiki_compiler = __esm({
|
|
|
1255
1305
|
});
|
|
1256
1306
|
|
|
1257
1307
|
// packages/core/dist/federation/identity.js
|
|
1258
|
-
import {
|
|
1259
|
-
import {
|
|
1260
|
-
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";
|
|
1261
1310
|
import { homedir as homedir4 } from "node:os";
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1273
|
-
const secretKey = randomBytes(32);
|
|
1274
|
-
const publicKey = createHash3("sha256").update(secretKey).digest();
|
|
1275
|
-
const peerId = createHash3("sha256").update(publicKey).digest("hex").slice(0, 16);
|
|
1276
|
-
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 {
|
|
1277
1321
|
peerId,
|
|
1278
1322
|
publicKey,
|
|
1279
1323
|
secretKey,
|
|
1280
1324
|
displayName: displayName ?? `node-${peerId.slice(0, 6)}`,
|
|
1281
1325
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1282
1326
|
};
|
|
1327
|
+
}
|
|
1328
|
+
function persistIdentity(identity) {
|
|
1283
1329
|
mkdirSync9(IDENTITY_DIR, { recursive: true });
|
|
1284
1330
|
const content = JSON.stringify({
|
|
1331
|
+
version: IDENTITY_VERSION,
|
|
1332
|
+
algorithm: IDENTITY_ALGORITHM,
|
|
1285
1333
|
peerId: identity.peerId,
|
|
1286
|
-
publicKey: publicKey.toString("hex"),
|
|
1287
|
-
secretKey: secretKey.toString("hex"),
|
|
1334
|
+
publicKey: identity.publicKey.toString("hex"),
|
|
1335
|
+
secretKey: identity.secretKey.toString("hex"),
|
|
1288
1336
|
displayName: identity.displayName,
|
|
1289
1337
|
createdAt: identity.createdAt
|
|
1290
1338
|
}, null, 2);
|
|
@@ -1293,36 +1341,103 @@ function getOrCreateIdentity(displayName) {
|
|
|
1293
1341
|
chmodSync(IDENTITY_FILE, 384);
|
|
1294
1342
|
} catch {
|
|
1295
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);
|
|
1296
1364
|
return identity;
|
|
1297
1365
|
}
|
|
1298
|
-
|
|
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;
|
|
1299
1382
|
var init_identity = __esm({
|
|
1300
1383
|
"packages/core/dist/federation/identity.js"() {
|
|
1301
1384
|
"use strict";
|
|
1302
1385
|
IDENTITY_DIR = join8(homedir4(), ".stellavault", "federation");
|
|
1303
1386
|
IDENTITY_FILE = join8(IDENTITY_DIR, "identity.json");
|
|
1387
|
+
IDENTITY_VERSION = 2;
|
|
1388
|
+
IDENTITY_ALGORITHM = "ed25519";
|
|
1304
1389
|
}
|
|
1305
1390
|
});
|
|
1306
1391
|
|
|
1307
1392
|
// packages/core/dist/federation/node.js
|
|
1308
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
1393
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
|
|
1309
1394
|
import { EventEmitter } from "node:events";
|
|
1310
|
-
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;
|
|
1311
1396
|
var init_node = __esm({
|
|
1312
1397
|
"packages/core/dist/federation/node.js"() {
|
|
1313
1398
|
"use strict";
|
|
1314
1399
|
init_identity();
|
|
1315
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;
|
|
1316
1405
|
FederationNode = class extends EventEmitter {
|
|
1317
1406
|
swarm = null;
|
|
1318
1407
|
identity;
|
|
1319
1408
|
peers = /* @__PURE__ */ new Map();
|
|
1409
|
+
connStates = /* @__PURE__ */ new WeakMap();
|
|
1410
|
+
replayCache = /* @__PURE__ */ new Set();
|
|
1411
|
+
replayQueue = [];
|
|
1320
1412
|
running = false;
|
|
1321
1413
|
documentCount = 0;
|
|
1322
1414
|
topTopics = [];
|
|
1323
|
-
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) {
|
|
1324
1429
|
super();
|
|
1325
|
-
|
|
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
|
+
}
|
|
1326
1441
|
}
|
|
1327
1442
|
get peerId() {
|
|
1328
1443
|
return this.identity.peerId;
|
|
@@ -1340,7 +1455,6 @@ var init_node = __esm({
|
|
|
1340
1455
|
this.documentCount = documentCount;
|
|
1341
1456
|
this.topTopics = topTopics.slice(0, 5);
|
|
1342
1457
|
}
|
|
1343
|
-
// Design Ref: §4 — join()
|
|
1344
1458
|
async join() {
|
|
1345
1459
|
if (this.running)
|
|
1346
1460
|
return;
|
|
@@ -1354,7 +1468,6 @@ var init_node = __esm({
|
|
|
1354
1468
|
this.running = true;
|
|
1355
1469
|
this.emit("joined", { peerId: this.peerId, topic: FEDERATION_TOPIC.toString("hex").slice(0, 16) });
|
|
1356
1470
|
}
|
|
1357
|
-
// Design Ref: §4 — joinDirect() 수동 IP 폴백
|
|
1358
1471
|
async joinDirect(host, port) {
|
|
1359
1472
|
const net = await import("node:net");
|
|
1360
1473
|
const conn = net.connect(port, host);
|
|
@@ -1374,7 +1487,7 @@ var init_node = __esm({
|
|
|
1374
1487
|
return;
|
|
1375
1488
|
for (const [, peer] of this.peers) {
|
|
1376
1489
|
try {
|
|
1377
|
-
this.
|
|
1490
|
+
this.sendSigned(peer.conn, { type: "leave", peerId: this.peerId });
|
|
1378
1491
|
peer.conn.end();
|
|
1379
1492
|
} catch {
|
|
1380
1493
|
}
|
|
@@ -1388,30 +1501,109 @@ var init_node = __esm({
|
|
|
1388
1501
|
getPeers() {
|
|
1389
1502
|
return [...this.peers.values()].map((p) => p.info);
|
|
1390
1503
|
}
|
|
1391
|
-
// 피어에게 검색 쿼리 전송 (FederatedSearch에서 사용)
|
|
1392
1504
|
sendSearchQuery(peerId, queryId, embedding, limit) {
|
|
1393
1505
|
const peer = this.peers.get(peerId);
|
|
1394
1506
|
if (!peer)
|
|
1395
1507
|
return;
|
|
1396
|
-
this.
|
|
1508
|
+
this.sendSigned(peer.conn, { type: "search_query", queryId, embedding, limit });
|
|
1397
1509
|
}
|
|
1398
|
-
// 피어에게 검색 결과 응답 (FederatedSearch에서 사용)
|
|
1399
1510
|
sendSearchResult(peerId, queryId, results) {
|
|
1400
1511
|
const peer = this.peers.get(peerId);
|
|
1401
1512
|
if (!peer)
|
|
1402
1513
|
return;
|
|
1403
|
-
this.
|
|
1514
|
+
this.sendSigned(peer.conn, { type: "search_result", queryId, results });
|
|
1404
1515
|
}
|
|
1405
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
|
+
}
|
|
1406
1563
|
handleConnection(conn) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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;
|
|
1415
1607
|
let buffer = "";
|
|
1416
1608
|
const MAX_BUFFER = 1024 * 1024;
|
|
1417
1609
|
const MAX_MESSAGE = 64 * 1024;
|
|
@@ -1430,107 +1622,214 @@ var init_node = __esm({
|
|
|
1430
1622
|
continue;
|
|
1431
1623
|
if (line.length > MAX_MESSAGE)
|
|
1432
1624
|
continue;
|
|
1433
|
-
|
|
1434
|
-
const msg = JSON.parse(line);
|
|
1435
|
-
this.handleMessage(conn, msg);
|
|
1436
|
-
} catch {
|
|
1437
|
-
}
|
|
1625
|
+
this.dispatchLine(conn, line);
|
|
1438
1626
|
}
|
|
1439
1627
|
});
|
|
1440
1628
|
conn.on("close", () => {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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 });
|
|
1447
1636
|
}
|
|
1448
1637
|
});
|
|
1449
1638
|
conn.on("error", () => {
|
|
1450
1639
|
});
|
|
1451
1640
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
if (
|
|
1460
|
-
return
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
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 });
|
|
1464
1658
|
return false;
|
|
1659
|
+
}
|
|
1660
|
+
state.rlTokens -= 1;
|
|
1465
1661
|
return true;
|
|
1466
1662
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
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)
|
|
1469
1717
|
return;
|
|
1718
|
+
if (!verifySignature(publicKey, canonical, sigBuf))
|
|
1719
|
+
return;
|
|
1720
|
+
this.handleMessage(conn, state, envelope.payload, publicKey);
|
|
1721
|
+
}
|
|
1722
|
+
handleMessage(conn, state, msg, publicKey) {
|
|
1470
1723
|
switch (msg.type) {
|
|
1471
|
-
case "
|
|
1472
|
-
|
|
1473
|
-
|
|
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 = {
|
|
1474
1742
|
peerId: msg.peerId,
|
|
1475
|
-
displayName:
|
|
1743
|
+
displayName: (msg.displayName ?? "").slice(0, 50),
|
|
1476
1744
|
documentCount: Math.min(msg.documentCount ?? 0, 1e6),
|
|
1477
|
-
// 합리적 상한
|
|
1478
1745
|
topTopics: (msg.topTopics ?? []).slice(0, 10),
|
|
1479
1746
|
joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1480
1747
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1481
1748
|
};
|
|
1482
|
-
this.
|
|
1483
|
-
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);
|
|
1484
1775
|
break;
|
|
1485
1776
|
}
|
|
1486
1777
|
case "search_query": {
|
|
1778
|
+
if (!state.ready)
|
|
1779
|
+
return;
|
|
1487
1780
|
this.emit("search_request", {
|
|
1488
|
-
peerId:
|
|
1489
|
-
// queryId를 추적용으로 사용
|
|
1781
|
+
peerId: state.peerId,
|
|
1490
1782
|
queryId: msg.queryId,
|
|
1491
1783
|
embedding: msg.embedding,
|
|
1492
1784
|
limit: msg.limit,
|
|
1493
|
-
|
|
1494
|
-
respondTo: (() => {
|
|
1495
|
-
for (const [pid, peer] of this.peers) {
|
|
1496
|
-
if (peer.conn === conn)
|
|
1497
|
-
return pid;
|
|
1498
|
-
}
|
|
1499
|
-
return null;
|
|
1500
|
-
})()
|
|
1785
|
+
respondTo: state.peerId
|
|
1501
1786
|
});
|
|
1502
1787
|
break;
|
|
1503
1788
|
}
|
|
1504
1789
|
case "search_result": {
|
|
1790
|
+
if (!state.ready)
|
|
1791
|
+
return;
|
|
1505
1792
|
this.emit("search_response", {
|
|
1506
1793
|
queryId: msg.queryId,
|
|
1507
1794
|
results: msg.results,
|
|
1508
|
-
peerId:
|
|
1509
|
-
for (const [pid, peer] of this.peers) {
|
|
1510
|
-
if (peer.conn === conn)
|
|
1511
|
-
return pid;
|
|
1512
|
-
}
|
|
1513
|
-
return "unknown";
|
|
1514
|
-
})()
|
|
1795
|
+
peerId: state.peerId ?? "unknown"
|
|
1515
1796
|
});
|
|
1516
1797
|
break;
|
|
1517
1798
|
}
|
|
1518
1799
|
case "leave": {
|
|
1519
|
-
|
|
1520
|
-
|
|
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) {
|
|
1521
1806
|
this.peers.delete(msg.peerId);
|
|
1522
1807
|
this.emit("peer_left", { peerId: msg.peerId });
|
|
1523
1808
|
}
|
|
1524
1809
|
break;
|
|
1525
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;
|
|
1526
1817
|
}
|
|
1527
1818
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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;
|
|
1533
1830
|
}
|
|
1831
|
+
this.peers.set(state.peerId, { info: state.pendingPeerInfo, conn });
|
|
1832
|
+
this.emit("peer_joined", state.pendingPeerInfo);
|
|
1534
1833
|
}
|
|
1535
1834
|
};
|
|
1536
1835
|
}
|
|
@@ -1710,10 +2009,10 @@ var init_sharing = __esm({
|
|
|
1710
2009
|
/\b\d{6}[-]\d{7}\b/
|
|
1711
2010
|
];
|
|
1712
2011
|
DEFAULT_CONFIG2 = {
|
|
1713
|
-
defaultLevel:
|
|
1714
|
-
// 기본:
|
|
1715
|
-
myNodeLevel:
|
|
1716
|
-
// 내 노드 기본:
|
|
2012
|
+
defaultLevel: 1,
|
|
2013
|
+
// 기본: 제목+유사도까지만 (스니펫 안 보냄)
|
|
2014
|
+
myNodeLevel: 0,
|
|
2015
|
+
// 내 노드 기본: 수신 전용 (명시적 set-level 필요)
|
|
1717
2016
|
rules: [
|
|
1718
2017
|
// 기본 규칙
|
|
1719
2018
|
{ pattern: "public", type: "tag", level: 4 },
|
|
@@ -1800,6 +2099,11 @@ var init_search = __esm({
|
|
|
1800
2099
|
this.node.on("search_request", async (req) => {
|
|
1801
2100
|
if (!req.respondTo)
|
|
1802
2101
|
return;
|
|
2102
|
+
const cfg = loadSharingConfig();
|
|
2103
|
+
if (cfg.myNodeLevel === 0) {
|
|
2104
|
+
this.node.sendSearchResult(req.respondTo, req.queryId, []);
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
1803
2107
|
try {
|
|
1804
2108
|
const allStores = [this.store, ...this.additionalStores];
|
|
1805
2109
|
const allScored = await Promise.all(allStores.map((s) => s.searchSemantic(req.embedding, req.limit).catch(() => [])));
|
|
@@ -1835,8 +2139,13 @@ var federation_exports = {};
|
|
|
1835
2139
|
__export(federation_exports, {
|
|
1836
2140
|
FederatedSearch: () => FederatedSearch,
|
|
1837
2141
|
FederationNode: () => FederationNode,
|
|
1838
|
-
getOrCreateIdentity: () => getOrCreateIdentity
|
|
2142
|
+
getOrCreateIdentity: () => getOrCreateIdentity,
|
|
2143
|
+
isFederationExperimentalEnabled: () => isFederationExperimentalEnabled
|
|
1839
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
|
+
}
|
|
1840
2149
|
var init_federation = __esm({
|
|
1841
2150
|
"packages/core/dist/federation/index.js"() {
|
|
1842
2151
|
"use strict";
|
|
@@ -3099,7 +3408,8 @@ import { Command } from "commander";
|
|
|
3099
3408
|
// packages/cli/dist/commands/index-cmd.js
|
|
3100
3409
|
import ora from "ora";
|
|
3101
3410
|
import chalk from "chalk";
|
|
3102
|
-
import { createHash as
|
|
3411
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
3412
|
+
import { writeFileSync as writeFileSync15 } from "node:fs";
|
|
3103
3413
|
import { join as join20 } from "node:path";
|
|
3104
3414
|
import { homedir as homedir12 } from "node:os";
|
|
3105
3415
|
import { mkdirSync as mkdirSync15 } from "node:fs";
|
|
@@ -3170,9 +3480,8 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
|
|
|
3170
3480
|
const rows = db.prepare(`
|
|
3171
3481
|
SELECT chunk_id, distance
|
|
3172
3482
|
FROM chunk_embeddings
|
|
3173
|
-
WHERE embedding MATCH ?
|
|
3483
|
+
WHERE embedding MATCH ? AND k = ?
|
|
3174
3484
|
ORDER BY distance
|
|
3175
|
-
LIMIT ?
|
|
3176
3485
|
`).all(float32Buffer(embedding), limit);
|
|
3177
3486
|
return rows.map((r) => ({
|
|
3178
3487
|
chunkId: r.chunk_id,
|
|
@@ -3258,6 +3567,22 @@ function createSqliteVecStore(dbPath, dimensions = 384) {
|
|
|
3258
3567
|
}
|
|
3259
3568
|
return result;
|
|
3260
3569
|
},
|
|
3570
|
+
async findDocumentNeighbors(embedding, limit) {
|
|
3571
|
+
const knnK = Math.max(limit * 3, 30);
|
|
3572
|
+
const rows = db.prepare(`
|
|
3573
|
+
SELECT c.document_id, MIN(ce.distance) as distance
|
|
3574
|
+
FROM chunk_embeddings ce
|
|
3575
|
+
JOIN chunks c ON c.id = ce.chunk_id
|
|
3576
|
+
WHERE ce.embedding MATCH ? AND k = ?
|
|
3577
|
+
GROUP BY c.document_id
|
|
3578
|
+
ORDER BY distance
|
|
3579
|
+
LIMIT ?
|
|
3580
|
+
`).all(float32Buffer(embedding), knnK, limit * 2);
|
|
3581
|
+
return rows.slice(0, limit).map((r) => ({
|
|
3582
|
+
documentId: r.document_id,
|
|
3583
|
+
similarity: 1 / (1 + r.distance)
|
|
3584
|
+
}));
|
|
3585
|
+
},
|
|
3261
3586
|
async close() {
|
|
3262
3587
|
db.close();
|
|
3263
3588
|
},
|
|
@@ -4599,7 +4924,7 @@ Please write the ${format} draft based on the context above. Save the result to
|
|
|
4599
4924
|
|
|
4600
4925
|
// packages/core/dist/mcp/tools/agentic-graph.js
|
|
4601
4926
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "node:fs";
|
|
4602
|
-
import { join as join7 } from "node:path";
|
|
4927
|
+
import { join as join7, resolve as resolvePath } from "node:path";
|
|
4603
4928
|
function createAgenticGraphTools(store, embedder, vaultPath) {
|
|
4604
4929
|
let nodeCreationCount = 0;
|
|
4605
4930
|
let nodeCreationWindowStart = Date.now();
|
|
@@ -4672,8 +4997,8 @@ ${content}${relatedSection}`;
|
|
|
4672
4997
|
const safeTitle = title.replace(/[<>:"/\\|?*]/g, "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
4673
4998
|
const dir = join7(vaultPath, folder);
|
|
4674
4999
|
const filePath = join7(dir, `${safeTitle}.md`);
|
|
4675
|
-
const resolvedPath =
|
|
4676
|
-
const resolvedVault =
|
|
5000
|
+
const resolvedPath = resolvePath(filePath);
|
|
5001
|
+
const resolvedVault = resolvePath(vaultPath);
|
|
4677
5002
|
if (!resolvedPath.startsWith(resolvedVault)) {
|
|
4678
5003
|
return { content: [{ type: "text", text: "Error: invalid folder path." }] };
|
|
4679
5004
|
}
|
|
@@ -4739,6 +5064,7 @@ The note will appear in the graph after next index.`
|
|
|
4739
5064
|
// packages/core/dist/mcp/server.js
|
|
4740
5065
|
function createMcpServer(options) {
|
|
4741
5066
|
const { store, searchEngine, embedder, vaultPath = "", decayEngine } = options;
|
|
5067
|
+
const ready = options.ready ?? Promise.resolve();
|
|
4742
5068
|
const learningPathTool = createLearningPathTool(store);
|
|
4743
5069
|
const detectGapsTool = createDetectGapsTool(store);
|
|
4744
5070
|
const getEvolutionTool = createGetEvolutionTool(store);
|
|
@@ -4746,7 +5072,7 @@ function createMcpServer(options) {
|
|
|
4746
5072
|
const askTool = createAskTool(searchEngine, vaultPath);
|
|
4747
5073
|
const generateDraftTool = createGenerateDraftTool(searchEngine, vaultPath);
|
|
4748
5074
|
const agenticTools = embedder ? createAgenticGraphTools(store, embedder, vaultPath) : [];
|
|
4749
|
-
const server = new Server({ name: "stellavault", version: "0.7.
|
|
5075
|
+
const server = new Server({ name: "stellavault", version: "0.7.3" }, { capabilities: { tools: {} } });
|
|
4750
5076
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
4751
5077
|
tools: [
|
|
4752
5078
|
searchToolDef,
|
|
@@ -4770,6 +5096,7 @@ function createMcpServer(options) {
|
|
|
4770
5096
|
]
|
|
4771
5097
|
}));
|
|
4772
5098
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
5099
|
+
await ready;
|
|
4773
5100
|
const { name, arguments: args } = request.params;
|
|
4774
5101
|
try {
|
|
4775
5102
|
let result;
|
|
@@ -4873,8 +5200,18 @@ function createMcpServer(options) {
|
|
|
4873
5200
|
const { createServer } = await import("node:http");
|
|
4874
5201
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => `sv-${Date.now()}` });
|
|
4875
5202
|
await server.connect(transport);
|
|
5203
|
+
const allowedOrigins = options.corsOrigins ?? ["http://localhost", "http://127.0.0.1"];
|
|
5204
|
+
const allowWildcard = allowedOrigins.includes("*");
|
|
4876
5205
|
const httpServer = createServer(async (req, res) => {
|
|
4877
|
-
|
|
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
|
+
}
|
|
4878
5215
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4879
5216
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
4880
5217
|
if (req.method === "OPTIONS") {
|
|
@@ -5087,11 +5424,11 @@ Author: ${pack2.author}`,
|
|
|
5087
5424
|
init_graph_data();
|
|
5088
5425
|
import express from "express";
|
|
5089
5426
|
import cors from "cors";
|
|
5090
|
-
import { randomBytes as
|
|
5427
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
5091
5428
|
|
|
5092
5429
|
// packages/core/dist/api/routes/federation.js
|
|
5093
5430
|
import { Router } from "express";
|
|
5094
|
-
function createFederationRouter(store) {
|
|
5431
|
+
function createFederationRouter(store, requireAuth) {
|
|
5095
5432
|
const router = Router();
|
|
5096
5433
|
let federationNode = null;
|
|
5097
5434
|
let federationAvailable = null;
|
|
@@ -5134,7 +5471,15 @@ function createFederationRouter(store) {
|
|
|
5134
5471
|
res.json({ available: true, active: false, peerCount: 0, peers: [], displayName: null, peerId: null });
|
|
5135
5472
|
}
|
|
5136
5473
|
});
|
|
5137
|
-
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
|
+
}
|
|
5138
5483
|
const available = await probeFederationAvailable();
|
|
5139
5484
|
if (!available) {
|
|
5140
5485
|
return res.status(501).json({
|
|
@@ -5176,7 +5521,7 @@ function createFederationRouter(store) {
|
|
|
5176
5521
|
res.status(500).json({ success: false, error: err instanceof Error ? err.message : "Federation join failed" });
|
|
5177
5522
|
}
|
|
5178
5523
|
});
|
|
5179
|
-
router.post("/leave", async (_req, res) => {
|
|
5524
|
+
router.post("/leave", requireAuth, async (_req, res) => {
|
|
5180
5525
|
if (!federationNode || !federationNode.isRunning) {
|
|
5181
5526
|
return res.json({ success: true, active: false, message: "Not active" });
|
|
5182
5527
|
}
|
|
@@ -5231,7 +5576,7 @@ function createKnowledgeRouter(opts) {
|
|
|
5231
5576
|
res.status(404).json({ error: "Document not found" });
|
|
5232
5577
|
return;
|
|
5233
5578
|
}
|
|
5234
|
-
const { readFileSync: readFileSync18, writeFileSync:
|
|
5579
|
+
const { readFileSync: readFileSync18, writeFileSync: writeFileSync22, unlinkSync } = await import("node:fs");
|
|
5235
5580
|
const { join: join30, resolve: resolve22 } = await import("node:path");
|
|
5236
5581
|
const [keeper, removed] = docA.content.length >= docB.content.length ? [docA, docB] : [docB, docA];
|
|
5237
5582
|
const keeperPath = resolve22(join30(vaultPath, keeper.filePath));
|
|
@@ -5249,7 +5594,7 @@ function createKnowledgeRouter(opts) {
|
|
|
5249
5594
|
> Merged from: ${removed.title} (${removed.filePath})
|
|
5250
5595
|
|
|
5251
5596
|
${removed.content}`;
|
|
5252
|
-
|
|
5597
|
+
writeFileSync22(keeperPath, keeperContent + appendix, "utf-8");
|
|
5253
5598
|
try {
|
|
5254
5599
|
unlinkSync(removedPath);
|
|
5255
5600
|
} catch {
|
|
@@ -5272,7 +5617,7 @@ ${removed.content}`;
|
|
|
5272
5617
|
res.status(400).json({ error: "clusterA, clusterB required" });
|
|
5273
5618
|
return;
|
|
5274
5619
|
}
|
|
5275
|
-
const { writeFileSync:
|
|
5620
|
+
const { writeFileSync: writeFileSync22, mkdirSync: mkdirSync22 } = await import("node:fs");
|
|
5276
5621
|
const { join: join30, resolve: resolve22 } = await import("node:path");
|
|
5277
5622
|
const nameA = clusterA.replace(/\s*\(\d+\)$/, "");
|
|
5278
5623
|
const nameB = clusterB.replace(/\s*\(\d+\)$/, "");
|
|
@@ -5320,7 +5665,7 @@ ${removed.content}`;
|
|
|
5320
5665
|
}
|
|
5321
5666
|
mkdirSync22(dir, { recursive: true });
|
|
5322
5667
|
const filePath = join30(dir, `${safeTitle}.md`);
|
|
5323
|
-
|
|
5668
|
+
writeFileSync22(filePath, content, "utf-8");
|
|
5324
5669
|
res.json({ success: true, title: safeTitle, path: filePath });
|
|
5325
5670
|
} catch (err) {
|
|
5326
5671
|
console.error(err);
|
|
@@ -5332,6 +5677,7 @@ ${removed.content}`;
|
|
|
5332
5677
|
|
|
5333
5678
|
// packages/core/dist/api/routes/ingest.js
|
|
5334
5679
|
import { Router as Router3 } from "express";
|
|
5680
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
5335
5681
|
function createIngestRouter(opts) {
|
|
5336
5682
|
const { store, vaultPath, requireAuth, assertNotPrivateUrl } = opts;
|
|
5337
5683
|
const router = Router3();
|
|
@@ -5402,7 +5748,7 @@ function createIngestRouter(opts) {
|
|
|
5402
5748
|
});
|
|
5403
5749
|
try {
|
|
5404
5750
|
const doc = {
|
|
5405
|
-
id:
|
|
5751
|
+
id: createHash5("sha256").update(result.savedTo).digest("hex").slice(0, 16),
|
|
5406
5752
|
filePath: result.savedTo,
|
|
5407
5753
|
title: result.title,
|
|
5408
5754
|
content,
|
|
@@ -5469,12 +5815,12 @@ function createIngestRouter(opts) {
|
|
|
5469
5815
|
return;
|
|
5470
5816
|
}
|
|
5471
5817
|
try {
|
|
5472
|
-
const { writeFileSync:
|
|
5818
|
+
const { writeFileSync: writeFileSync22, unlinkSync } = await import("node:fs");
|
|
5473
5819
|
const { join: join30 } = await import("node:path");
|
|
5474
5820
|
const { tmpdir } = await import("node:os");
|
|
5475
5821
|
const safeName = (file.originalname ?? "upload").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 100);
|
|
5476
5822
|
const tmpPath = join30(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
|
|
5477
|
-
|
|
5823
|
+
writeFileSync22(tmpPath, file.buffer);
|
|
5478
5824
|
const { extractFileContent: extractFileContent2 } = await Promise.resolve().then(() => (init_file_extractors(), file_extractors_exports));
|
|
5479
5825
|
const ext = file.originalname.split(".").pop()?.toLowerCase() ?? "";
|
|
5480
5826
|
const binaryExts = /* @__PURE__ */ new Set(["pdf", "docx", "pptx", "xlsx", "xls"]);
|
|
@@ -5510,7 +5856,7 @@ function createIngestRouter(opts) {
|
|
|
5510
5856
|
});
|
|
5511
5857
|
try {
|
|
5512
5858
|
const doc = {
|
|
5513
|
-
id:
|
|
5859
|
+
id: createHash5("sha256").update(result.savedTo).digest("hex").slice(0, 16),
|
|
5514
5860
|
filePath: result.savedTo,
|
|
5515
5861
|
title: result.title,
|
|
5516
5862
|
content,
|
|
@@ -5580,7 +5926,7 @@ ${desc}
|
|
|
5580
5926
|
if (content.length > 1e4)
|
|
5581
5927
|
content = content.slice(0, 1e4) + "\n\n...(truncated)";
|
|
5582
5928
|
}
|
|
5583
|
-
const { writeFileSync:
|
|
5929
|
+
const { writeFileSync: writeFileSync22, mkdirSync: mkdirSync22 } = await import("node:fs");
|
|
5584
5930
|
const { join: join30 } = await import("node:path");
|
|
5585
5931
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5586
5932
|
const clipDir = join30(vaultPath || ".", "06_Research", "clips");
|
|
@@ -5598,7 +5944,7 @@ tags: [clip${isYT ? ", youtube" : ""}]
|
|
|
5598
5944
|
> Source: ${url}
|
|
5599
5945
|
|
|
5600
5946
|
${content}`;
|
|
5601
|
-
|
|
5947
|
+
writeFileSync22(join30(clipDir, fileName), md, "utf-8");
|
|
5602
5948
|
res.json({ success: true, fileName, path: join30(clipDir, fileName) });
|
|
5603
5949
|
} catch (err) {
|
|
5604
5950
|
console.error(err);
|
|
@@ -5856,7 +6202,7 @@ function createAnalyticsRouter(opts) {
|
|
|
5856
6202
|
function createApiServer(options) {
|
|
5857
6203
|
const { store, searchEngine, port = 3333, vaultName = "", vaultPath = "", decayEngine, graphUiPath } = options;
|
|
5858
6204
|
const app = express();
|
|
5859
|
-
const authToken =
|
|
6205
|
+
const authToken = randomBytes3(32).toString("hex");
|
|
5860
6206
|
app.use((_req, res, next) => {
|
|
5861
6207
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
5862
6208
|
res.setHeader("X-Frame-Options", "DENY");
|
|
@@ -5897,11 +6243,14 @@ function createApiServer(options) {
|
|
|
5897
6243
|
const token = req.headers["x-stellavault-token"];
|
|
5898
6244
|
if (token === authToken)
|
|
5899
6245
|
return next();
|
|
5900
|
-
|
|
5901
|
-
return next();
|
|
5902
|
-
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." });
|
|
5903
6247
|
}
|
|
5904
|
-
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
|
+
}
|
|
5905
6254
|
res.json({ token: authToken });
|
|
5906
6255
|
});
|
|
5907
6256
|
function assertNotPrivateUrl(url) {
|
|
@@ -6085,7 +6434,7 @@ function createApiServer(options) {
|
|
|
6085
6434
|
return;
|
|
6086
6435
|
}
|
|
6087
6436
|
const { resolve: resolve22, join: join30 } = await import("node:path");
|
|
6088
|
-
const { writeFileSync:
|
|
6437
|
+
const { writeFileSync: writeFileSync22, readFileSync: readFileSync18 } = await import("node:fs");
|
|
6089
6438
|
const fullPath = resolve22(vaultPath, doc.filePath);
|
|
6090
6439
|
if (!fullPath.startsWith(resolve22(vaultPath))) {
|
|
6091
6440
|
res.status(403).json({ error: "Access denied" });
|
|
@@ -6112,7 +6461,7 @@ function createApiServer(options) {
|
|
|
6112
6461
|
updated = content;
|
|
6113
6462
|
}
|
|
6114
6463
|
}
|
|
6115
|
-
|
|
6464
|
+
writeFileSync22(fullPath, updated, "utf-8");
|
|
6116
6465
|
await store.upsertDocument({
|
|
6117
6466
|
...doc,
|
|
6118
6467
|
title: title ?? doc.title,
|
|
@@ -6301,7 +6650,7 @@ function createApiServer(options) {
|
|
|
6301
6650
|
app.get("/api/sync/status", (_req, res) => {
|
|
6302
6651
|
res.json(syncState);
|
|
6303
6652
|
});
|
|
6304
|
-
app.use("/api/federate", createFederationRouter(store));
|
|
6653
|
+
app.use("/api/federate", createFederationRouter(store, requireAuth));
|
|
6305
6654
|
if (graphUiPath) {
|
|
6306
6655
|
app.get(/^(?!\/api\/)(?!.*\.[a-z0-9]+$).*$/i, (_req, res) => {
|
|
6307
6656
|
res.sendFile(`${graphUiPath}/index.html`);
|
|
@@ -6749,7 +7098,7 @@ async function captureVoice(audioPath, options) {
|
|
|
6749
7098
|
}
|
|
6750
7099
|
|
|
6751
7100
|
// packages/core/dist/cloud/sync.js
|
|
6752
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
7101
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, createHash as createHash6 } from "node:crypto";
|
|
6753
7102
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync14, chmodSync as chmodSync2 } from "node:fs";
|
|
6754
7103
|
import { join as join15 } from "node:path";
|
|
6755
7104
|
import { homedir as homedir7 } from "node:os";
|
|
@@ -6757,7 +7106,7 @@ var CLOUD_DIR = join15(homedir7(), ".stellavault", "cloud");
|
|
|
6757
7106
|
var KEY_FILE = join15(CLOUD_DIR, "encryption.key");
|
|
6758
7107
|
var SYNC_STATE_FILE = join15(CLOUD_DIR, "sync-state.json");
|
|
6759
7108
|
function encrypt(data, key) {
|
|
6760
|
-
const iv =
|
|
7109
|
+
const iv = randomBytes4(16);
|
|
6761
7110
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
6762
7111
|
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
6763
7112
|
const tag = cipher.getAuthTag();
|
|
@@ -6771,14 +7120,14 @@ function decrypt(encrypted, key, iv, tag) {
|
|
|
6771
7120
|
function getOrCreateEncryptionKey(userKey) {
|
|
6772
7121
|
mkdirSync14(CLOUD_DIR, { recursive: true });
|
|
6773
7122
|
if (userKey) {
|
|
6774
|
-
const key2 =
|
|
7123
|
+
const key2 = createHash6("sha256").update(userKey).digest();
|
|
6775
7124
|
writeFileSync14(KEY_FILE, key2.toString("hex"), "utf-8");
|
|
6776
7125
|
return key2;
|
|
6777
7126
|
}
|
|
6778
7127
|
if (existsSync14(KEY_FILE)) {
|
|
6779
7128
|
return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
|
|
6780
7129
|
}
|
|
6781
|
-
const key =
|
|
7130
|
+
const key = randomBytes4(32);
|
|
6782
7131
|
writeFileSync14(KEY_FILE, key.toString("hex"), { encoding: "utf-8", mode: 384 });
|
|
6783
7132
|
try {
|
|
6784
7133
|
chmodSync2(KEY_FILE, 384);
|
|
@@ -6805,7 +7154,7 @@ async function s3Put(config, objectKey, data, contentType = "application/octet-s
|
|
|
6805
7154
|
"Content-Type": contentType,
|
|
6806
7155
|
"Content-Length": String(data.length),
|
|
6807
7156
|
"x-amz-date": date,
|
|
6808
|
-
"x-amz-content-sha256":
|
|
7157
|
+
"x-amz-content-sha256": createHash6("sha256").update(data).digest("hex"),
|
|
6809
7158
|
// R2는 Bearer token 지원
|
|
6810
7159
|
"Authorization": `Bearer ${secretAccessKey}`
|
|
6811
7160
|
},
|
|
@@ -6902,24 +7251,27 @@ var CREDITS_FILE = join19(homedir11(), ".stellavault", "federation", "credits.js
|
|
|
6902
7251
|
|
|
6903
7252
|
// packages/core/dist/index.js
|
|
6904
7253
|
init_retry();
|
|
7254
|
+
init_math();
|
|
6905
7255
|
init_indexer();
|
|
6906
|
-
function createKnowledgeHub(config) {
|
|
7256
|
+
function createKnowledgeHub(config, options = {}) {
|
|
6907
7257
|
const embedder = createLocalEmbedder(config.embedding.localModel);
|
|
6908
7258
|
const dims = embedder.dimensions;
|
|
6909
7259
|
const store = createSqliteVecStore(config.dbPath, dims);
|
|
6910
7260
|
const searchEngine = createSearchEngine({ store, embedder, rrfK: config.search.rrfK });
|
|
6911
|
-
const mcpServer = createMcpServer({ store, searchEngine, vaultPath: config.vaultPath });
|
|
7261
|
+
const mcpServer = createMcpServer({ store, searchEngine, vaultPath: config.vaultPath, ready: options.ready });
|
|
6912
7262
|
return { store, embedder, searchEngine, mcpServer, config };
|
|
6913
7263
|
}
|
|
6914
7264
|
|
|
6915
7265
|
// packages/cli/dist/commands/index-cmd.js
|
|
6916
7266
|
function getVaultDbPath(vaultPath) {
|
|
6917
|
-
const hash =
|
|
7267
|
+
const hash = createHash7("sha256").update(vaultPath).digest("hex").slice(0, 8);
|
|
6918
7268
|
const dir = join20(homedir12(), ".stellavault", "vaults");
|
|
6919
7269
|
mkdirSync15(dir, { recursive: true });
|
|
6920
7270
|
return join20(dir, `${hash}.db`);
|
|
6921
7271
|
}
|
|
6922
|
-
async function indexCommand(vaultPath) {
|
|
7272
|
+
async function indexCommand(vaultPath, opts = {}) {
|
|
7273
|
+
if (opts.profileMemory)
|
|
7274
|
+
process.env.STELLAVAULT_PROFILE_MEMORY = "1";
|
|
6923
7275
|
const config = loadConfig();
|
|
6924
7276
|
const vault2 = vaultPath ?? config.vaultPath;
|
|
6925
7277
|
if (!vault2) {
|
|
@@ -6936,28 +7288,85 @@ async function indexCommand(vaultPath) {
|
|
|
6936
7288
|
} catch {
|
|
6937
7289
|
}
|
|
6938
7290
|
}
|
|
6939
|
-
const
|
|
7291
|
+
const spinnerEnabled = !opts.noSpinner && !opts.verbose && process.stderr.isTTY;
|
|
7292
|
+
const spinner = ora({ text: "Initializing...", isEnabled: spinnerEnabled }).start();
|
|
6940
7293
|
const store = createSqliteVecStore(dbPath);
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
spinner.text = "Starting indexing...";
|
|
6946
|
-
const result = await indexVault(vault2, {
|
|
6947
|
-
store,
|
|
6948
|
-
embedder,
|
|
6949
|
-
chunkOptions: config.chunking,
|
|
6950
|
-
onProgress(current, total, doc) {
|
|
6951
|
-
spinner.text = `[${current}/${total}] ${doc.title}`;
|
|
7294
|
+
const cleanupSpinner = () => {
|
|
7295
|
+
try {
|
|
7296
|
+
spinner.stop();
|
|
7297
|
+
} catch {
|
|
6952
7298
|
}
|
|
7299
|
+
};
|
|
7300
|
+
process.once("uncaughtException", cleanupSpinner);
|
|
7301
|
+
process.once("SIGINT", () => {
|
|
7302
|
+
cleanupSpinner();
|
|
7303
|
+
process.exit(130);
|
|
6953
7304
|
});
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
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
|
+
}
|
|
6961
7370
|
}
|
|
6962
7371
|
|
|
6963
7372
|
// packages/cli/dist/commands/search-cmd.js
|
|
@@ -7005,14 +7414,29 @@ async function searchCommand(query, options, cmd) {
|
|
|
7005
7414
|
import chalk3 from "chalk";
|
|
7006
7415
|
async function serveCommand() {
|
|
7007
7416
|
const config = loadConfig();
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
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"));
|
|
7014
7423
|
console.error(chalk3.dim("\u{1F4A1} Claude Code: claude mcp add stellavault -- stellavault serve"));
|
|
7015
|
-
|
|
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;
|
|
7016
7440
|
}
|
|
7017
7441
|
|
|
7018
7442
|
// packages/cli/dist/commands/status-cmd.js
|
|
@@ -7026,14 +7450,34 @@ async function statusCommand(_opts, cmd) {
|
|
|
7026
7450
|
const stats = await store.getStats();
|
|
7027
7451
|
const topics = await store.getTopics();
|
|
7028
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
|
+
}
|
|
7029
7463
|
if (jsonMode) {
|
|
7030
|
-
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));
|
|
7031
7472
|
return;
|
|
7032
7473
|
}
|
|
7033
7474
|
console.log("");
|
|
7034
7475
|
console.log(chalk4.bold("\u{1F4CA} Stellavault Status"));
|
|
7035
7476
|
console.log("\u2500".repeat(40));
|
|
7036
|
-
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
|
+
}
|
|
7037
7481
|
console.log(` \u{1F9E9} Chunks: ${stats.chunkCount}`);
|
|
7038
7482
|
console.log(` \u{1F550} Last indexed: ${stats.lastIndexed ?? "never"}`);
|
|
7039
7483
|
console.log(` \u{1F4BE} DB: ${config.dbPath}`);
|
|
@@ -7153,7 +7597,7 @@ async function openBrowser(url) {
|
|
|
7153
7597
|
|
|
7154
7598
|
// packages/cli/dist/commands/card-cmd.js
|
|
7155
7599
|
import chalk6 from "chalk";
|
|
7156
|
-
import { writeFileSync as
|
|
7600
|
+
import { writeFileSync as writeFileSync16 } from "node:fs";
|
|
7157
7601
|
import { resolve as resolve12 } from "node:path";
|
|
7158
7602
|
async function cardCommand(options) {
|
|
7159
7603
|
const output = options.output ?? "knowledge-card.svg";
|
|
@@ -7164,7 +7608,7 @@ async function cardCommand(options) {
|
|
|
7164
7608
|
if (!res.ok)
|
|
7165
7609
|
throw new Error(`API error: ${res.status}. Is 'stellavault graph' running?`);
|
|
7166
7610
|
const svg = await res.text();
|
|
7167
|
-
|
|
7611
|
+
writeFileSync16(outPath, svg, "utf-8");
|
|
7168
7612
|
console.error(chalk6.green(`\u2705 Profile card saved: ${outPath}`));
|
|
7169
7613
|
console.error(chalk6.dim(" Embed in GitHub README:"));
|
|
7170
7614
|
console.error(chalk6.dim(` `));
|
|
@@ -7215,8 +7659,8 @@ async function packExportCommand(name, options) {
|
|
|
7215
7659
|
}
|
|
7216
7660
|
const outPath = resolve13(process.cwd(), options.output ?? `${name}.sv-pack`);
|
|
7217
7661
|
const content = readFileSync15(srcPath, "utf-8");
|
|
7218
|
-
const { writeFileSync:
|
|
7219
|
-
|
|
7662
|
+
const { writeFileSync: writeFileSync22 } = await import("node:fs");
|
|
7663
|
+
writeFileSync22(outPath, content);
|
|
7220
7664
|
console.error(chalk7.green(`\u2705 Exported: ${outPath}`));
|
|
7221
7665
|
}
|
|
7222
7666
|
async function packImportCommand(filePath) {
|
|
@@ -7373,11 +7817,29 @@ function runScript(scriptPath, cwd) {
|
|
|
7373
7817
|
// packages/cli/dist/commands/review-cmd.js
|
|
7374
7818
|
import chalk10 from "chalk";
|
|
7375
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
|
+
}
|
|
7376
7835
|
async function reviewCommand(options) {
|
|
7377
7836
|
const config = loadConfig();
|
|
7378
7837
|
const hub = createKnowledgeHub(config);
|
|
7379
7838
|
const count = parseInt(options.count ?? "5", 10);
|
|
7380
|
-
|
|
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..."));
|
|
7381
7843
|
await hub.store.initialize();
|
|
7382
7844
|
const db = hub.store.getDb();
|
|
7383
7845
|
if (!db) {
|
|
@@ -7385,7 +7847,41 @@ async function reviewCommand(options) {
|
|
|
7385
7847
|
process.exit(1);
|
|
7386
7848
|
}
|
|
7387
7849
|
const decayEngine = new DecayEngine(db);
|
|
7388
|
-
|
|
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
|
+
}
|
|
7389
7885
|
if (decaying.length === 0) {
|
|
7390
7886
|
console.log(chalk10.green("\n\u2728 All knowledge is healthy! No notes to review."));
|
|
7391
7887
|
return;
|
|
@@ -7534,7 +8030,7 @@ async function gapsCommand() {
|
|
|
7534
8030
|
|
|
7535
8031
|
// packages/cli/dist/commands/clip-cmd.js
|
|
7536
8032
|
import chalk13 from "chalk";
|
|
7537
|
-
import { writeFileSync as
|
|
8033
|
+
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync17 } from "node:fs";
|
|
7538
8034
|
import { join as join22 } from "node:path";
|
|
7539
8035
|
async function clipCommand(url, options) {
|
|
7540
8036
|
if (!url) {
|
|
@@ -7583,7 +8079,7 @@ async function clipCommand(url, options) {
|
|
|
7583
8079
|
"",
|
|
7584
8080
|
content
|
|
7585
8081
|
].join("\n");
|
|
7586
|
-
|
|
8082
|
+
writeFileSync17(filePath, md, "utf-8");
|
|
7587
8083
|
console.log(chalk13.green(`\u2705 Saved: ${fileName}`));
|
|
7588
8084
|
console.log(chalk13.dim(` \u2192 ${filePath}`));
|
|
7589
8085
|
console.log(chalk13.dim(" \u{1F4A1} Run stellavault index to make it searchable"));
|
|
@@ -7784,7 +8280,7 @@ async function digestCommand(options) {
|
|
|
7784
8280
|
\u{1F9E0} Health: R=${report.averageR} | Decaying ${report.decayingCount} | Critical ${report.criticalCount}`);
|
|
7785
8281
|
console.log(chalk15.dim("\n\u2550".repeat(50)));
|
|
7786
8282
|
if (options.visual) {
|
|
7787
|
-
const { writeFileSync:
|
|
8283
|
+
const { writeFileSync: writeFileSync22, mkdirSync: mkdirSync22, existsSync: existsSync25 } = await import("node:fs");
|
|
7788
8284
|
const { join: join30, resolve: resolve22 } = await import("node:path");
|
|
7789
8285
|
const outputDir = resolve22(config.vaultPath, "_stellavault/digests");
|
|
7790
8286
|
if (!existsSync25(outputDir))
|
|
@@ -7829,7 +8325,7 @@ async function digestCommand(options) {
|
|
|
7829
8325
|
"---",
|
|
7830
8326
|
`*Generated by \`stellavault digest --visual\` on ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
7831
8327
|
].filter(Boolean).join("\n");
|
|
7832
|
-
|
|
8328
|
+
writeFileSync22(outputPath, md, "utf-8");
|
|
7833
8329
|
console.log(chalk15.green(`
|
|
7834
8330
|
Visual digest saved: ${filename}`));
|
|
7835
8331
|
console.log(chalk15.dim(`Open in Obsidian to see Mermaid charts.`));
|
|
@@ -7838,7 +8334,7 @@ Visual digest saved: ${filename}`));
|
|
|
7838
8334
|
|
|
7839
8335
|
// packages/cli/dist/commands/init-cmd.js
|
|
7840
8336
|
import { createInterface as createInterface2 } from "node:readline";
|
|
7841
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync18, writeFileSync as
|
|
8337
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync18, writeFileSync as writeFileSync18 } from "node:fs";
|
|
7842
8338
|
import { join as join23 } from "node:path";
|
|
7843
8339
|
import { homedir as homedir14 } from "node:os";
|
|
7844
8340
|
import ora2 from "ora";
|
|
@@ -7886,7 +8382,7 @@ async function initCommand() {
|
|
|
7886
8382
|
search: { defaultLimit: 10, rrfK: 60 },
|
|
7887
8383
|
mcp: { mode: "stdio", port: 3333 }
|
|
7888
8384
|
};
|
|
7889
|
-
|
|
8385
|
+
writeFileSync18(join23(homedir14(), ".stellavault.json"), JSON.stringify(configData, null, 2), "utf-8");
|
|
7890
8386
|
console.log(chalk16.dim(` Config saved: ~/.stellavault.json`));
|
|
7891
8387
|
console.log("");
|
|
7892
8388
|
console.log(chalk16.cyan(" Step 2/3") + " \u2014 Indexing your vault");
|
|
@@ -7984,7 +8480,7 @@ Andrej Karpathy's approach: every session auto-compiles into structured knowledg
|
|
|
7984
8480
|
}
|
|
7985
8481
|
];
|
|
7986
8482
|
for (const s of samples) {
|
|
7987
|
-
|
|
8483
|
+
writeFileSync18(join23(rawDir, s.file), s.content, "utf-8");
|
|
7988
8484
|
}
|
|
7989
8485
|
const reSpinner = ora2({ text: " Indexing sample notes...", indent: 2 }).start();
|
|
7990
8486
|
const reResult = await indexVault(vaultPath, {
|
|
@@ -8158,10 +8654,22 @@ async function contradictionsCommand(_opts, cmd) {
|
|
|
8158
8654
|
import { createInterface as createInterface3 } from "node:readline";
|
|
8159
8655
|
import chalk19 from "chalk";
|
|
8160
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
|
+
}
|
|
8161
8669
|
const config = loadConfig();
|
|
8162
8670
|
const identity = getOrCreateIdentity(options.name);
|
|
8163
8671
|
console.log("");
|
|
8164
|
-
console.log(chalk19.bold(" \u2726 Stellavault Federation"));
|
|
8672
|
+
console.log(chalk19.bold(" \u2726 Stellavault Federation") + chalk19.yellow(" (experimental)"));
|
|
8165
8673
|
console.log(chalk19.dim(` Node: ${identity.displayName} (${identity.peerId})`));
|
|
8166
8674
|
console.log("");
|
|
8167
8675
|
const store = createSqliteVecStore(config.dbPath);
|
|
@@ -8174,6 +8682,13 @@ async function federateJoinCommand(options) {
|
|
|
8174
8682
|
node.setLocalStats(stats.documentCount, topics.slice(0, 5).map((t2) => t2.topic));
|
|
8175
8683
|
const search = new FederatedSearch(node, store, embedder);
|
|
8176
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
|
+
}
|
|
8177
8692
|
node.on("joined", (info) => {
|
|
8178
8693
|
console.log(chalk19.green(` \u2726 Joined federation network`));
|
|
8179
8694
|
console.log(chalk19.dim(` Topic: ${info.topic}`));
|
|
@@ -8621,7 +9136,7 @@ import chalk25 from "chalk";
|
|
|
8621
9136
|
// packages/core/dist/intelligence/draft-generator.js
|
|
8622
9137
|
init_wiki_compiler();
|
|
8623
9138
|
init_config();
|
|
8624
|
-
import { writeFileSync as
|
|
9139
|
+
import { writeFileSync as writeFileSync19, mkdirSync as mkdirSync19, existsSync as existsSync19 } from "node:fs";
|
|
8625
9140
|
import { join as join24, resolve as resolve16, basename as basename5, extname as extname7 } from "node:path";
|
|
8626
9141
|
function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
|
|
8627
9142
|
const { topic, format = "blog", maxSections = 8, blueprint } = options;
|
|
@@ -8705,7 +9220,7 @@ function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
|
|
|
8705
9220
|
const filename = `${timestamp}-${slug}.md`;
|
|
8706
9221
|
const filePath = join24("_drafts", filename);
|
|
8707
9222
|
const fullPath = resolve16(vaultPath, filePath);
|
|
8708
|
-
|
|
9223
|
+
writeFileSync19(fullPath, body, "utf-8");
|
|
8709
9224
|
return {
|
|
8710
9225
|
title: draftTitle,
|
|
8711
9226
|
filePath,
|
|
@@ -8901,7 +9416,7 @@ async function draftCommand(topic, options) {
|
|
|
8901
9416
|
}
|
|
8902
9417
|
}
|
|
8903
9418
|
async function enhanceWithAI(vaultPath, draftPath, topic, format) {
|
|
8904
|
-
const { readFileSync: readFileSync18, writeFileSync:
|
|
9419
|
+
const { readFileSync: readFileSync18, writeFileSync: writeFileSync22 } = await import("node:fs");
|
|
8905
9420
|
const { resolve: resolve22 } = await import("node:path");
|
|
8906
9421
|
const fullPath = resolve22(vaultPath, draftPath);
|
|
8907
9422
|
const scaffold = readFileSync18(fullPath, "utf-8");
|
|
@@ -8946,7 +9461,7 @@ ${aiContent.text}
|
|
|
8946
9461
|
---
|
|
8947
9462
|
*Generated by \`stellavault draft --ai\` using Claude API at ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
8948
9463
|
`;
|
|
8949
|
-
|
|
9464
|
+
writeFileSync22(fullPath, enhanced, "utf-8");
|
|
8950
9465
|
}
|
|
8951
9466
|
} catch (err) {
|
|
8952
9467
|
console.error(chalk25.yellow(` AI enhancement failed: ${err instanceof Error ? err.message : "unknown"}. Keeping rule-based draft.`));
|
|
@@ -8955,7 +9470,7 @@ ${aiContent.text}
|
|
|
8955
9470
|
|
|
8956
9471
|
// packages/cli/dist/commands/session-cmd.js
|
|
8957
9472
|
import chalk26 from "chalk";
|
|
8958
|
-
import { writeFileSync as
|
|
9473
|
+
import { writeFileSync as writeFileSync20, mkdirSync as mkdirSync20, existsSync as existsSync20, appendFileSync } from "node:fs";
|
|
8959
9474
|
import { resolve as resolve17, join as join25 } from "node:path";
|
|
8960
9475
|
async function sessionSaveCommand(options) {
|
|
8961
9476
|
const config = loadConfig();
|
|
@@ -9021,7 +9536,7 @@ async function sessionSaveCommand(options) {
|
|
|
9021
9536
|
`# Daily Log \u2014 ${dateStr}`,
|
|
9022
9537
|
""
|
|
9023
9538
|
].join("\n");
|
|
9024
|
-
|
|
9539
|
+
writeFileSync20(logFile, header + entry.join("\n"), "utf-8");
|
|
9025
9540
|
} else {
|
|
9026
9541
|
appendFileSync(logFile, entry.join("\n"), "utf-8");
|
|
9027
9542
|
}
|
|
@@ -9192,7 +9707,7 @@ async function lintCommand() {
|
|
|
9192
9707
|
|
|
9193
9708
|
// packages/cli/dist/commands/fleeting-cmd.js
|
|
9194
9709
|
import chalk30 from "chalk";
|
|
9195
|
-
import { writeFileSync as
|
|
9710
|
+
import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync21, existsSync as existsSync22 } from "node:fs";
|
|
9196
9711
|
import { join as join27, resolve as resolve19 } from "node:path";
|
|
9197
9712
|
async function fleetingCommand(text, options) {
|
|
9198
9713
|
if (!text || text.trim().length < 2) {
|
|
@@ -9226,7 +9741,7 @@ async function fleetingCommand(text, options) {
|
|
|
9226
9741
|
"---",
|
|
9227
9742
|
`*Captured via \`stellavault fleeting\` at ${now.toLocaleString("ko-KR")}*`
|
|
9228
9743
|
].join("\n");
|
|
9229
|
-
|
|
9744
|
+
writeFileSync21(filePath, content, "utf-8");
|
|
9230
9745
|
console.log(chalk30.green(`Captured: ${filename}`));
|
|
9231
9746
|
console.log(chalk30.dim(`Location: raw/${filename}`));
|
|
9232
9747
|
console.log(chalk30.dim("Run `stellavault compile` to process into wiki."));
|
|
@@ -9647,25 +10162,25 @@ if (nodeVersion < 20) {
|
|
|
9647
10162
|
process.exit(1);
|
|
9648
10163
|
}
|
|
9649
10164
|
var program = new Command();
|
|
9650
|
-
var SV_VERSION = true ? "0.7.
|
|
10165
|
+
var SV_VERSION = true ? "0.7.4" : "0.0.0-dev";
|
|
9651
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");
|
|
9652
10167
|
program.command("init").description("Interactive setup wizard \u2014 get started in 3 minutes").action(initCommand);
|
|
9653
10168
|
program.command("doctor").description("Diagnose setup issues (config, vault, DB, model, Node version)").action(doctorCommand);
|
|
9654
|
-
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));
|
|
9655
10170
|
program.command("status").description("Show index status (document count, last indexed, DB size)").action(statusCommand);
|
|
9656
10171
|
program.command("search <query>").description("Search your knowledge base (hybrid BM25 + vector)").option("-l, --limit <n>", "Max results", "5").action(searchCommand);
|
|
9657
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));
|
|
9658
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));
|
|
9659
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);
|
|
9660
10175
|
program.command("graph").description("Launch the 3D knowledge graph in your browser").action(graphCommand);
|
|
9661
|
-
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);
|
|
9662
10177
|
program.command("decay").description("Memory decay report \u2014 find notes you are forgetting").action(decayCommand);
|
|
9663
10178
|
program.command("brief").description("Daily knowledge briefing (decay + gaps + activity)").action(briefCommand);
|
|
9664
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);
|
|
9665
10180
|
program.command("gaps").description("Detect knowledge gaps (weak connections between clusters)").action(gapsCommand);
|
|
9666
10181
|
program.command("duplicates").description("Find duplicate or near-identical notes").option("-t, --threshold <n>", "Similarity threshold (0\u20131)", "0.88").action(duplicatesCommand);
|
|
9667
10182
|
program.command("contradictions").description("Detect contradicting statements across your notes").action(contradictionsCommand);
|
|
9668
|
-
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);
|
|
9669
10184
|
program.command("learn").description("AI learning path \u2014 personalized recommendations based on decay + gaps").action(learnCommand);
|
|
9670
10185
|
program.command("lint").description("Knowledge health check \u2014 gaps, duplicates, contradictions, stale notes").action(() => lintCommand());
|
|
9671
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));
|