@vheins/local-memory-mcp 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -173,8 +173,8 @@ var LOG_LEVEL_VALUES = Object.keys(LEVELS);
|
|
|
173
173
|
|
|
174
174
|
// src/mcp/storage/sqlite.ts
|
|
175
175
|
import Database from "better-sqlite3";
|
|
176
|
-
import
|
|
177
|
-
import
|
|
176
|
+
import path3 from "path";
|
|
177
|
+
import fs3 from "fs";
|
|
178
178
|
import os from "os";
|
|
179
179
|
|
|
180
180
|
// src/mcp/storage/migrations.ts
|
|
@@ -783,6 +783,9 @@ var BaseEntity = class {
|
|
|
783
783
|
this.db = db;
|
|
784
784
|
}
|
|
785
785
|
db;
|
|
786
|
+
transaction(fn) {
|
|
787
|
+
return this.db.transaction(fn)();
|
|
788
|
+
}
|
|
786
789
|
run(sql, params = []) {
|
|
787
790
|
const stmt = this.db.prepare(sql);
|
|
788
791
|
const result = stmt.run(...params);
|
|
@@ -1031,40 +1034,42 @@ var MemoryEntity = class extends BaseEntity {
|
|
|
1031
1034
|
return rows.map((row) => this.rowToMemoryEntry(row));
|
|
1032
1035
|
}
|
|
1033
1036
|
bulkInsertMemories(entries) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1037
|
+
return this.transaction(() => {
|
|
1038
|
+
let count = 0;
|
|
1039
|
+
for (const entry of entries) {
|
|
1040
|
+
this.run(
|
|
1041
|
+
`INSERT INTO memories (
|
|
1042
|
+
id, repo, type, title, content, importance, folder, language,
|
|
1043
|
+
created_at, updated_at, hit_count, recall_count, last_used_at, expires_at,
|
|
1044
|
+
supersedes, status, is_global, tags, metadata, agent, role, model, completed_at
|
|
1045
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1046
|
+
[
|
|
1047
|
+
entry.id,
|
|
1048
|
+
entry.scope.repo,
|
|
1049
|
+
entry.type,
|
|
1050
|
+
entry.title || null,
|
|
1051
|
+
entry.content,
|
|
1052
|
+
entry.importance,
|
|
1053
|
+
entry.scope.folder || null,
|
|
1054
|
+
entry.scope.language || null,
|
|
1055
|
+
entry.created_at,
|
|
1056
|
+
entry.updated_at,
|
|
1057
|
+
entry.expires_at ?? null,
|
|
1058
|
+
entry.supersedes ?? null,
|
|
1059
|
+
entry.status || "active",
|
|
1060
|
+
entry.is_global ? 1 : 0,
|
|
1061
|
+
entry.tags ? JSON.stringify(entry.tags) : null,
|
|
1062
|
+
entry.metadata ? JSON.stringify(entry.metadata) : null,
|
|
1063
|
+
entry.agent || "unknown",
|
|
1064
|
+
entry.role || "unknown",
|
|
1065
|
+
entry.model || "unknown",
|
|
1066
|
+
entry.completed_at || null
|
|
1067
|
+
]
|
|
1068
|
+
);
|
|
1069
|
+
count++;
|
|
1070
|
+
}
|
|
1071
|
+
return count;
|
|
1072
|
+
});
|
|
1068
1073
|
}
|
|
1069
1074
|
bulkUpdateMemories(ids, updates) {
|
|
1070
1075
|
if (ids.length === 0) return 0;
|
|
@@ -1088,28 +1093,32 @@ var MemoryEntity = class extends BaseEntity {
|
|
|
1088
1093
|
if (fields.length === 0) return 0;
|
|
1089
1094
|
fields.push("updated_at = ?");
|
|
1090
1095
|
values.push((/* @__PURE__ */ new Date()).toISOString());
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1096
|
+
return this.transaction(() => {
|
|
1097
|
+
let count = 0;
|
|
1098
|
+
const chunkSize = 500;
|
|
1099
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
1100
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
1101
|
+
const result = this.run(
|
|
1102
|
+
`UPDATE memories SET ${fields.join(", ")} WHERE id IN (${chunk.map(() => "?").join(",")})`,
|
|
1103
|
+
[...values, ...chunk]
|
|
1104
|
+
);
|
|
1105
|
+
count += result.changes;
|
|
1106
|
+
}
|
|
1107
|
+
return count;
|
|
1108
|
+
});
|
|
1102
1109
|
}
|
|
1103
1110
|
bulkDeleteMemories(ids) {
|
|
1104
1111
|
if (ids.length === 0) return 0;
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1112
|
+
return this.transaction(() => {
|
|
1113
|
+
let count = 0;
|
|
1114
|
+
const chunkSize = 500;
|
|
1115
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
1116
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
1117
|
+
const result = this.run(`DELETE FROM memories WHERE id IN (${chunk.map(() => "?").join(",")})`, chunk);
|
|
1118
|
+
count += result.changes;
|
|
1119
|
+
}
|
|
1120
|
+
return count;
|
|
1121
|
+
});
|
|
1113
1122
|
}
|
|
1114
1123
|
getRecentMemories(repo, limit, offset = 0, includeArchived = false, excludeTypes = [], sortOrder = "DESC") {
|
|
1115
1124
|
let query = "SELECT * FROM memories WHERE repo = ?";
|
|
@@ -1521,40 +1530,42 @@ var TaskEntity = class extends BaseEntity {
|
|
|
1521
1530
|
return (row?.count ?? 0) > 0;
|
|
1522
1531
|
}
|
|
1523
1532
|
bulkInsertTasks(tasks) {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1533
|
+
return this.transaction(() => {
|
|
1534
|
+
let count = 0;
|
|
1535
|
+
for (const task of tasks) {
|
|
1536
|
+
this.run(
|
|
1537
|
+
`INSERT INTO tasks (
|
|
1538
|
+
id, repo, task_code, phase, title, description, status, priority,
|
|
1539
|
+
agent, role, doc_path, created_at, updated_at, finished_at, canceled_at, tags, metadata, parent_id, depends_on, est_tokens, in_progress_at
|
|
1540
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1541
|
+
[
|
|
1542
|
+
task.id,
|
|
1543
|
+
task.repo,
|
|
1544
|
+
task.task_code,
|
|
1545
|
+
task.phase || null,
|
|
1546
|
+
task.title,
|
|
1547
|
+
task.description || null,
|
|
1548
|
+
task.status || "backlog",
|
|
1549
|
+
task.priority || 3,
|
|
1550
|
+
task.agent || "unknown",
|
|
1551
|
+
task.role || "unknown",
|
|
1552
|
+
task.doc_path || null,
|
|
1553
|
+
task.created_at,
|
|
1554
|
+
task.updated_at,
|
|
1555
|
+
task.finished_at || null,
|
|
1556
|
+
task.canceled_at || null,
|
|
1557
|
+
task.tags ? JSON.stringify(task.tags) : null,
|
|
1558
|
+
task.metadata ? JSON.stringify(task.metadata) : null,
|
|
1559
|
+
task.parent_id || null,
|
|
1560
|
+
task.depends_on || null,
|
|
1561
|
+
task.est_tokens || 0,
|
|
1562
|
+
task.in_progress_at || null
|
|
1563
|
+
]
|
|
1564
|
+
);
|
|
1565
|
+
count++;
|
|
1566
|
+
}
|
|
1567
|
+
return count;
|
|
1568
|
+
});
|
|
1558
1569
|
}
|
|
1559
1570
|
insertTaskComment(comment) {
|
|
1560
1571
|
this.run(
|
|
@@ -1882,16 +1893,79 @@ var SummaryEntity = class extends BaseEntity {
|
|
|
1882
1893
|
}
|
|
1883
1894
|
};
|
|
1884
1895
|
|
|
1896
|
+
// src/mcp/storage/write-lock.ts
|
|
1897
|
+
import lockfile from "proper-lockfile";
|
|
1898
|
+
import path2 from "path";
|
|
1899
|
+
import fs2 from "fs";
|
|
1900
|
+
var LOCK_STALE_MS = 15e3;
|
|
1901
|
+
var LOCK_RETRY_DELAY_MS = 100;
|
|
1902
|
+
var LOCK_RETRY_COUNT = 150;
|
|
1903
|
+
var WriteLock = class {
|
|
1904
|
+
lockTarget;
|
|
1905
|
+
locked = false;
|
|
1906
|
+
constructor(dbPath) {
|
|
1907
|
+
this.lockTarget = dbPath;
|
|
1908
|
+
if (!fs2.existsSync(dbPath)) {
|
|
1909
|
+
fs2.mkdirSync(path2.dirname(dbPath), { recursive: true });
|
|
1910
|
+
fs2.writeFileSync(dbPath, "");
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Acquire the write lock. Waits up to 15s for other processes to release.
|
|
1915
|
+
*/
|
|
1916
|
+
async acquire() {
|
|
1917
|
+
await lockfile.lock(this.lockTarget, {
|
|
1918
|
+
stale: LOCK_STALE_MS,
|
|
1919
|
+
retries: {
|
|
1920
|
+
retries: LOCK_RETRY_COUNT,
|
|
1921
|
+
minTimeout: LOCK_RETRY_DELAY_MS,
|
|
1922
|
+
maxTimeout: LOCK_RETRY_DELAY_MS
|
|
1923
|
+
},
|
|
1924
|
+
realpath: false
|
|
1925
|
+
});
|
|
1926
|
+
this.locked = true;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Release the write lock.
|
|
1930
|
+
*/
|
|
1931
|
+
async release() {
|
|
1932
|
+
if (!this.locked) return;
|
|
1933
|
+
try {
|
|
1934
|
+
await lockfile.unlock(this.lockTarget, { realpath: false });
|
|
1935
|
+
} catch {
|
|
1936
|
+
}
|
|
1937
|
+
this.locked = false;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Run a synchronous write function under the lock.
|
|
1941
|
+
* Guarantees lock is always released, even on error.
|
|
1942
|
+
*/
|
|
1943
|
+
async withLock(fn) {
|
|
1944
|
+
await this.acquire();
|
|
1945
|
+
try {
|
|
1946
|
+
return fn();
|
|
1947
|
+
} finally {
|
|
1948
|
+
await this.release();
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Check if a lock file exists (another process may be writing).
|
|
1953
|
+
*/
|
|
1954
|
+
isLocked() {
|
|
1955
|
+
return lockfile.checkSync(this.lockTarget, { realpath: false });
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1885
1959
|
// src/mcp/storage/sqlite.ts
|
|
1886
1960
|
function resolveDbPath() {
|
|
1887
1961
|
if (process.env.MEMORY_DB_PATH) return process.env.MEMORY_DB_PATH;
|
|
1888
|
-
const standardConfigDir = process.platform === "win32" ?
|
|
1889
|
-
const standardPath =
|
|
1890
|
-
if (
|
|
1891
|
-
const legacyPath =
|
|
1892
|
-
if (
|
|
1893
|
-
const localCwdFile =
|
|
1894
|
-
if (
|
|
1962
|
+
const standardConfigDir = process.platform === "win32" ? path3.join(os.homedir(), ".local-memory-mcp") : process.platform === "darwin" ? path3.join(os.homedir(), "Library", "Application Support", "local-memory-mcp") : path3.join(os.homedir(), ".config", "local-memory-mcp");
|
|
1963
|
+
const standardPath = path3.join(standardConfigDir, "memory.db");
|
|
1964
|
+
if (fs3.existsSync(standardPath)) return standardPath;
|
|
1965
|
+
const legacyPath = path3.join(os.homedir(), ".config", "local-memory-mcp", "memory.db");
|
|
1966
|
+
if (fs3.existsSync(legacyPath)) return legacyPath;
|
|
1967
|
+
const localCwdFile = path3.join(process.cwd(), "storage", "memory.db");
|
|
1968
|
+
if (fs3.existsSync(localCwdFile)) return localCwdFile;
|
|
1895
1969
|
return standardPath;
|
|
1896
1970
|
}
|
|
1897
1971
|
var DB_PATH = resolveDbPath();
|
|
@@ -1902,21 +1976,26 @@ var SQLiteStore = class _SQLiteStore {
|
|
|
1902
1976
|
actions;
|
|
1903
1977
|
system;
|
|
1904
1978
|
summaries;
|
|
1979
|
+
lock;
|
|
1905
1980
|
dbPathInstance;
|
|
1906
1981
|
constructor(dbPath) {
|
|
1907
1982
|
const finalPath = dbPath ?? DB_PATH;
|
|
1908
1983
|
this.dbPathInstance = finalPath;
|
|
1909
1984
|
if (finalPath !== ":memory:") {
|
|
1910
|
-
const dbDir =
|
|
1911
|
-
if (!
|
|
1912
|
-
|
|
1985
|
+
const dbDir = path3.dirname(finalPath);
|
|
1986
|
+
if (!fs3.existsSync(dbDir)) {
|
|
1987
|
+
fs3.mkdirSync(dbDir, { recursive: true });
|
|
1913
1988
|
}
|
|
1914
1989
|
}
|
|
1915
1990
|
this.db = new Database(finalPath);
|
|
1916
1991
|
this.db.pragma("journal_mode = WAL");
|
|
1917
|
-
this.db.pragma("synchronous =
|
|
1918
|
-
this.db.pragma("busy_timeout =
|
|
1992
|
+
this.db.pragma("synchronous = FULL");
|
|
1993
|
+
this.db.pragma("busy_timeout = 30000");
|
|
1919
1994
|
this.db.pragma("foreign_keys = ON");
|
|
1995
|
+
this.db.pragma("wal_autocheckpoint = 100");
|
|
1996
|
+
if (finalPath !== ":memory:") {
|
|
1997
|
+
this._startupChecks(finalPath);
|
|
1998
|
+
}
|
|
1920
1999
|
const migrator = new MigrationManager(this.db);
|
|
1921
2000
|
migrator.migrate();
|
|
1922
2001
|
migrator.addMemoryCodeColumn();
|
|
@@ -1925,10 +2004,88 @@ var SQLiteStore = class _SQLiteStore {
|
|
|
1925
2004
|
this.actions = new ActionEntity(this.db);
|
|
1926
2005
|
this.system = new SystemEntity(this.db);
|
|
1927
2006
|
this.summaries = new SummaryEntity(this.db);
|
|
2007
|
+
this.lock = new WriteLock(finalPath);
|
|
1928
2008
|
}
|
|
1929
|
-
|
|
2009
|
+
/**
|
|
2010
|
+
* Run on startup: checkpoint WAL and verify integrity.
|
|
2011
|
+
* If integrity check fails, attempt to restore from backup.
|
|
2012
|
+
*/
|
|
2013
|
+
_startupChecks(dbPath) {
|
|
2014
|
+
try {
|
|
2015
|
+
this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
2016
|
+
logger.debug("[SQLiteStore] WAL checkpoint completed on startup");
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
logger.warn("[SQLiteStore] WAL checkpoint failed on startup", { error: String(err) });
|
|
2019
|
+
}
|
|
2020
|
+
try {
|
|
2021
|
+
const result = this.db.pragma("integrity_check");
|
|
2022
|
+
const ok = result.length === 1 && result[0].integrity_check === "ok";
|
|
2023
|
+
if (!ok) {
|
|
2024
|
+
logger.error("[SQLiteStore] Integrity check FAILED", { result });
|
|
2025
|
+
this._attemptRecovery(dbPath);
|
|
2026
|
+
} else {
|
|
2027
|
+
logger.debug("[SQLiteStore] Integrity check passed");
|
|
2028
|
+
}
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
logger.error("[SQLiteStore] Integrity check threw error", { error: String(err) });
|
|
2031
|
+
this._attemptRecovery(dbPath);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Attempt to recover from a corrupt DB by restoring the latest backup.
|
|
2036
|
+
*/
|
|
2037
|
+
_attemptRecovery(dbPath) {
|
|
2038
|
+
const backupPath = dbPath + ".backup";
|
|
2039
|
+
if (fs3.existsSync(backupPath)) {
|
|
2040
|
+
logger.warn("[SQLiteStore] Attempting recovery from backup", { backupPath });
|
|
2041
|
+
try {
|
|
2042
|
+
const corruptPath = `${dbPath}.corrupt_${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15)}`;
|
|
2043
|
+
fs3.copyFileSync(dbPath, corruptPath);
|
|
2044
|
+
fs3.copyFileSync(backupPath, dbPath);
|
|
2045
|
+
logger.warn("[SQLiteStore] Recovery successful. Corrupt file saved to", { corruptPath });
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
logger.error("[SQLiteStore] Recovery failed", { error: String(err) });
|
|
2048
|
+
}
|
|
2049
|
+
} else {
|
|
2050
|
+
logger.error("[SQLiteStore] No backup found for recovery. DB may be corrupt.");
|
|
2051
|
+
}
|
|
1930
2052
|
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Create a timestamped backup of the DB file.
|
|
2055
|
+
* Called automatically after successful writes (via withWrite).
|
|
2056
|
+
*/
|
|
2057
|
+
backup() {
|
|
2058
|
+
if (this.dbPathInstance === ":memory:") return;
|
|
2059
|
+
try {
|
|
2060
|
+
this.db.pragma("wal_checkpoint(PASSIVE)");
|
|
2061
|
+
const backupPath = this.dbPathInstance + ".backup";
|
|
2062
|
+
fs3.copyFileSync(this.dbPathInstance, backupPath);
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
logger.warn("[SQLiteStore] Backup failed", { error: String(err) });
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Execute a write operation under the file lock.
|
|
2069
|
+
* This is the ONLY way writes should be performed.
|
|
2070
|
+
*
|
|
2071
|
+
* @example
|
|
2072
|
+
* await db.withWrite(() => db.tasks.insertTask(task));
|
|
2073
|
+
*/
|
|
2074
|
+
async withWrite(fn) {
|
|
2075
|
+
return this.lock.withLock(fn);
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Checkpoint WAL so dashboard (and other readers) see latest data.
|
|
2079
|
+
* Called by dashboard controllers before reads.
|
|
2080
|
+
*/
|
|
1931
2081
|
async refresh() {
|
|
2082
|
+
try {
|
|
2083
|
+
this.db.pragma("wal_checkpoint(PASSIVE)");
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
logger.warn("[SQLiteStore] refresh checkpoint failed", { error: String(err) });
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
async ready() {
|
|
1932
2089
|
}
|
|
1933
2090
|
static async create(dbPath) {
|
|
1934
2091
|
return new _SQLiteStore(dbPath);
|
|
@@ -1938,13 +2095,17 @@ var SQLiteStore = class _SQLiteStore {
|
|
|
1938
2095
|
}
|
|
1939
2096
|
close() {
|
|
1940
2097
|
if (this.db && this.db.open) {
|
|
2098
|
+
try {
|
|
2099
|
+
this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
2100
|
+
} catch {
|
|
2101
|
+
}
|
|
1941
2102
|
this.db.close();
|
|
1942
2103
|
}
|
|
1943
2104
|
}
|
|
1944
2105
|
};
|
|
1945
2106
|
|
|
1946
2107
|
// src/mcp/session.ts
|
|
1947
|
-
import
|
|
2108
|
+
import path4 from "path";
|
|
1948
2109
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1949
2110
|
function createSessionContext() {
|
|
1950
2111
|
return {
|
|
@@ -2010,7 +2171,7 @@ function getFilesystemRoots(session) {
|
|
|
2010
2171
|
for (const root of session.roots) {
|
|
2011
2172
|
if (!root.uri.startsWith("file://")) continue;
|
|
2012
2173
|
try {
|
|
2013
|
-
resolved.push(
|
|
2174
|
+
resolved.push(path4.resolve(fileURLToPath2(root.uri)));
|
|
2014
2175
|
} catch {
|
|
2015
2176
|
}
|
|
2016
2177
|
}
|
|
@@ -2019,19 +2180,19 @@ function getFilesystemRoots(session) {
|
|
|
2019
2180
|
function isPathWithinRoots(targetPath, session) {
|
|
2020
2181
|
const roots = getFilesystemRoots(session);
|
|
2021
2182
|
if (roots.length === 0) return true;
|
|
2022
|
-
const normalizedTarget =
|
|
2183
|
+
const normalizedTarget = path4.resolve(targetPath);
|
|
2023
2184
|
return roots.some((rootPath) => {
|
|
2024
|
-
const relative =
|
|
2025
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
2185
|
+
const relative = path4.relative(rootPath, normalizedTarget);
|
|
2186
|
+
return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
2026
2187
|
});
|
|
2027
2188
|
}
|
|
2028
2189
|
function findContainingRoot(targetPath, session) {
|
|
2029
2190
|
const roots = getFilesystemRoots(session);
|
|
2030
2191
|
if (roots.length === 0) return null;
|
|
2031
|
-
const normalizedTarget =
|
|
2192
|
+
const normalizedTarget = path4.resolve(targetPath);
|
|
2032
2193
|
for (const rootPath of roots) {
|
|
2033
|
-
const relative =
|
|
2034
|
-
if (relative === "" || !relative.startsWith("..") && !
|
|
2194
|
+
const relative = path4.relative(rootPath, normalizedTarget);
|
|
2195
|
+
if (relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative)) {
|
|
2035
2196
|
return rootPath;
|
|
2036
2197
|
}
|
|
2037
2198
|
}
|
|
@@ -2040,7 +2201,7 @@ function findContainingRoot(targetPath, session) {
|
|
|
2040
2201
|
function inferRepoFromSession(session) {
|
|
2041
2202
|
const roots = getFilesystemRoots(session);
|
|
2042
2203
|
if (roots.length === 1) {
|
|
2043
|
-
return
|
|
2204
|
+
return path4.basename(roots[0]);
|
|
2044
2205
|
}
|
|
2045
2206
|
return void 0;
|
|
2046
2207
|
}
|
|
@@ -3468,9 +3629,9 @@ function parseRepoUri(uri) {
|
|
|
3468
3629
|
const slashIdx = withoutQuery.indexOf("/");
|
|
3469
3630
|
if (slashIdx === -1) return null;
|
|
3470
3631
|
const name = withoutQuery.slice(0, slashIdx);
|
|
3471
|
-
const
|
|
3472
|
-
if (!name || !
|
|
3473
|
-
return { name, path:
|
|
3632
|
+
const path6 = withoutQuery.slice(slashIdx + 1);
|
|
3633
|
+
if (!name || !path6) return null;
|
|
3634
|
+
return { name, path: path6, query: new URLSearchParams(queryString) };
|
|
3474
3635
|
}
|
|
3475
3636
|
function paginateEntries(key, entries, params) {
|
|
3476
3637
|
const limit = normalizeLimit(params?.limit);
|
|
@@ -3505,12 +3666,12 @@ function invalidCompletionParams(message) {
|
|
|
3505
3666
|
}
|
|
3506
3667
|
|
|
3507
3668
|
// src/mcp/prompts/loader.ts
|
|
3508
|
-
import
|
|
3509
|
-
import
|
|
3669
|
+
import fs4 from "fs";
|
|
3670
|
+
import path5 from "path";
|
|
3510
3671
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3511
3672
|
import matter from "gray-matter";
|
|
3512
3673
|
var __filename = fileURLToPath3(import.meta.url);
|
|
3513
|
-
var __dirname2 =
|
|
3674
|
+
var __dirname2 = path5.dirname(__filename);
|
|
3514
3675
|
function findPromptDir() {
|
|
3515
3676
|
const candidates = [
|
|
3516
3677
|
// Production if chunked into dist/
|
|
@@ -3519,28 +3680,28 @@ function findPromptDir() {
|
|
|
3519
3680
|
"../prompts",
|
|
3520
3681
|
// Dev: /src/mcp/prompts/definitions (next to loader.ts)
|
|
3521
3682
|
"./definitions"
|
|
3522
|
-
].map((relPath) =>
|
|
3683
|
+
].map((relPath) => path5.resolve(__dirname2, relPath));
|
|
3523
3684
|
for (const dir of candidates) {
|
|
3524
|
-
if (
|
|
3525
|
-
const files =
|
|
3685
|
+
if (fs4.existsSync(dir)) {
|
|
3686
|
+
const files = fs4.readdirSync(dir);
|
|
3526
3687
|
if (files.some((f) => f.endsWith(".md"))) {
|
|
3527
3688
|
return dir;
|
|
3528
3689
|
}
|
|
3529
3690
|
}
|
|
3530
3691
|
}
|
|
3531
|
-
return
|
|
3692
|
+
return path5.resolve(__dirname2, "./definitions");
|
|
3532
3693
|
}
|
|
3533
3694
|
var PROMPT_DIR = findPromptDir();
|
|
3534
3695
|
function listPromptFiles() {
|
|
3535
|
-
if (!
|
|
3536
|
-
return
|
|
3696
|
+
if (!fs4.existsSync(PROMPT_DIR)) return [];
|
|
3697
|
+
return fs4.readdirSync(PROMPT_DIR).filter((file) => file.endsWith(".md")).map((file) => file.replace(/\.md$/, "")).sort();
|
|
3537
3698
|
}
|
|
3538
3699
|
function loadPromptFromMarkdown(name) {
|
|
3539
|
-
const filePath =
|
|
3540
|
-
if (!
|
|
3700
|
+
const filePath = path5.join(PROMPT_DIR, `${name}.md`);
|
|
3701
|
+
if (!fs4.existsSync(filePath)) {
|
|
3541
3702
|
throw new Error(`Prompt file not found: ${filePath}`);
|
|
3542
3703
|
}
|
|
3543
|
-
const fileContent =
|
|
3704
|
+
const fileContent = fs4.readFileSync(filePath, "utf-8");
|
|
3544
3705
|
const { data, content } = matter(fileContent);
|
|
3545
3706
|
return {
|
|
3546
3707
|
name: data.name || name,
|