@4da/mcp-server 6.0.2 → 6.2.0
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/CHANGELOG.md +433 -0
- package/README.md +26 -18
- package/dist/db.d.ts +73 -8
- package/dist/db.js +180 -43
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +15 -9
- package/dist/embeddings.js +19 -7
- package/dist/gitignore.js +9 -3
- package/dist/index.js +43 -4
- package/dist/live/cache.d.ts +31 -2
- package/dist/live/cache.js +113 -1
- package/dist/live/changelog-classify.d.ts +9 -3
- package/dist/live/changelog-classify.js +125 -12
- package/dist/live/changelog.d.ts +22 -3
- package/dist/live/changelog.js +132 -38
- package/dist/live/dev-scope.d.ts +2 -2
- package/dist/live/fix-paths.d.ts +118 -0
- package/dist/live/fix-paths.js +498 -0
- package/dist/live/github-releases.d.ts +69 -0
- package/dist/live/github-releases.js +252 -0
- package/dist/live/go-registry.d.ts +5 -0
- package/dist/live/go-registry.js +1 -1
- package/dist/live/go-releases.d.ts +48 -0
- package/dist/live/go-releases.js +196 -0
- package/dist/live/index.d.ts +34 -8
- package/dist/live/index.js +66 -7
- package/dist/live/lockfile-parents.d.ts +61 -0
- package/dist/live/lockfile-parents.js +360 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
- package/dist/live/lockfile-parsers-pygo.js +178 -28
- package/dist/live/lockfile-parsers.d.ts +4 -1
- package/dist/live/lockfile-parsers.js +57 -24
- package/dist/live/lockfile-types.d.ts +12 -3
- package/dist/live/lockfile-types.js +2 -1
- package/dist/live/osv-ranges.d.ts +85 -0
- package/dist/live/osv-ranges.js +136 -0
- package/dist/live/osv-scanner.d.ts +1 -0
- package/dist/live/osv-scanner.js +85 -78
- package/dist/live/package-archive.d.ts +2 -0
- package/dist/live/package-archive.js +7 -4
- package/dist/live/pypi-releases.d.ts +44 -0
- package/dist/live/pypi-releases.js +103 -0
- package/dist/live/range-satisfies.d.ts +21 -0
- package/dist/live/range-satisfies.js +190 -0
- package/dist/live/rate-limiter.d.ts +6 -0
- package/dist/live/rate-limiter.js +5 -0
- package/dist/live/registry-health.js +2 -0
- package/dist/live/release-metadata.d.ts +17 -8
- package/dist/live/release-metadata.js +6 -3
- package/dist/live/resolution.d.ts +11 -1
- package/dist/live/resolution.js +16 -6
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +10 -2
- package/dist/live/types.d.ts +17 -1
- package/dist/live/upgrade-go.d.ts +62 -0
- package/dist/live/upgrade-go.js +130 -0
- package/dist/live/upgrade-pypi.d.ts +42 -0
- package/dist/live/upgrade-pypi.js +85 -0
- package/dist/live/upgrade-sources.d.ts +30 -11
- package/dist/live/upgrade-sources.js +93 -21
- package/dist/live/zip-archive.d.ts +22 -0
- package/dist/live/zip-archive.js +98 -0
- package/dist/memory-store.d.ts +19 -0
- package/dist/memory-store.js +8 -0
- package/dist/native-bindings.d.ts +7 -2
- package/dist/native-bindings.js +4 -0
- package/dist/project-tree.d.ts +50 -8
- package/dist/project-tree.js +121 -29
- package/dist/schema-registry.js +5 -5
- package/dist/server-instructions.d.ts +6 -0
- package/dist/server-instructions.js +20 -15
- package/dist/sqlite-driver.d.ts +134 -0
- package/dist/sqlite-driver.js +359 -0
- package/dist/tool-dispatch.d.ts +8 -1
- package/dist/tool-dispatch.js +17 -2
- package/dist/tools/agent-memory.js +10 -2
- package/dist/tools/app-plan.d.ts +11 -1
- package/dist/tools/app-plan.js +32 -6
- package/dist/tools/briefing-task-scope.d.ts +21 -2
- package/dist/tools/briefing-task-scope.js +97 -4
- package/dist/tools/decision-memory.js +9 -3
- package/dist/tools/dependency-check-signals.d.ts +23 -5
- package/dist/tools/dependency-check-signals.js +62 -19
- package/dist/tools/dependency-check-versions.d.ts +94 -0
- package/dist/tools/dependency-check-versions.js +213 -0
- package/dist/tools/dependency-check.d.ts +27 -11
- package/dist/tools/dependency-check.js +321 -74
- package/dist/tools/dependency-health.d.ts +1 -1
- package/dist/tools/dependency-health.js +5 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
- package/dist/tools/knowledge-gap-ranges.js +1 -1
- package/dist/tools/knowledge-gap-scope.d.ts +2 -2
- package/dist/tools/project-scope.d.ts +45 -0
- package/dist/tools/project-scope.js +119 -0
- package/dist/tools/record-feedback.js +1 -1
- package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
- package/dist/tools/upgrade-impact-callsites.js +23 -5
- package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
- package/dist/tools/upgrade-impact-changelog.js +79 -0
- package/dist/tools/upgrade-impact-imports.d.ts +42 -0
- package/dist/tools/upgrade-impact-imports.js +260 -0
- package/dist/tools/upgrade-impact-literals.js +1 -1
- package/dist/tools/upgrade-impact-report.d.ts +23 -0
- package/dist/tools/upgrade-impact-report.js +45 -6
- package/dist/tools/upgrade-impact.d.ts +13 -4
- package/dist/tools/upgrade-impact.js +236 -85
- package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +31 -3
- package/dist/tools/upgrade-planner.js +74 -19
- package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
- package/dist/tools/vulnerability-scan-concise.js +15 -3
- package/dist/tools/vulnerability-scan-format.d.ts +58 -1
- package/dist/tools/vulnerability-scan-format.js +107 -11
- package/dist/tools/vulnerability-scan.d.ts +104 -13
- package/dist/tools/vulnerability-scan.js +27 -3
- package/dist/tools/what-should-i-know.d.ts +22 -2
- package/dist/tools/what-should-i-know.js +122 -12
- package/package.json +10 -32
package/dist/db.d.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Database module for 4DA MCP Server
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The desktop app's database (`4da.db`) is opened READ-ONLY
|
|
5
|
+
* (`{ readonly: true, fileMustExist: true }`) for everything the server reads.
|
|
6
|
+
* The app owns that file and its migrations: the server never creates or
|
|
7
|
+
* alters anything in it. It writes rows into exactly the app-owned tables its
|
|
8
|
+
* tools exist to write (APP_WRITABLE_TABLES: decisions, agent memory, feedback
|
|
9
|
+
* and interactions, which the app's calibration and decision UI read), through
|
|
10
|
+
* a separate write connection opened only for those statements (writerFor).
|
|
11
|
+
*
|
|
12
|
+
* Everything else the server writes goes to files it owns, under
|
|
13
|
+
* serverDataDir():
|
|
14
|
+
* - `cache.db`: registry, OSV and changelog responses (LiveCache).
|
|
15
|
+
* - `standalone.db`: the standalone database when no app is installed, and
|
|
16
|
+
* the fallback for a record the app's schema cannot hold (schema drift).
|
|
17
|
+
*
|
|
18
|
+
* Until 6.0.2 the server created a `live_cache` table inside the app's
|
|
19
|
+
* database (2,774 rows on one machine, 2026-10-07) and added `embedding`
|
|
20
|
+
* columns to two app tables when an embedding provider was set.
|
|
21
|
+
*/
|
|
22
|
+
import { type SqliteDatabase } from "./sqlite-driver.js";
|
|
23
|
+
/**
|
|
24
|
+
* Null when a SQLite driver (node:sqlite, or better-sqlite3 with a working
|
|
25
|
+
* native module) can open a database here; otherwise why not, with the fix.
|
|
6
26
|
*/
|
|
7
|
-
import type BetterSqlite3 from "better-sqlite3";
|
|
8
|
-
/** Null when better-sqlite3 can open a database here; otherwise why not, with the fix. */
|
|
9
27
|
export declare function nativeBindingProblem(): string | null;
|
|
10
28
|
import type { RelevantItem, UserContext, FeedbackAction, FeedbackResult } from "./types.js";
|
|
11
29
|
import type { ProjectScanResult } from "./project-scanner.js";
|
|
@@ -30,9 +48,17 @@ export declare function desktopAppDbPaths(platform?: NodeJS.Platform, env?: Node
|
|
|
30
48
|
* in, so agent memory and decisions did not follow the user between projects.
|
|
31
49
|
*/
|
|
32
50
|
export declare function standaloneDbPath(): string;
|
|
51
|
+
/**
|
|
52
|
+
* The directory holding every file this server writes: a per-user data
|
|
53
|
+
* directory (`%LOCALAPPDATA%\4da-mcp`, `~/Library/Application Support/4da-mcp`,
|
|
54
|
+
* `$XDG_DATA_HOME/4da-mcp`). FOURDA_MCP_HOME overrides it (tests use a temp dir).
|
|
55
|
+
*/
|
|
56
|
+
export declare function serverDataDir(): string;
|
|
57
|
+
/** The server's response cache (registry, OSV, changelogs). Disposable: deleting it costs refetches only. */
|
|
58
|
+
export declare function serverCacheDbPath(): string;
|
|
33
59
|
/**
|
|
34
60
|
* Whether an error says the database file itself is unreadable: not SQLite at
|
|
35
|
-
* all, or damaged.
|
|
61
|
+
* all, or damaged. SQLite reports these as SQLITE_NOTADB ("file is not
|
|
36
62
|
* a database") and SQLITE_CORRUPT ("database disk image is malformed").
|
|
37
63
|
*/
|
|
38
64
|
export declare function isUnreadableDbError(error: unknown): boolean;
|
|
@@ -105,6 +131,20 @@ export declare function dedupeByTitle<T extends {
|
|
|
105
131
|
* the `is_direct` standalone-schema gap slipped through precisely because the
|
|
106
132
|
* two modes were only ever tested separately.
|
|
107
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* The app-owned tables this server is designed to write when the desktop app
|
|
136
|
+
* is installed: decisions (decision_memory), agent memory (agent_memory) and
|
|
137
|
+
* feedback (record_feedback -> the app's calibration). Nothing else in the
|
|
138
|
+
* app's database is ever written, and nothing in it is created or altered.
|
|
139
|
+
*/
|
|
140
|
+
export declare const APP_WRITABLE_TABLES: readonly ["developer_decisions", "agent_memory", "interactions", "feedback"];
|
|
141
|
+
export type AppWritableTable = (typeof APP_WRITABLE_TABLES)[number];
|
|
142
|
+
export interface WriteTarget {
|
|
143
|
+
raw: SqliteDatabase;
|
|
144
|
+
/** database = the open (standalone) database; app = the app's table; server_store = fallback on schema drift. */
|
|
145
|
+
target: "database" | "app" | "server_store";
|
|
146
|
+
note?: string;
|
|
147
|
+
}
|
|
108
148
|
export declare const DEPENDENCY_GROUP_QUERY = "SELECT DISTINCT package_name, language, project_path, is_dev, is_direct FROM project_dependencies";
|
|
109
149
|
/**
|
|
110
150
|
* 4DA Database accessor
|
|
@@ -120,6 +160,30 @@ export declare class FourDADatabase {
|
|
|
120
160
|
* (no pre-existing 4DA desktop app database found).
|
|
121
161
|
*/
|
|
122
162
|
get isStandalone(): boolean;
|
|
163
|
+
/** True for the desktop app's database, which this connection only reads. */
|
|
164
|
+
get isReadOnly(): boolean;
|
|
165
|
+
private memoryStore;
|
|
166
|
+
private appWriter;
|
|
167
|
+
/**
|
|
168
|
+
* The connection a record into one of the app-owned tables this server is
|
|
169
|
+
* designed to write (APP_WRITABLE_TABLES) goes through, and where it lands.
|
|
170
|
+
*
|
|
171
|
+
* - Standalone database: the database itself.
|
|
172
|
+
* - The desktop app's database: a SEPARATE writable connection, opened on
|
|
173
|
+
* first use for these statements only (the main connection stays
|
|
174
|
+
* read-only), with a busy timeout so a write waits briefly for the app
|
|
175
|
+
* instead of failing. Only when the app's table has every column the
|
|
176
|
+
* statement names; nothing is ever created or altered in the app's file.
|
|
177
|
+
* - Schema drift (table or column missing in the app): the record goes to
|
|
178
|
+
* this server's own store (standaloneDbPath()) and `note` says so.
|
|
179
|
+
*/
|
|
180
|
+
writerFor(table: AppWritableTable, columns: readonly string[]): WriteTarget;
|
|
181
|
+
/**
|
|
182
|
+
* The server's own store: its standalone database (standaloneDbPath()).
|
|
183
|
+
* Used for records the app's schema cannot hold (see writerFor). A
|
|
184
|
+
* standalone database is its own store.
|
|
185
|
+
*/
|
|
186
|
+
getMemoryStore(): FourDADatabase;
|
|
123
187
|
/**
|
|
124
188
|
* Validate that a database file exists and is a readable SQLite database.
|
|
125
189
|
*
|
|
@@ -128,7 +192,7 @@ export declare class FourDADatabase {
|
|
|
128
192
|
* By default this runs a CHEAP probe (open + list tables, ~0.1s) which surfaces
|
|
129
193
|
* a missing, locked, or corrupt-header file. It deliberately does NOT run a full
|
|
130
194
|
* `PRAGMA integrity_check`: that is an O(database-size) scan (~11s on a 1.7 GB DB,
|
|
131
|
-
* and growing) and, because
|
|
195
|
+
* and growing) and, because the SQLite drivers are synchronous, it blocks the event
|
|
132
196
|
* loop — running it on the startup path stalls the MCP stdio handshake until it
|
|
133
197
|
* finishes, so the host's connect timeout can fire and the server looks like it
|
|
134
198
|
* "failed to start". Pass `{ deep: true }` for the full integrity_check on the
|
|
@@ -142,9 +206,9 @@ export declare class FourDADatabase {
|
|
|
142
206
|
deep?: boolean;
|
|
143
207
|
}): DatabaseValidationResult;
|
|
144
208
|
/**
|
|
145
|
-
* Get the raw
|
|
209
|
+
* Get the raw database connection (sqlite-driver.ts) for custom queries
|
|
146
210
|
*/
|
|
147
|
-
getRawDb():
|
|
211
|
+
getRawDb(): SqliteDatabase;
|
|
148
212
|
/**
|
|
149
213
|
* Close the database connection
|
|
150
214
|
*/
|
|
@@ -192,6 +256,7 @@ export declare class FourDADatabase {
|
|
|
192
256
|
getFreshness(): DataFreshness;
|
|
193
257
|
/** `MAX(sources.last_fetch)`, tolerant of a missing `sources` table (returns null). */
|
|
194
258
|
private safeMaxLastFetch;
|
|
259
|
+
hasTable(table: string): boolean;
|
|
195
260
|
hasColumn(table: string, column: string): boolean;
|
|
196
261
|
/**
|
|
197
262
|
* Idempotently add a column to a table if it does not already exist.
|
package/dist/db.js
CHANGED
|
@@ -2,24 +2,35 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Database module for 4DA MCP Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The desktop app's database (`4da.db`) is opened READ-ONLY
|
|
6
|
+
* (`{ readonly: true, fileMustExist: true }`) for everything the server reads.
|
|
7
|
+
* The app owns that file and its migrations: the server never creates or
|
|
8
|
+
* alters anything in it. It writes rows into exactly the app-owned tables its
|
|
9
|
+
* tools exist to write (APP_WRITABLE_TABLES: decisions, agent memory, feedback
|
|
10
|
+
* and interactions, which the app's calibration and decision UI read), through
|
|
11
|
+
* a separate write connection opened only for those statements (writerFor).
|
|
12
|
+
*
|
|
13
|
+
* Everything else the server writes goes to files it owns, under
|
|
14
|
+
* serverDataDir():
|
|
15
|
+
* - `cache.db`: registry, OSV and changelog responses (LiveCache).
|
|
16
|
+
* - `standalone.db`: the standalone database when no app is installed, and
|
|
17
|
+
* the fallback for a record the app's schema cannot hold (schema drift).
|
|
18
|
+
*
|
|
19
|
+
* Until 6.0.2 the server created a `live_cache` table inside the app's
|
|
20
|
+
* database (2,774 rows on one machine, 2026-10-07) and added `embedding`
|
|
21
|
+
* columns to two app tables when an embedding provider was set.
|
|
7
22
|
*/
|
|
8
23
|
import path from "path";
|
|
9
24
|
import * as fs from "fs";
|
|
10
25
|
import * as os from "os";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.error(`\n [4DA] ${nativeBindingMessage(err)}\n`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
/** Null when better-sqlite3 can open a database here; otherwise why not, with the fix. */
|
|
26
|
+
import { openDatabase, sqliteDriverStatus, SqliteUnavailableError, } from "./sqlite-driver.js";
|
|
27
|
+
import { isNativeBindingError, nativeBindingMessage } from "./native-bindings.js";
|
|
28
|
+
/**
|
|
29
|
+
* Null when a SQLite driver (node:sqlite, or better-sqlite3 with a working
|
|
30
|
+
* native module) can open a database here; otherwise why not, with the fix.
|
|
31
|
+
*/
|
|
21
32
|
export function nativeBindingProblem() {
|
|
22
|
-
return
|
|
33
|
+
return sqliteDriverStatus().problem;
|
|
23
34
|
}
|
|
24
35
|
import { dbRecoveryNote, readDbRecoveredMarker, readEngineBlockMarker } from "./freshness-markers.js";
|
|
25
36
|
/**
|
|
@@ -93,17 +104,32 @@ export function desktopAppDbPaths(platform = process.platform, env = process.env
|
|
|
93
104
|
* in, so agent memory and decisions did not follow the user between projects.
|
|
94
105
|
*/
|
|
95
106
|
export function standaloneDbPath() {
|
|
107
|
+
return path.join(serverDataDir(), "standalone.db");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The directory holding every file this server writes: a per-user data
|
|
111
|
+
* directory (`%LOCALAPPDATA%\4da-mcp`, `~/Library/Application Support/4da-mcp`,
|
|
112
|
+
* `$XDG_DATA_HOME/4da-mcp`). FOURDA_MCP_HOME overrides it (tests use a temp dir).
|
|
113
|
+
*/
|
|
114
|
+
export function serverDataDir() {
|
|
115
|
+
const override = process.env.FOURDA_MCP_HOME?.trim();
|
|
116
|
+
if (override)
|
|
117
|
+
return override;
|
|
96
118
|
const home = os.homedir();
|
|
97
119
|
const base = process.platform === "win32"
|
|
98
120
|
? process.env.LOCALAPPDATA || path.join(home, "AppData", "Local")
|
|
99
121
|
: process.platform === "darwin"
|
|
100
122
|
? path.join(home, "Library", "Application Support")
|
|
101
123
|
: process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
102
|
-
return path.join(base, "4da-mcp"
|
|
124
|
+
return path.join(base, "4da-mcp");
|
|
125
|
+
}
|
|
126
|
+
/** The server's response cache (registry, OSV, changelogs). Disposable: deleting it costs refetches only. */
|
|
127
|
+
export function serverCacheDbPath() {
|
|
128
|
+
return path.join(serverDataDir(), "cache.db");
|
|
103
129
|
}
|
|
104
130
|
/**
|
|
105
131
|
* Whether an error says the database file itself is unreadable: not SQLite at
|
|
106
|
-
* all, or damaged.
|
|
132
|
+
* all, or damaged. SQLite reports these as SQLITE_NOTADB ("file is not
|
|
107
133
|
* a database") and SQLITE_CORRUPT ("database disk image is malformed").
|
|
108
134
|
*/
|
|
109
135
|
export function isUnreadableDbError(error) {
|
|
@@ -222,6 +248,15 @@ export function dedupeByTitle(items, threshold = 0.6) {
|
|
|
222
248
|
* the `is_direct` standalone-schema gap slipped through precisely because the
|
|
223
249
|
* two modes were only ever tested separately.
|
|
224
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* The app-owned tables this server is designed to write when the desktop app
|
|
253
|
+
* is installed: decisions (decision_memory), agent memory (agent_memory) and
|
|
254
|
+
* feedback (record_feedback -> the app's calibration). Nothing else in the
|
|
255
|
+
* app's database is ever written, and nothing in it is created or altered.
|
|
256
|
+
*/
|
|
257
|
+
export const APP_WRITABLE_TABLES = ["developer_decisions", "agent_memory", "interactions", "feedback"];
|
|
258
|
+
/** How long a write into the app's database waits for the app's own lock. */
|
|
259
|
+
const APP_WRITE_BUSY_TIMEOUT_MS = 3000;
|
|
225
260
|
export const DEPENDENCY_GROUP_QUERY = "SELECT DISTINCT package_name, language, project_path, is_dev, is_direct FROM project_dependencies";
|
|
226
261
|
/**
|
|
227
262
|
* 4DA Database accessor
|
|
@@ -246,9 +281,26 @@ export class FourDADatabase {
|
|
|
246
281
|
}
|
|
247
282
|
}
|
|
248
283
|
let opened = null;
|
|
284
|
+
let standalone = isNew;
|
|
249
285
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
286
|
+
if (isNew) {
|
|
287
|
+
opened = openDatabase(absolutePath, { readonly: false });
|
|
288
|
+
opened.pragma("journal_mode = WAL");
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
// An existing file is opened read-only first. Reading the schema here
|
|
292
|
+
// makes a damaged or non-SQLite file fail now, with the message below.
|
|
293
|
+
opened = openDatabase(absolutePath, { readonly: true, fileMustExist: true });
|
|
294
|
+
opened.prepare("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1").all();
|
|
295
|
+
standalone = detectStandalone(opened);
|
|
296
|
+
if (standalone) {
|
|
297
|
+
// This server's own database: reopen it writable.
|
|
298
|
+
opened.close();
|
|
299
|
+
opened = null;
|
|
300
|
+
opened = openDatabase(absolutePath, { readonly: false, fileMustExist: true });
|
|
301
|
+
opened.pragma("journal_mode = WAL");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
252
304
|
}
|
|
253
305
|
catch (error) {
|
|
254
306
|
// Close the handle a failed pragma leaves open: every tool call retries
|
|
@@ -260,6 +312,8 @@ export class FourDADatabase {
|
|
|
260
312
|
catch {
|
|
261
313
|
// Nothing to close when the open itself failed.
|
|
262
314
|
}
|
|
315
|
+
if (error instanceof SqliteUnavailableError)
|
|
316
|
+
throw error;
|
|
263
317
|
throw new Error(isNativeBindingError(error)
|
|
264
318
|
? nativeBindingMessage(error)
|
|
265
319
|
: isUnreadableDbError(error)
|
|
@@ -268,25 +322,22 @@ export class FourDADatabase {
|
|
|
268
322
|
}
|
|
269
323
|
this.db = opened;
|
|
270
324
|
this.dbPath = absolutePath;
|
|
325
|
+
this._isStandalone = standalone;
|
|
326
|
+
// The desktop app's database: read-only, and nothing below runs against it.
|
|
327
|
+
if (!standalone)
|
|
328
|
+
return;
|
|
271
329
|
// Standalone mode: create schema for a brand-new database, and recognise
|
|
272
330
|
// an existing standalone database as one on every later session.
|
|
273
|
-
if (isNew)
|
|
331
|
+
if (isNew)
|
|
274
332
|
this.createMinimalSchema();
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
this._isStandalone = detectStandalone(this.db);
|
|
279
|
-
}
|
|
280
|
-
if (this._isStandalone) {
|
|
281
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS ${STANDALONE_MARKER} (created_at TEXT DEFAULT (datetime('now')))`);
|
|
282
|
-
}
|
|
333
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS ${STANDALONE_MARKER} (created_at TEXT DEFAULT (datetime('now')))`);
|
|
283
334
|
// Schema upgrade for standalone databases created before `is_direct` was
|
|
284
335
|
// added to the minimal schema: the full-DB init branch queries that column
|
|
285
336
|
// (DEPENDENCY_GROUP_QUERY), and without it session 2+ of a standalone
|
|
286
337
|
// install throws, gets caught, and silently disables vulnerability_scan /
|
|
287
338
|
// dependency_health / upgrade_planner. Scanner-inserted manifest deps are
|
|
288
|
-
// direct by definition, so DEFAULT 1 backfills correctly.
|
|
289
|
-
// app's database
|
|
339
|
+
// direct by definition, so DEFAULT 1 backfills correctly. Standalone
|
|
340
|
+
// databases only: the desktop app's database is never altered.
|
|
290
341
|
try {
|
|
291
342
|
const hasDepsTable = this.db
|
|
292
343
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='project_dependencies'")
|
|
@@ -306,6 +357,67 @@ export class FourDADatabase {
|
|
|
306
357
|
get isStandalone() {
|
|
307
358
|
return this._isStandalone;
|
|
308
359
|
}
|
|
360
|
+
/** True for the desktop app's database, which this connection only reads. */
|
|
361
|
+
get isReadOnly() {
|
|
362
|
+
return this.db.readonly;
|
|
363
|
+
}
|
|
364
|
+
memoryStore = null;
|
|
365
|
+
appWriter = null;
|
|
366
|
+
/**
|
|
367
|
+
* The connection a record into one of the app-owned tables this server is
|
|
368
|
+
* designed to write (APP_WRITABLE_TABLES) goes through, and where it lands.
|
|
369
|
+
*
|
|
370
|
+
* - Standalone database: the database itself.
|
|
371
|
+
* - The desktop app's database: a SEPARATE writable connection, opened on
|
|
372
|
+
* first use for these statements only (the main connection stays
|
|
373
|
+
* read-only), with a busy timeout so a write waits briefly for the app
|
|
374
|
+
* instead of failing. Only when the app's table has every column the
|
|
375
|
+
* statement names; nothing is ever created or altered in the app's file.
|
|
376
|
+
* - Schema drift (table or column missing in the app): the record goes to
|
|
377
|
+
* this server's own store (standaloneDbPath()) and `note` says so.
|
|
378
|
+
*/
|
|
379
|
+
writerFor(table, columns) {
|
|
380
|
+
if (!this.isReadOnly)
|
|
381
|
+
return { raw: this.db, target: "database" };
|
|
382
|
+
const missing = columns.filter((c) => !this.hasColumn(table, c));
|
|
383
|
+
if (missing.length === 0) {
|
|
384
|
+
if (!this.appWriter?.open) {
|
|
385
|
+
const writer = openDatabase(this.dbPath, { fileMustExist: true });
|
|
386
|
+
writer.pragma(`busy_timeout = ${APP_WRITE_BUSY_TIMEOUT_MS}`);
|
|
387
|
+
this.appWriter = writer;
|
|
388
|
+
}
|
|
389
|
+
return { raw: this.appWriter, target: "app" };
|
|
390
|
+
}
|
|
391
|
+
const store = this.getMemoryStore();
|
|
392
|
+
return {
|
|
393
|
+
raw: store.db,
|
|
394
|
+
target: "server_store",
|
|
395
|
+
note: `The desktop app's ${table} table ${this.hasTable(table) ? `has no ${missing.join(", ")} column` : "does not exist"} ` +
|
|
396
|
+
`in this app version, so this was recorded in the server's own store (${store.dbPath}) instead; the app will not see it. ` +
|
|
397
|
+
`The server never changes the app's schema.`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The server's own store: its standalone database (standaloneDbPath()).
|
|
402
|
+
* Used for records the app's schema cannot hold (see writerFor). A
|
|
403
|
+
* standalone database is its own store.
|
|
404
|
+
*/
|
|
405
|
+
getMemoryStore() {
|
|
406
|
+
if (!this.isReadOnly)
|
|
407
|
+
return this;
|
|
408
|
+
if (!this.memoryStore) {
|
|
409
|
+
const storePath = standaloneDbPath();
|
|
410
|
+
if (path.resolve(storePath) === path.resolve(this.dbPath)) {
|
|
411
|
+
throw new Error(`The server's store ${storePath} is open read-only; it cannot record.`);
|
|
412
|
+
}
|
|
413
|
+
const store = new FourDADatabase(storePath);
|
|
414
|
+
// Rows here reference the app's items and decisions by id; those rows
|
|
415
|
+
// live in the other file, so foreign keys cannot be enforced here.
|
|
416
|
+
store.db.pragma("foreign_keys = OFF");
|
|
417
|
+
this.memoryStore = store;
|
|
418
|
+
}
|
|
419
|
+
return this.memoryStore;
|
|
420
|
+
}
|
|
309
421
|
/**
|
|
310
422
|
* Validate that a database file exists and is a readable SQLite database.
|
|
311
423
|
*
|
|
@@ -314,7 +426,7 @@ export class FourDADatabase {
|
|
|
314
426
|
* By default this runs a CHEAP probe (open + list tables, ~0.1s) which surfaces
|
|
315
427
|
* a missing, locked, or corrupt-header file. It deliberately does NOT run a full
|
|
316
428
|
* `PRAGMA integrity_check`: that is an O(database-size) scan (~11s on a 1.7 GB DB,
|
|
317
|
-
* and growing) and, because
|
|
429
|
+
* and growing) and, because the SQLite drivers are synchronous, it blocks the event
|
|
318
430
|
* loop — running it on the startup path stalls the MCP stdio handshake until it
|
|
319
431
|
* finishes, so the host's connect timeout can fire and the server looks like it
|
|
320
432
|
* "failed to start". Pass `{ deep: true }` for the full integrity_check on the
|
|
@@ -339,7 +451,7 @@ export class FourDADatabase {
|
|
|
339
451
|
}
|
|
340
452
|
let testDb = null;
|
|
341
453
|
try {
|
|
342
|
-
testDb =
|
|
454
|
+
testDb = openDatabase(absolutePath, { readonly: true, fileMustExist: true });
|
|
343
455
|
// Deep, opt-in integrity check (slow: full-database scan). Only on --doctor.
|
|
344
456
|
if (opts?.deep) {
|
|
345
457
|
const integrityResult = testDb.pragma("integrity_check");
|
|
@@ -384,7 +496,7 @@ export class FourDADatabase {
|
|
|
384
496
|
}
|
|
385
497
|
}
|
|
386
498
|
/**
|
|
387
|
-
* Get the raw
|
|
499
|
+
* Get the raw database connection (sqlite-driver.ts) for custom queries
|
|
388
500
|
*/
|
|
389
501
|
getRawDb() {
|
|
390
502
|
return this.db;
|
|
@@ -393,6 +505,11 @@ export class FourDADatabase {
|
|
|
393
505
|
* Close the database connection
|
|
394
506
|
*/
|
|
395
507
|
close() {
|
|
508
|
+
this.memoryStore?.close();
|
|
509
|
+
this.memoryStore = null;
|
|
510
|
+
if (this.appWriter?.open)
|
|
511
|
+
this.appWriter.close();
|
|
512
|
+
this.appWriter = null;
|
|
396
513
|
this.db.close();
|
|
397
514
|
}
|
|
398
515
|
// ===========================================================================
|
|
@@ -908,6 +1025,14 @@ export class FourDADatabase {
|
|
|
908
1025
|
return null;
|
|
909
1026
|
}
|
|
910
1027
|
}
|
|
1028
|
+
hasTable(table) {
|
|
1029
|
+
try {
|
|
1030
|
+
return this.db.pragma(`table_info(${table})`).length > 0;
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
911
1036
|
hasColumn(table, column) {
|
|
912
1037
|
try {
|
|
913
1038
|
const cols = this.db.pragma(`table_info(${table})`);
|
|
@@ -926,6 +1051,9 @@ export class FourDADatabase {
|
|
|
926
1051
|
ensureColumn(table, column, type) {
|
|
927
1052
|
if (this.hasColumn(table, column))
|
|
928
1053
|
return;
|
|
1054
|
+
if (this.isReadOnly) {
|
|
1055
|
+
throw new Error(`${this.dbPath} is the desktop app's database, which this server never alters (${table}.${column}).`);
|
|
1056
|
+
}
|
|
929
1057
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
930
1058
|
}
|
|
931
1059
|
/**
|
|
@@ -1274,29 +1402,38 @@ export class FourDADatabase {
|
|
|
1274
1402
|
};
|
|
1275
1403
|
const label = relevanceLabel[action];
|
|
1276
1404
|
try {
|
|
1277
|
-
|
|
1405
|
+
// Into the app's own interactions/feedback tables (the app's calibration
|
|
1406
|
+
// reads them) through the separate write connection; on schema drift,
|
|
1407
|
+
// into the server's store with a note. See writerFor.
|
|
1408
|
+
if (!this.isReadOnly && label !== undefined)
|
|
1278
1409
|
this.ensureFeedbackTable();
|
|
1279
|
-
const
|
|
1410
|
+
const interactionsTo = this.writerFor("interactions", ["item_id", "action_type", "item_source", "signal_strength", "timestamp"]);
|
|
1411
|
+
const feedbackTo = label !== undefined ? this.writerFor("feedback", ["source_item_id", "relevant"]) : null;
|
|
1412
|
+
if (interactionsTo.target === "server_store" || feedbackTo?.target === "server_store") {
|
|
1413
|
+
this.getMemoryStore().ensureFeedbackTable();
|
|
1414
|
+
}
|
|
1415
|
+
const insertInteraction = interactionsTo.raw.prepare(`
|
|
1280
1416
|
INSERT INTO interactions (item_id, action_type, item_source, signal_strength, timestamp)
|
|
1281
1417
|
VALUES (?, ?, ?, ?, datetime('now'))
|
|
1282
1418
|
`);
|
|
1283
|
-
const
|
|
1419
|
+
const insertFeedback = feedbackTo?.raw.prepare(`INSERT INTO feedback (source_item_id, relevant) VALUES (?, ?)`);
|
|
1420
|
+
// One short transaction when both rows go to the same file.
|
|
1421
|
+
const write = () => {
|
|
1284
1422
|
const r = insertInteraction.run(itemId, action, row.source_type, signalStrength[action]);
|
|
1285
|
-
|
|
1286
|
-
this.db
|
|
1287
|
-
.prepare(`INSERT INTO feedback (source_item_id, relevant) VALUES (?, ?)`)
|
|
1288
|
-
.run(itemId, label);
|
|
1289
|
-
}
|
|
1423
|
+
insertFeedback?.run(itemId, label);
|
|
1290
1424
|
return r;
|
|
1291
|
-
}
|
|
1292
|
-
const result = write();
|
|
1425
|
+
};
|
|
1426
|
+
const result = feedbackTo && feedbackTo.raw !== interactionsTo.raw ? write() : interactionsTo.raw.transaction(write)();
|
|
1427
|
+
const notes = [interactionsTo.note, feedbackTo?.note].filter((n) => Boolean(n));
|
|
1428
|
+
const where = notes.length > 0 ? ` (${[...new Set(notes)].join(" ")})` : "";
|
|
1293
1429
|
return {
|
|
1294
1430
|
success: true,
|
|
1295
|
-
message: label === undefined
|
|
1431
|
+
message: (label === undefined
|
|
1296
1432
|
? `Recorded ${action} feedback for item ${itemId}`
|
|
1297
|
-
: `Recorded ${action} feedback for item ${itemId} (relevance label: ${label === 1 ? "relevant" : "not relevant"})
|
|
1433
|
+
: `Recorded ${action} feedback for item ${itemId} (relevance label: ${label === 1 ? "relevant" : "not relevant"})`) + where,
|
|
1298
1434
|
interaction_id: result.lastInsertRowid,
|
|
1299
1435
|
relevance_label: label === undefined ? null : label === 1,
|
|
1436
|
+
...(notes.length > 0 ? { _meta: { written_to: "server_store", note: [...new Set(notes)].join(" ") } } : {}),
|
|
1300
1437
|
};
|
|
1301
1438
|
}
|
|
1302
1439
|
catch (error) {
|
package/dist/doctor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Validates that the MCP server can run correctly:
|
|
5
5
|
* 1. Node.js version is sufficient
|
|
6
|
-
* 2.
|
|
6
|
+
* 2. A SQLite driver opens a database (node:sqlite, else better-sqlite3)
|
|
7
7
|
* 3. Database is found and readable
|
|
8
8
|
* 4. LLM providers are configured (optional)
|
|
9
9
|
*
|
package/dist/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Validates that the MCP server can run correctly:
|
|
6
6
|
* 1. Node.js version is sufficient
|
|
7
|
-
* 2.
|
|
7
|
+
* 2. A SQLite driver opens a database (node:sqlite, else better-sqlite3)
|
|
8
8
|
* 3. Database is found and readable
|
|
9
9
|
* 4. LLM providers are configured (optional)
|
|
10
10
|
*
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* 4da-mcp --doctor
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
|
-
import { FourDADatabase
|
|
16
|
+
import { FourDADatabase } from "./db.js";
|
|
17
|
+
import { NODE_SQLITE_MIN, sqliteDriverStatus } from "./sqlite-driver.js";
|
|
17
18
|
import { checkBuildStaleness } from "./build-staleness.js";
|
|
18
19
|
export function runDoctor() {
|
|
19
20
|
console.log("\n 4DA MCP Server — Doctor\n");
|
|
@@ -32,14 +33,19 @@ export function runDoctor() {
|
|
|
32
33
|
? `${nodeVersion} (>= ${nodeRequired} required)`
|
|
33
34
|
: `${nodeVersion} — Node.js ${nodeRequired}+ required`,
|
|
34
35
|
});
|
|
35
|
-
// 2.
|
|
36
|
-
//
|
|
37
|
-
// install script left none, and this check used to say "loaded").
|
|
38
|
-
const
|
|
36
|
+
// 2. SQLite driver — opening a database, not just importing a package:
|
|
37
|
+
// better-sqlite3's import succeeds without its compiled module (npm 12
|
|
38
|
+
// blocking its install script left none, and this check used to say "loaded").
|
|
39
|
+
const sqlite = sqliteDriverStatus();
|
|
40
|
+
const passedOver = sqlite.rejected.map((r) => `${r.name} not used: ${r.reason}`).join("; ");
|
|
39
41
|
checks.push({
|
|
40
|
-
name: "SQLite
|
|
41
|
-
status:
|
|
42
|
-
detail:
|
|
42
|
+
name: "SQLite driver",
|
|
43
|
+
status: sqlite.driver ? "pass" : "fail",
|
|
44
|
+
detail: !sqlite.driver
|
|
45
|
+
? (sqlite.problem ?? "no SQLite driver")
|
|
46
|
+
: sqlite.driver.name === "node:sqlite"
|
|
47
|
+
? `node:sqlite (built into Node ${process.versions.node}) opened an in-memory database`
|
|
48
|
+
: `better-sqlite3 opened an in-memory database (${passedOver || `node:sqlite needs Node ${NODE_SQLITE_MIN}+`})`,
|
|
43
49
|
});
|
|
44
50
|
// 3. Database discovery — deep integrity_check is appropriate here: --doctor is
|
|
45
51
|
// an explicit, user-invoked diagnostic where a slow full scan is expected. The
|
package/dist/embeddings.js
CHANGED
|
@@ -238,12 +238,24 @@ export async function semanticScores(db, table, query, items, config, opts = {})
|
|
|
238
238
|
if (!queryVec) {
|
|
239
239
|
return { semanticById: new Map(), queryEmbedded: false, embeddedCount: 0, newlyEmbedded: 0, model: tag };
|
|
240
240
|
}
|
|
241
|
-
// Cache columns are optional
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
241
|
+
// Cache columns are optional. This server's own database gets them lazily;
|
|
242
|
+
// the desktop app's database is never altered, so there the cache is used
|
|
243
|
+
// only when the app's table already has both columns (written through the
|
|
244
|
+
// separate write connection, see FourDADatabase.writerFor), and vectors are
|
|
245
|
+
// otherwise computed per call and not stored.
|
|
246
|
+
const readOnly = db.isReadOnly === true;
|
|
247
|
+
let cacheDb = null;
|
|
248
|
+
if (!readOnly) {
|
|
249
|
+
db.ensureColumn(table, "embedding", "BLOB");
|
|
250
|
+
db.ensureColumn(table, "embedding_model", "TEXT");
|
|
251
|
+
cacheDb = db.getRawDb();
|
|
252
|
+
}
|
|
253
|
+
else if (db.hasColumn(table, "embedding") && db.hasColumn(table, "embedding_model")) {
|
|
254
|
+
const to = db.writerFor(table, ["id", "embedding", "embedding_model"]);
|
|
255
|
+
cacheDb = to.target === "app" ? to.raw : null;
|
|
256
|
+
}
|
|
257
|
+
const stored = cacheDb ? loadStoredEmbeddings(db.getRawDb(), table, items.map((i) => i.id), tag) : new Map();
|
|
258
|
+
const updateStmt = cacheDb?.prepare(`UPDATE ${table} SET embedding = ?, embedding_model = ? WHERE id = ?`);
|
|
247
259
|
const maxNew = opts.maxNewEmbeds ?? DEFAULT_MAX_NEW_EMBEDS;
|
|
248
260
|
const semanticById = new Map();
|
|
249
261
|
let embeddedCount = 0;
|
|
@@ -254,7 +266,7 @@ export async function semanticScores(db, table, query, items, config, opts = {})
|
|
|
254
266
|
vec = await embedText(item.text, config);
|
|
255
267
|
if (vec) {
|
|
256
268
|
try {
|
|
257
|
-
updateStmt
|
|
269
|
+
updateStmt?.run(vectorToBlob(vec), tag, item.id);
|
|
258
270
|
}
|
|
259
271
|
catch {
|
|
260
272
|
// Persisting the cache is best-effort; scoring still proceeds.
|
package/dist/gitignore.js
CHANGED
|
@@ -116,9 +116,15 @@ export class IgnoreRules {
|
|
|
116
116
|
if (rule.base && !relPath.startsWith(`${rule.base}/`))
|
|
117
117
|
continue;
|
|
118
118
|
const local = rule.base ? relPath.slice(rule.base.length + 1) : relPath;
|
|
119
|
-
if (rule.dirOnly && !isDir
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
if (rule.dirOnly && !isDir) {
|
|
120
|
+
// A `dir/` rule ignores a file only through a directory above it:
|
|
121
|
+
// `docs/api_reference/*/` matched docs/api_reference/requirements.txt
|
|
122
|
+
// itself, so langchain's docs requirements were never read.
|
|
123
|
+
const parent = local.includes("/") ? local.slice(0, local.lastIndexOf("/")) : null;
|
|
124
|
+
if (parent === null || !rule.regex.test(parent))
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
else if (!rule.regex.test(local))
|
|
122
128
|
continue;
|
|
123
129
|
if (rule.negated)
|
|
124
130
|
reincludable = true;
|