@4da/mcp-server 6.0.2 → 6.1.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 +265 -0
- package/README.md +13 -10
- 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.js +4 -1
- package/dist/live/changelog.d.ts +15 -0
- package/dist/live/changelog.js +67 -11
- 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/go-registry.d.ts +5 -0
- package/dist/live/go-registry.js +1 -1
- package/dist/live/index.d.ts +30 -3
- package/dist/live/index.js +59 -6
- 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 +79 -0
- package/dist/live/osv-ranges.js +133 -0
- package/dist/live/osv-scanner.d.ts +1 -0
- package/dist/live/osv-scanner.js +85 -78
- 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/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-sources.d.ts +2 -0
- package/dist/live/upgrade-sources.js +7 -2
- 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 +2 -2
- 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 +7 -0
- package/dist/tools/dependency-check-signals.js +17 -2
- package/dist/tools/dependency-check.d.ts +1 -1
- package/dist/tools/dependency-check.js +1 -1
- 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-literals.js +1 -1
- package/dist/tools/upgrade-impact-report.d.ts +10 -0
- package/dist/tools/upgrade-impact-report.js +32 -2
- package/dist/tools/upgrade-impact.js +1 -1
- 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 +121 -11
- package/package.json +5 -27
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The SQLite driver: Node's built-in `node:sqlite` when the runtime has it,
|
|
4
|
+
* otherwise better-sqlite3, behind one interface.
|
|
5
|
+
*
|
|
6
|
+
* Why: npm 12 blocks dependency install scripts unless allowed, so a default
|
|
7
|
+
* `npx @4da/mcp-server` or `npm install` left better-sqlite3's native module
|
|
8
|
+
* unbuilt and every tool failed (measured 2026-10-03 on npm 12.2.0, see
|
|
9
|
+
* native-bindings.ts). `node:sqlite` ships inside Node: no install script, no
|
|
10
|
+
* native download. It is available without a flag from Node 22.13.0 (and
|
|
11
|
+
* 23.4.0); npm 12 itself requires Node ^22.22.2 || ^24.15.0 || >=26, so every
|
|
12
|
+
* npm 12 install has it. better-sqlite3 is an optional dependency now, used on
|
|
13
|
+
* Node 22.0-22.12 where it builds (npm 10 and 11 run its install script).
|
|
14
|
+
*
|
|
15
|
+
* The interface is the subset of better-sqlite3 this server uses: open
|
|
16
|
+
* options `readonly` / `fileMustExist`, prepare().get/all/run (positional
|
|
17
|
+
* parameters, arrays flattened, or one object of named parameters), exec,
|
|
18
|
+
* pragma (incl. `{ simple: true }`), transaction (nested calls become
|
|
19
|
+
* savepoints), close, and the `open` / `readonly` / `name` properties.
|
|
20
|
+
* better-sqlite3's Database satisfies it as is; node:sqlite is adapted
|
|
21
|
+
* (NodeSqliteDatabase) so that rows, BLOBs, error codes and the 5 s busy
|
|
22
|
+
* timeout match what the server's code already expects.
|
|
23
|
+
*
|
|
24
|
+
* `FOURDA_SQLITE_DRIVER=node|better` forces one driver (tests, diagnostics):
|
|
25
|
+
* a forced driver that cannot load is reported, never silently replaced.
|
|
26
|
+
* `none` simulates a host where neither works.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from "node:fs";
|
|
29
|
+
import { createRequire } from "node:module";
|
|
30
|
+
import { checkNativeBindings, NATIVE_BINDING_FIX } from "./native-bindings.js";
|
|
31
|
+
/** Thrown by sqliteDriver() when neither driver can open a database here. */
|
|
32
|
+
export class SqliteUnavailableError extends Error {
|
|
33
|
+
code = "FOURDA_NO_SQLITE";
|
|
34
|
+
}
|
|
35
|
+
/** The first Node release with `node:sqlite` unflagged on the 22 line. */
|
|
36
|
+
export const NODE_SQLITE_MIN = "22.13.0";
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// node:sqlite adapter
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/** Primary and extended SQLite result codes, by number, as better-sqlite3 names them. */
|
|
41
|
+
const SQLITE_CODES = {
|
|
42
|
+
1: "SQLITE_ERROR", 2: "SQLITE_INTERNAL", 3: "SQLITE_PERM", 4: "SQLITE_ABORT", 5: "SQLITE_BUSY",
|
|
43
|
+
6: "SQLITE_LOCKED", 7: "SQLITE_NOMEM", 8: "SQLITE_READONLY", 9: "SQLITE_INTERRUPT", 10: "SQLITE_IOERR",
|
|
44
|
+
11: "SQLITE_CORRUPT", 12: "SQLITE_NOTFOUND", 13: "SQLITE_FULL", 14: "SQLITE_CANTOPEN", 15: "SQLITE_PROTOCOL",
|
|
45
|
+
16: "SQLITE_EMPTY", 17: "SQLITE_SCHEMA", 18: "SQLITE_TOOBIG", 19: "SQLITE_CONSTRAINT", 20: "SQLITE_MISMATCH",
|
|
46
|
+
21: "SQLITE_MISUSE", 22: "SQLITE_NOLFS", 23: "SQLITE_AUTH", 24: "SQLITE_FORMAT", 25: "SQLITE_RANGE",
|
|
47
|
+
26: "SQLITE_NOTADB",
|
|
48
|
+
261: "SQLITE_BUSY_RECOVERY", 517: "SQLITE_BUSY_SNAPSHOT", 773: "SQLITE_BUSY_TIMEOUT",
|
|
49
|
+
262: "SQLITE_LOCKED_SHAREDCACHE", 264: "SQLITE_READONLY_RECOVERY", 520: "SQLITE_READONLY_CANTLOCK",
|
|
50
|
+
776: "SQLITE_READONLY_ROLLBACK", 1032: "SQLITE_READONLY_DBMOVED", 267: "SQLITE_CORRUPT_VTAB",
|
|
51
|
+
275: "SQLITE_CONSTRAINT_CHECK", 787: "SQLITE_CONSTRAINT_FOREIGNKEY", 1299: "SQLITE_CONSTRAINT_NOTNULL",
|
|
52
|
+
1555: "SQLITE_CONSTRAINT_PRIMARYKEY", 2067: "SQLITE_CONSTRAINT_UNIQUE", 2579: "SQLITE_CONSTRAINT_ROWID",
|
|
53
|
+
};
|
|
54
|
+
/** better-sqlite3's error code for a SQLite result code (extended when known, else primary). */
|
|
55
|
+
export function sqliteCodeName(errcode) {
|
|
56
|
+
return SQLITE_CODES[errcode] ?? SQLITE_CODES[errcode & 0xff] ?? `SQLITE_UNKNOWN_${errcode}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* node:sqlite errors carry `code: "ERR_SQLITE_ERROR"` and the SQLite result
|
|
60
|
+
* code in `errcode`; the server's checks (unreadable database, BUSY/LOCKED
|
|
61
|
+
* retry) read better-sqlite3's `code: "SQLITE_BUSY"` form. Rewrite it in place.
|
|
62
|
+
*/
|
|
63
|
+
function normaliseError(error) {
|
|
64
|
+
const e = error;
|
|
65
|
+
if (e && typeof e === "object" && typeof e.errcode === "number") {
|
|
66
|
+
try {
|
|
67
|
+
Object.defineProperty(e, "code", { value: sqliteCodeName(e.errcode), writable: true, enumerable: true, configurable: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A frozen error keeps its own code.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return error;
|
|
74
|
+
}
|
|
75
|
+
function guarded(fn) {
|
|
76
|
+
try {
|
|
77
|
+
return fn();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw normaliseError(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function isPlainParamObject(value) {
|
|
84
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer);
|
|
85
|
+
}
|
|
86
|
+
/** better-sqlite3's argument forms -> node:sqlite's (named object first, then positionals). */
|
|
87
|
+
function bindArgs(params) {
|
|
88
|
+
let named = null;
|
|
89
|
+
const positional = [];
|
|
90
|
+
const add = (value) => {
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
value.forEach(add);
|
|
93
|
+
else if (isPlainParamObject(value))
|
|
94
|
+
named = { ...(named ?? {}), ...value };
|
|
95
|
+
else
|
|
96
|
+
positional.push(value);
|
|
97
|
+
};
|
|
98
|
+
params.forEach(add);
|
|
99
|
+
return named ? [named, ...positional] : positional;
|
|
100
|
+
}
|
|
101
|
+
function toNumber(value) {
|
|
102
|
+
// better-sqlite3 returns integers past 2^53 as (inexact) numbers; so do we.
|
|
103
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
104
|
+
}
|
|
105
|
+
/** A row as better-sqlite3 returns it: a plain object, BLOBs as Buffers. */
|
|
106
|
+
function plainRow(row, bigints) {
|
|
107
|
+
if (row === undefined || row === null || typeof row !== "object")
|
|
108
|
+
return row;
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const [key, value] of Object.entries(row)) {
|
|
111
|
+
out[key] =
|
|
112
|
+
value instanceof Uint8Array && !Buffer.isBuffer(value)
|
|
113
|
+
? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
114
|
+
: bigints
|
|
115
|
+
? toNumber(value)
|
|
116
|
+
: value;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function isOutOfRange(error) {
|
|
121
|
+
return error?.code === "ERR_OUT_OF_RANGE";
|
|
122
|
+
}
|
|
123
|
+
class NodeSqliteStatement {
|
|
124
|
+
stmt;
|
|
125
|
+
constructor(stmt) {
|
|
126
|
+
this.stmt = stmt;
|
|
127
|
+
}
|
|
128
|
+
/** Read once as numbers; an integer past 2^53 makes node:sqlite throw, so re-read as BigInts and convert. */
|
|
129
|
+
read(fn, args) {
|
|
130
|
+
const bound = bindArgs(args);
|
|
131
|
+
try {
|
|
132
|
+
return { value: fn(bound), bigints: false };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!isOutOfRange(error))
|
|
136
|
+
throw normaliseError(error);
|
|
137
|
+
this.stmt.setReadBigInts(true);
|
|
138
|
+
try {
|
|
139
|
+
return { value: guarded(() => fn(bound)), bigints: true };
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
this.stmt.setReadBigInts(false);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
run(...params) {
|
|
147
|
+
const result = guarded(() => this.stmt.run(...bindArgs(params)));
|
|
148
|
+
return { changes: Number(result.changes), lastInsertRowid: result.lastInsertRowid };
|
|
149
|
+
}
|
|
150
|
+
get(...params) {
|
|
151
|
+
const { value, bigints } = this.read((a) => this.stmt.get(...a), params);
|
|
152
|
+
return plainRow(value, bigints);
|
|
153
|
+
}
|
|
154
|
+
all(...params) {
|
|
155
|
+
const { value, bigints } = this.read((a) => this.stmt.all(...a), params);
|
|
156
|
+
return value.map((row) => plainRow(row, bigints));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** better-sqlite3's default busy timeout, which the server's retry logic assumes. */
|
|
160
|
+
const BUSY_TIMEOUT_MS = 5000;
|
|
161
|
+
export class NodeSqliteDatabase {
|
|
162
|
+
name;
|
|
163
|
+
readonly;
|
|
164
|
+
db;
|
|
165
|
+
isOpen = true;
|
|
166
|
+
depth = 0;
|
|
167
|
+
savepoints = 0;
|
|
168
|
+
constructor(Ctor, file, options = {}) {
|
|
169
|
+
this.name = file;
|
|
170
|
+
this.readonly = options.readonly === true;
|
|
171
|
+
const inMemory = file === ":memory:" || file === "";
|
|
172
|
+
if (!inMemory && (options.fileMustExist || this.readonly) && !fs.existsSync(file)) {
|
|
173
|
+
// better-sqlite3's error for the same case.
|
|
174
|
+
throw Object.assign(new Error("unable to open database file"), { code: "SQLITE_CANTOPEN", errcode: 14 });
|
|
175
|
+
}
|
|
176
|
+
this.db = guarded(() => new Ctor(file, { readOnly: this.readonly }));
|
|
177
|
+
try {
|
|
178
|
+
this.db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
179
|
+
// Belt and braces for the read-only contract on the desktop app's
|
|
180
|
+
// database: refused writes even if a runtime ignored `readOnly`.
|
|
181
|
+
if (this.readonly)
|
|
182
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.db.close();
|
|
186
|
+
throw normaliseError(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
get open() {
|
|
190
|
+
return this.isOpen;
|
|
191
|
+
}
|
|
192
|
+
assertOpen() {
|
|
193
|
+
if (!this.isOpen)
|
|
194
|
+
throw new TypeError("The database connection is not open");
|
|
195
|
+
}
|
|
196
|
+
prepare(source) {
|
|
197
|
+
this.assertOpen();
|
|
198
|
+
return new NodeSqliteStatement(guarded(() => this.db.prepare(source)));
|
|
199
|
+
}
|
|
200
|
+
exec(source) {
|
|
201
|
+
this.assertOpen();
|
|
202
|
+
guarded(() => this.db.exec(source));
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
pragma(source, options) {
|
|
206
|
+
const rows = this.prepare(`PRAGMA ${source}`).all();
|
|
207
|
+
if (options?.simple) {
|
|
208
|
+
const first = rows[0];
|
|
209
|
+
return first ? Object.values(first)[0] : undefined;
|
|
210
|
+
}
|
|
211
|
+
return rows;
|
|
212
|
+
}
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
|
+
transaction(fn) {
|
|
215
|
+
return (...args) => {
|
|
216
|
+
this.assertOpen();
|
|
217
|
+
const nested = this.depth > 0;
|
|
218
|
+
const savepoint = nested ? `fourda_sp_${++this.savepoints}` : null;
|
|
219
|
+
this.exec(savepoint ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
220
|
+
this.depth++;
|
|
221
|
+
try {
|
|
222
|
+
const result = fn(...args);
|
|
223
|
+
this.exec(savepoint ? `RELEASE ${savepoint}` : "COMMIT");
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
try {
|
|
228
|
+
this.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK");
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// SQLite already rolled back (e.g. after SQLITE_FULL); the original error matters.
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
this.depth--;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
close() {
|
|
241
|
+
if (this.isOpen) {
|
|
242
|
+
this.isOpen = false;
|
|
243
|
+
this.db.close();
|
|
244
|
+
}
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Selection
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
const require = createRequire(import.meta.url);
|
|
252
|
+
/** Load node:sqlite without its one-time "SQLite is an experimental feature" warning; every other warning passes. */
|
|
253
|
+
export function loadNodeSqlite() {
|
|
254
|
+
const original = process.emitWarning;
|
|
255
|
+
process.emitWarning = function (warning, ...rest) {
|
|
256
|
+
const message = typeof warning === "string" ? warning : warning?.message;
|
|
257
|
+
const opt = rest[0];
|
|
258
|
+
const type = typeof opt === "string" ? opt : opt?.type ?? warning?.name;
|
|
259
|
+
if (type === "ExperimentalWarning" && /^SQLite is an experimental feature/.test(String(message)))
|
|
260
|
+
return;
|
|
261
|
+
return original.call(process, warning, ...rest);
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
const mod = require("node:sqlite");
|
|
265
|
+
if (typeof mod.DatabaseSync !== "function")
|
|
266
|
+
throw new Error("node:sqlite has no DatabaseSync");
|
|
267
|
+
return mod.DatabaseSync;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
process.emitWarning = original;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function loadBetterSqlite3() {
|
|
274
|
+
const mod = require("better-sqlite3");
|
|
275
|
+
return typeof mod === "function" ? mod : mod.default;
|
|
276
|
+
}
|
|
277
|
+
function firstLine(error) {
|
|
278
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0].trim();
|
|
279
|
+
}
|
|
280
|
+
function forcedDriver(env) {
|
|
281
|
+
const raw = env.FOURDA_SQLITE_DRIVER?.trim().toLowerCase();
|
|
282
|
+
if (!raw)
|
|
283
|
+
return null;
|
|
284
|
+
if (raw === "node" || raw === "node:sqlite")
|
|
285
|
+
return "node:sqlite";
|
|
286
|
+
if (raw === "better" || raw === "better-sqlite3")
|
|
287
|
+
return "better-sqlite3";
|
|
288
|
+
if (raw === "none")
|
|
289
|
+
return "none";
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Pick the driver: node:sqlite, then better-sqlite3 if its native module
|
|
294
|
+
* actually opens a database (importing it succeeds without one). Pure apart
|
|
295
|
+
* from the loaders, so tests can make either one fail.
|
|
296
|
+
*/
|
|
297
|
+
export function resolveSqliteDriver(loaders = { node: loadNodeSqlite, better: loadBetterSqlite3 }, env = process.env) {
|
|
298
|
+
const forced = forcedDriver(env);
|
|
299
|
+
// "none" simulates a host where neither driver works (tests, diagnostics).
|
|
300
|
+
const order = forced === "none" ? [] : forced ? [forced] : ["node:sqlite", "better-sqlite3"];
|
|
301
|
+
const rejected = [];
|
|
302
|
+
for (const name of order) {
|
|
303
|
+
if (name === "node:sqlite") {
|
|
304
|
+
try {
|
|
305
|
+
const Ctor = loaders.node();
|
|
306
|
+
new NodeSqliteDatabase(Ctor, ":memory:").close();
|
|
307
|
+
return { driver: { name, open: (file, options) => new NodeSqliteDatabase(Ctor, file, options) }, rejected, problem: null };
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
rejected.push({ name, reason: `not available in Node ${process.versions.node} (${firstLine(error)})` });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
let Ctor;
|
|
315
|
+
try {
|
|
316
|
+
Ctor = loaders.better();
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
rejected.push({ name, reason: `not installed (${firstLine(error)})` });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const problem = checkNativeBindings(Ctor);
|
|
323
|
+
if (problem) {
|
|
324
|
+
rejected.push({ name, reason: problem });
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
return { driver: { name, open: (file, options) => new Ctor(file, options) }, rejected, problem: null };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const tried = rejected.map((r) => `${r.name}: ${r.reason}`).join("; ");
|
|
331
|
+
const problem = forced === "none"
|
|
332
|
+
? "FOURDA_SQLITE_DRIVER=none disables SQLite: the database tools are unavailable. Unset it to let the server choose a driver."
|
|
333
|
+
: forced
|
|
334
|
+
? `FOURDA_SQLITE_DRIVER=${env.FOURDA_SQLITE_DRIVER} forces ${forced}, which cannot be used here (${tried}). Unset it to let the server choose.`
|
|
335
|
+
: `No SQLite driver is usable (${tried}). Fix either one: upgrade Node.js to ${NODE_SQLITE_MIN} or later ` +
|
|
336
|
+
`(its built-in node:sqlite needs no install step), or let better-sqlite3 build: ${NATIVE_BINDING_FIX}`;
|
|
337
|
+
return { driver: null, rejected, problem };
|
|
338
|
+
}
|
|
339
|
+
let resolved = null;
|
|
340
|
+
/** The resolution for this process (computed once). */
|
|
341
|
+
export function sqliteDriverStatus() {
|
|
342
|
+
resolved ??= resolveSqliteDriver();
|
|
343
|
+
return resolved;
|
|
344
|
+
}
|
|
345
|
+
/** The driver in use; throws SqliteUnavailableError with the fix when there is none. */
|
|
346
|
+
export function sqliteDriver() {
|
|
347
|
+
const status = sqliteDriverStatus();
|
|
348
|
+
if (!status.driver)
|
|
349
|
+
throw new SqliteUnavailableError(status.problem ?? "No SQLite driver is usable.");
|
|
350
|
+
return status.driver;
|
|
351
|
+
}
|
|
352
|
+
/** Open a database with the driver in use. */
|
|
353
|
+
export function openDatabase(file, options) {
|
|
354
|
+
return sqliteDriver().open(file, options);
|
|
355
|
+
}
|
|
356
|
+
/** Forget the cached resolution (tests that change FOURDA_SQLITE_DRIVER). */
|
|
357
|
+
export function resetSqliteDriver() {
|
|
358
|
+
resolved = null;
|
|
359
|
+
}
|
package/dist/tool-dispatch.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ import type { FourDADatabase } from "./db.js";
|
|
|
11
11
|
* Awaiting a sync return resolves immediately, so we can await uniformly.
|
|
12
12
|
*/
|
|
13
13
|
export type ToolExecutor = (db: FourDADatabase, params: any) => unknown | Promise<unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* Tools that answer from the project's lockfiles and the registries alone, so
|
|
16
|
+
* they still run when no SQLite driver is usable at all (sqlite-driver.ts:
|
|
17
|
+
* Node < 22.13 with better-sqlite3 unbuilt). Their executors accept a null
|
|
18
|
+
* database; every other tool needs one.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DATABASE_FREE_TOOLS: ReadonlySet<string>;
|
|
14
21
|
/**
|
|
15
22
|
* Dispatch a tool call by name. Awaits uniformly (no-op for sync executors).
|
|
16
23
|
* Returns MCP-formatted response with JSON-serialized result.
|
|
@@ -19,7 +26,7 @@ export type ToolExecutor = (db: FourDADatabase, params: any) => unknown | Promis
|
|
|
19
26
|
* every tool call passes through. Stdio callers are unaffected (no auth
|
|
20
27
|
* context; the host already owns the process).
|
|
21
28
|
*/
|
|
22
|
-
export declare function dispatchTool(name: string, db: FourDADatabase, args: Record<string, unknown> | undefined): Promise<CallToolResult>;
|
|
29
|
+
export declare function dispatchTool(name: string, db: FourDADatabase | null, args: Record<string, unknown> | undefined): Promise<CallToolResult>;
|
|
23
30
|
/** What a desktop-only tool says when no desktop app database was found. */
|
|
24
31
|
export declare function desktopAppNote(name: string): string;
|
|
25
32
|
/** Check if a tool exists in the dispatch map */
|
package/dist/tool-dispatch.js
CHANGED
|
@@ -37,6 +37,18 @@ const DISPATCH_MAP = {
|
|
|
37
37
|
// Identity
|
|
38
38
|
developer_dna: executeDeveloperDna,
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Tools that answer from the project's lockfiles and the registries alone, so
|
|
42
|
+
* they still run when no SQLite driver is usable at all (sqlite-driver.ts:
|
|
43
|
+
* Node < 22.13 with better-sqlite3 unbuilt). Their executors accept a null
|
|
44
|
+
* database; every other tool needs one.
|
|
45
|
+
*/
|
|
46
|
+
export const DATABASE_FREE_TOOLS = new Set([
|
|
47
|
+
"vulnerability_scan",
|
|
48
|
+
"dependency_health",
|
|
49
|
+
"dependency_check",
|
|
50
|
+
"upgrade_impact",
|
|
51
|
+
]);
|
|
40
52
|
/**
|
|
41
53
|
* DB-backed tools whose payload reflects the curated feed and can therefore go stale. The MCP
|
|
42
54
|
* server reads `4da.db` but cannot fetch or score, so these get a `data_freshness` annotation that
|
|
@@ -62,6 +74,9 @@ export async function dispatchTool(name, db, args) {
|
|
|
62
74
|
if (!executor) {
|
|
63
75
|
throw new Error(`Unknown tool: ${name}`);
|
|
64
76
|
}
|
|
77
|
+
if (!db && !DATABASE_FREE_TOOLS.has(name)) {
|
|
78
|
+
throw new Error(`${name} needs a database, and none is open.`);
|
|
79
|
+
}
|
|
65
80
|
assertToolPermission(name);
|
|
66
81
|
// BUSY/LOCKED retry at the single choke point every tool passes through.
|
|
67
82
|
// The desktop engine writes to the same database every 30 minutes; a read
|
|
@@ -86,11 +101,11 @@ export async function dispatchTool(name, db, args) {
|
|
|
86
101
|
}
|
|
87
102
|
// Feed freshness describes the desktop app's feed; a standalone install has
|
|
88
103
|
// none, and telling it to "run fourda-engine" is noise.
|
|
89
|
-
const withFreshness = FRESHNESS_TOOLS.has(name) && !db.isStandalone;
|
|
104
|
+
const withFreshness = db !== null && FRESHNESS_TOOLS.has(name) && !db.isStandalone;
|
|
90
105
|
// A desktop-only tool called without the desktop app's database (tools/list
|
|
91
106
|
// hides these in standalone mode, but a host can call one by name) used to
|
|
92
107
|
// answer `[]`, which reads as "nothing relevant" rather than "no app".
|
|
93
|
-
const desktopOnly = db.isStandalone && TOOL_REGISTRY[name]?.standalone === false;
|
|
108
|
+
const desktopOnly = db !== null && db.isStandalone && TOOL_REGISTRY[name]?.standalone === false;
|
|
94
109
|
const payload = cleanStrings(withFreshness ? attachFreshness(db, result) : desktopOnly ? attachDesktopAppNote(name, result) : result);
|
|
95
110
|
// An executor that returns `{ error: "<message>" }` failed: say so in the
|
|
96
111
|
// protocol, not only in the body, so the host and model treat it as an error.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Cross-agent persistent memory. What Claude Code learns, Cursor can access.
|
|
6
6
|
* Enables AI agents to store and recall memories across sessions and tools.
|
|
7
7
|
*/
|
|
8
|
+
import { writeMeta, writerOf } from "../memory-store.js";
|
|
8
9
|
import { rankRowsByRecall } from "./recall.js";
|
|
9
10
|
import { getEmbeddingConfig, semanticScores, } from "../embeddings.js";
|
|
10
11
|
// ============================================================================
|
|
@@ -149,8 +150,14 @@ export function executeAgentMemory(db, params) {
|
|
|
149
150
|
}
|
|
150
151
|
// --- Storage quota: max 1000 entries per agent ---
|
|
151
152
|
const agentType = params.agent_type || "unknown";
|
|
153
|
+
// The app's own agent_memory table (separate write connection), or the
|
|
154
|
+
// server's store when the app's schema cannot hold the row.
|
|
155
|
+
const to = writerOf(db, "agent_memory", [
|
|
156
|
+
"session_id", "agent_type", "memory_type", "subject", "content", "context_tags", "expires_at",
|
|
157
|
+
]);
|
|
152
158
|
try {
|
|
153
|
-
|
|
159
|
+
// Counted where the row will go.
|
|
160
|
+
const countRow = (to.target === "server_store" ? to.raw : rawDb)
|
|
154
161
|
.prepare(`SELECT COUNT(*) as cnt FROM agent_memory WHERE agent_type = ?`)
|
|
155
162
|
.get(agentType);
|
|
156
163
|
const currentCount = countRow?.cnt ?? 0;
|
|
@@ -166,7 +173,7 @@ export function executeAgentMemory(db, params) {
|
|
|
166
173
|
};
|
|
167
174
|
}
|
|
168
175
|
try {
|
|
169
|
-
const stmt =
|
|
176
|
+
const stmt = to.raw.prepare(`INSERT INTO agent_memory
|
|
170
177
|
(session_id, agent_type, memory_type, subject, content, context_tags, expires_at)
|
|
171
178
|
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
172
179
|
const result = stmt.run(params.session_id || "unknown", params.agent_type || "unknown", params.memory_type || "context", params.subject, params.content, JSON.stringify(params.context_tags || []), params.expires_at || null);
|
|
@@ -174,6 +181,7 @@ export function executeAgentMemory(db, params) {
|
|
|
174
181
|
success: true,
|
|
175
182
|
id: result.lastInsertRowid,
|
|
176
183
|
message: `Memory stored: ${params.subject}`,
|
|
184
|
+
...writeMeta(to),
|
|
177
185
|
};
|
|
178
186
|
}
|
|
179
187
|
catch (error) {
|
package/dist/tools/app-plan.d.ts
CHANGED
|
@@ -76,12 +76,22 @@ export interface AppPlanResult {
|
|
|
76
76
|
totalSteps: number;
|
|
77
77
|
steps: AppPlanStepView[];
|
|
78
78
|
summary: string;
|
|
79
|
+
/** The project the steps were narrowed to, or null for every project in the plan. */
|
|
80
|
+
projectScope: string | null;
|
|
81
|
+
/** Steps of the full plan that only touch other projects (left out by projectScope). */
|
|
82
|
+
otherProjectSteps?: number;
|
|
79
83
|
snapshot: Omit<AppPlanSnapshot, "steps" | "generatedAt" | "expiresAt">;
|
|
80
84
|
provenance: {
|
|
81
85
|
mode: "app_plan";
|
|
82
86
|
note: string;
|
|
83
87
|
};
|
|
84
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The steps that touch `scope` (a project directory): each step keeps only the
|
|
91
|
+
* installed lines with a site in that project, and those lines keep only
|
|
92
|
+
* those sites. Null keeps everything.
|
|
93
|
+
*/
|
|
94
|
+
export declare function scopeAppPlanSteps(steps: AppPlanStepView[], scope: string | null): AppPlanStepView[];
|
|
85
95
|
/** Shape the app's plan as the tool result; `risk_threshold` / `max_recommendations` narrow it. */
|
|
86
96
|
export declare function formatAppPlan(read: Extract<AppPlanRead, {
|
|
87
97
|
kind: "plan";
|
|
@@ -89,4 +99,4 @@ export declare function formatAppPlan(read: Extract<AppPlanRead, {
|
|
|
89
99
|
risk_threshold?: string;
|
|
90
100
|
max_recommendations?: number;
|
|
91
101
|
package?: string;
|
|
92
|
-
}): AppPlanResult;
|
|
102
|
+
}, scope?: string | null): AppPlanResult;
|
package/dist/tools/app-plan.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* APP_PLAN_SCHEMA_VERSION is treated as absent and the caller falls back to
|
|
15
15
|
* its standalone heuristic.
|
|
16
16
|
*/
|
|
17
|
+
import { isWithin } from "./project-scope.js";
|
|
17
18
|
/** The snapshot schema this reader understands (Rust `PLAN_SCHEMA_VERSION`). */
|
|
18
19
|
export const APP_PLAN_SCHEMA_VERSION = 4;
|
|
19
20
|
const PLAN_KV_KEY = "upgrade_plan_snapshot";
|
|
@@ -93,13 +94,32 @@ export function readAppPlan(db, now = new Date()) {
|
|
|
93
94
|
}
|
|
94
95
|
const URGENCY_RISK = { critical: 3, high: 2, medium: 1, watch: 0 };
|
|
95
96
|
const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
97
|
+
/**
|
|
98
|
+
* The steps that touch `scope` (a project directory): each step keeps only the
|
|
99
|
+
* installed lines with a site in that project, and those lines keep only
|
|
100
|
+
* those sites. Null keeps everything.
|
|
101
|
+
*/
|
|
102
|
+
export function scopeAppPlanSteps(steps, scope) {
|
|
103
|
+
if (!scope)
|
|
104
|
+
return steps;
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const step of steps) {
|
|
107
|
+
const lines = step.lines
|
|
108
|
+
.map((line) => ({ ...line, sites: line.sites.filter((s) => isWithin(s.project, scope)) }))
|
|
109
|
+
.filter((line) => line.sites.length > 0);
|
|
110
|
+
if (lines.length > 0)
|
|
111
|
+
out.push({ ...step, lines });
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
96
115
|
const APP_PROVENANCE_NOTE = "The 4DA desktop app's own cross-project, version-confirmed Upgrade Plan, read from its database " +
|
|
97
116
|
"(the same plan the app shows and `4da plan --json` prints). Each step is keyed by the app's item id.";
|
|
98
117
|
/** Shape the app's plan as the tool result; `risk_threshold` / `max_recommendations` narrow it. */
|
|
99
|
-
export function formatAppPlan(read, params) {
|
|
118
|
+
export function formatAppPlan(read, params, scope = null) {
|
|
100
119
|
const { snapshot, stale } = read;
|
|
101
120
|
const threshold = params.risk_threshold ?? "all";
|
|
102
|
-
|
|
121
|
+
const scoped = scopeAppPlanSteps(snapshot.steps, scope);
|
|
122
|
+
let steps = scoped;
|
|
103
123
|
// `package` narrows the app's plan the same way it narrows the standalone one.
|
|
104
124
|
const wanted = params.package?.trim().toLowerCase();
|
|
105
125
|
if (wanted)
|
|
@@ -113,8 +133,9 @@ export function formatAppPlan(read, params) {
|
|
|
113
133
|
const count = (m) => selected.filter((s) => s.mechanism === m).length;
|
|
114
134
|
const majors = selected.filter((s) => s.lines.some((l) => l.upgrade_type === "major")).length;
|
|
115
135
|
const parts = [
|
|
116
|
-
`${selected.length} step${selected.length !== 1 ? "s" : ""} from the 4DA app's plan (generated ${snapshot.generatedAt})`,
|
|
136
|
+
`${selected.length} step${selected.length !== 1 ? "s" : ""} from the 4DA app's plan (generated ${snapshot.generatedAt})${scope ? ` for ${scope}` : " across every project it tracks"}`,
|
|
117
137
|
];
|
|
138
|
+
const otherProjectSteps = snapshot.steps.length - scoped.length;
|
|
118
139
|
if (count("manifest_bump") > 0)
|
|
119
140
|
parts.push(`${count("manifest_bump")} manifest bump(s)`);
|
|
120
141
|
if (count("lockfile_or_parent_update") > 0) {
|
|
@@ -126,8 +147,11 @@ export function formatAppPlan(read, params) {
|
|
|
126
147
|
parts.push(`${count("no_fix")} with no fix published`);
|
|
127
148
|
if (majors > 0)
|
|
128
149
|
parts.push(`${majors} cross a major version`);
|
|
129
|
-
if (selected.length <
|
|
130
|
-
parts.push(`${
|
|
150
|
+
if (selected.length < scoped.length) {
|
|
151
|
+
parts.push(`${scoped.length} in ${scope ? "the plan for this project" : "the full plan"} (raise max_recommendations for the rest)`);
|
|
152
|
+
}
|
|
153
|
+
if (scope && otherProjectSteps > 0) {
|
|
154
|
+
parts.push(`${otherProjectSteps} step${otherProjectSteps !== 1 ? "s" : ""} only touch other projects (project_path "*" lists every project)`);
|
|
131
155
|
}
|
|
132
156
|
const staleness = stale
|
|
133
157
|
? `STALE: past its freshness horizon (${snapshot.expiresAt}). The advisories or installs may have changed since; open the 4DA app to recompute.`
|
|
@@ -139,9 +163,11 @@ export function formatAppPlan(read, params) {
|
|
|
139
163
|
expiresAt: snapshot.expiresAt,
|
|
140
164
|
stale,
|
|
141
165
|
staleness,
|
|
142
|
-
totalSteps:
|
|
166
|
+
totalSteps: scoped.length,
|
|
143
167
|
steps: selected,
|
|
144
168
|
summary: parts.join(". ") + ".",
|
|
169
|
+
projectScope: scope,
|
|
170
|
+
...(scope ? { otherProjectSteps } : {}),
|
|
145
171
|
snapshot: {
|
|
146
172
|
schemaVersion: snapshot.schemaVersion,
|
|
147
173
|
generatorVersion: snapshot.generatorVersion,
|
|
@@ -23,6 +23,8 @@ export interface TaskPackage {
|
|
|
23
23
|
/** Versions the task text names after the package ("5.x to 7.x" -> from 5.x, to 7.x). */
|
|
24
24
|
requested_from: string | null;
|
|
25
25
|
requested_to: string | null;
|
|
26
|
+
/** Set when the package was found through a family the task names ("all tauri plugins" -> "tauri"). */
|
|
27
|
+
family?: string;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Package names that are also everyday words. One of these counts as named
|
|
@@ -36,8 +38,25 @@ export declare const COMMON_WORDS: Set<string>;
|
|
|
36
38
|
* Direct dependencies first; at most `limit` packages.
|
|
37
39
|
*/
|
|
38
40
|
export declare function detectTaskPackages(task: string, files: string[], deps: ResolvedDependency[], limit?: number): TaskPackage[];
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The package families a task names, as base names: "all tauri plugins",
|
|
43
|
+
* "tauri and its plugins", "@tauri-apps packages", "tauri-plugin-*",
|
|
44
|
+
* "@tauri-apps/*". A name alone ("bump tauri") is not a family.
|
|
45
|
+
*/
|
|
46
|
+
export declare function detectFamilyBases(task: string, files?: string[]): string[];
|
|
47
|
+
/** True when `name` belongs to the family of `base`: base-*, base_*, @base/*, @base-apps/*. */
|
|
48
|
+
export declare function inFamily(name: string, base: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The direct dependencies in the families the task names, other than the
|
|
51
|
+
* packages it names exactly. Only packages this project actually declares:
|
|
52
|
+
* a family is expanded against the lockfiles, never guessed. Measured
|
|
53
|
+
* 2026-10-07: "bump tauri and all tauri plugins" matched only `tauri`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectFamilyPackages(task: string, files: string[], deps: ResolvedDependency[], exact: TaskPackage[], limit?: number): TaskPackage[];
|
|
56
|
+
/** True when the task asks for the newest release ("to latest", "newest major"). */
|
|
57
|
+
export declare function wantsLatest(task: string): boolean;
|
|
58
|
+
/** The caret line a version sits on: "2" for 2.12.1, "0.13" for 0.13.5. */
|
|
59
|
+
export declare function caretLine(version: string | null): string | null;
|
|
41
60
|
/** Actionable advisories on the installed copies of one package, highest presented severity first. */
|
|
42
61
|
export declare function advisoriesFor(pkg: TaskPackage, vulns: VulnerabilityEntry[]): {
|
|
43
62
|
id: string;
|