@4da/mcp-server 5.0.1 → 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/auth-context.d.ts +0 -1
- package/dist/auth-context.js +0 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js +0 -1
- package/dist/db.d.ts +17 -2
- package/dist/db.js +93 -66
- package/dist/doctor.d.ts +0 -1
- package/dist/doctor.js +17 -15
- package/dist/embeddings.d.ts +0 -1
- package/dist/embeddings.js +0 -1
- package/dist/http-transport.d.ts +0 -1
- package/dist/http-transport.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +17 -5
- package/dist/live/cache.d.ts +0 -1
- package/dist/live/cache.js +0 -1
- package/dist/live/crates-registry.d.ts +0 -1
- package/dist/live/crates-registry.js +5 -8
- package/dist/live/cvss.d.ts +0 -1
- package/dist/live/cvss.js +0 -1
- package/dist/live/go-registry.d.ts +0 -1
- package/dist/live/go-registry.js +4 -9
- package/dist/live/hn-fetcher.d.ts +0 -1
- package/dist/live/hn-fetcher.js +23 -11
- package/dist/live/http-utils.d.ts +0 -1
- package/dist/live/http-utils.js +0 -1
- package/dist/live/index.d.ts +0 -1
- package/dist/live/index.js +0 -1
- package/dist/live/npm-registry.d.ts +0 -1
- package/dist/live/npm-registry.js +4 -8
- package/dist/live/osv-scanner.d.ts +0 -1
- package/dist/live/osv-scanner.js +0 -1
- package/dist/live/platform.d.ts +0 -1
- package/dist/live/platform.js +0 -1
- package/dist/live/pypi-registry.d.ts +0 -1
- package/dist/live/pypi-registry.js +7 -10
- package/dist/live/rate-limiter.d.ts +0 -1
- package/dist/live/rate-limiter.js +0 -1
- package/dist/live/semver-utils.d.ts +11 -1
- package/dist/live/semver-utils.js +14 -1
- package/dist/live/types.d.ts +0 -1
- package/dist/live/types.js +0 -1
- package/dist/live/version-resolver.d.ts +0 -1
- package/dist/live/version-resolver.js +0 -1
- package/dist/live-singleton.d.ts +0 -1
- package/dist/live-singleton.js +0 -1
- package/dist/project-scanner.d.ts +0 -1
- package/dist/project-scanner.js +0 -1
- package/dist/schema-registry.d.ts +17 -8
- package/dist/schema-registry.js +54 -9
- package/dist/schemas/developer-dna.json +1 -1
- package/dist/schemas/get-context.json +2 -2
- package/dist/schemas/get-relevant-content.json +1 -1
- package/dist/schemas/record-feedback.json +1 -1
- package/dist/setup.d.ts +0 -1
- package/dist/setup.js +0 -1
- package/dist/tool-dispatch.d.ts +0 -1
- package/dist/tool-dispatch.js +21 -2
- package/dist/tools/agent-memory.d.ts +0 -1
- package/dist/tools/agent-memory.js +0 -1
- package/dist/tools/decision-enforcement.d.ts +0 -1
- package/dist/tools/decision-enforcement.js +0 -1
- package/dist/tools/decision-memory.d.ts +0 -1
- package/dist/tools/decision-memory.js +0 -1
- package/dist/tools/decision-recall.d.ts +0 -1
- package/dist/tools/decision-recall.js +0 -1
- package/dist/tools/dependency-health.d.ts +0 -1
- package/dist/tools/dependency-health.js +31 -5
- package/dist/tools/developer-dna.d.ts +3 -3
- package/dist/tools/developer-dna.js +63 -47
- package/dist/tools/ecosystem-pulse.d.ts +7 -2
- package/dist/tools/ecosystem-pulse.js +39 -3
- package/dist/tools/get-actionable-signals.d.ts +0 -1
- package/dist/tools/get-actionable-signals.js +0 -1
- package/dist/tools/get-context.d.ts +0 -1
- package/dist/tools/get-context.js +2 -3
- package/dist/tools/get-relevant-content.d.ts +0 -1
- package/dist/tools/get-relevant-content.js +1 -3
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/dist/tools/knowledge-gaps.d.ts +0 -1
- package/dist/tools/knowledge-gaps.js +79 -17
- package/dist/tools/recall.d.ts +0 -1
- package/dist/tools/recall.js +0 -1
- package/dist/tools/record-feedback.d.ts +1 -2
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/upgrade-planner.d.ts +0 -1
- package/dist/tools/upgrade-planner.js +15 -3
- package/dist/tools/vulnerability-scan.d.ts +0 -1
- package/dist/tools/vulnerability-scan.js +0 -1
- package/dist/tools/what-should-i-know.d.ts +0 -1
- package/dist/tools/what-should-i-know.js +6 -3
- package/dist/types.d.ts +2 -3
- package/dist/types.js +0 -1
- package/package.json +16 -5
- package/dist/llm.d.ts +0 -162
- package/dist/llm.js +0 -502
- package/dist/output-manager.d.ts +0 -156
- package/dist/output-manager.js +0 -157
- package/dist/scheduler/core.d.ts +0 -25
- package/dist/scheduler/core.js +0 -61
- package/dist/scheduler/freshness.d.ts +0 -17
- package/dist/scheduler/freshness.js +0 -41
- package/dist/scheduler/index.d.ts +0 -21
- package/dist/scheduler/index.js +0 -32
- package/dist/scheduler/refreshers.d.ts +0 -21
- package/dist/scheduler/refreshers.js +0 -21
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* developer_dna tool
|
|
4
4
|
*
|
|
5
5
|
* Export a comprehensive Developer DNA profile built from project scans,
|
|
6
|
-
*
|
|
6
|
+
* explicit interests, and local interaction history.
|
|
7
7
|
*/
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Tool Definition
|
|
10
10
|
// =============================================================================
|
|
11
11
|
export const developerDnaTool = {
|
|
12
12
|
name: "developer_dna",
|
|
13
|
-
description: `Export your Developer DNA — a comprehensive profile of your tech identity built from project scans,
|
|
13
|
+
description: `Export your Developer DNA — a comprehensive profile of your tech identity built from project scans, explicit interests, and local interaction history. Includes primary stack, adjacent tech, top dependencies, engaged topics, blind spots, source engagement rates, and aggregate stats.`,
|
|
14
14
|
inputSchema: {
|
|
15
15
|
type: "object",
|
|
16
16
|
properties: {
|
|
@@ -135,24 +135,40 @@ export function executeDeveloperDna(db, params) {
|
|
|
135
135
|
// table may not exist
|
|
136
136
|
}
|
|
137
137
|
// -------------------------------------------------------------------------
|
|
138
|
-
// 5. Top Engaged Topics — from topic_affinities
|
|
138
|
+
// 5. Top Engaged Topics — from explicit interactions (topic_affinities was
|
|
139
|
+
// dropped with the implicit-capture system in desktop schema 105; querying
|
|
140
|
+
// it returned empty forever).
|
|
139
141
|
// -------------------------------------------------------------------------
|
|
140
142
|
let topEngagedTopics = [];
|
|
141
143
|
try {
|
|
142
|
-
const
|
|
143
|
-
.prepare(`SELECT
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
144
|
+
const interactionRows = rawDb
|
|
145
|
+
.prepare(`SELECT item_topics FROM interactions
|
|
146
|
+
WHERE action_type IN ('click', 'save') AND item_topics IS NOT NULL`)
|
|
147
|
+
.all();
|
|
148
|
+
const topicCounts = new Map();
|
|
149
|
+
for (const row of interactionRows) {
|
|
150
|
+
try {
|
|
151
|
+
const topics = JSON.parse(row.item_topics);
|
|
152
|
+
if (!Array.isArray(topics))
|
|
153
|
+
continue;
|
|
154
|
+
for (const t of topics) {
|
|
155
|
+
if (typeof t === "string" && t.length > 0) {
|
|
156
|
+
topicCounts.set(t, (topicCounts.get(t) || 0) + 1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Malformed topic JSON on one row — skip it
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const totalPositive = [...topicCounts.values()].reduce((a, b) => a + b, 0);
|
|
165
|
+
topEngagedTopics = [...topicCounts.entries()]
|
|
166
|
+
.sort((a, b) => b[1] - a[1])
|
|
167
|
+
.slice(0, maxTopics)
|
|
168
|
+
.map(([topic, count]) => ({
|
|
169
|
+
topic,
|
|
170
|
+
interactions: count,
|
|
171
|
+
percent_of_total: totalPositive > 0 ? Math.round((count / totalPositive) * 10000) / 100 : 0,
|
|
156
172
|
}));
|
|
157
173
|
}
|
|
158
174
|
catch {
|
|
@@ -175,25 +191,9 @@ export function executeDeveloperDna(db, params) {
|
|
|
175
191
|
const now = Date.now();
|
|
176
192
|
for (const dep of topDependencies) {
|
|
177
193
|
const depLower = dep.name.toLowerCase();
|
|
178
|
-
// Check if any interaction item's source mentions this dependency
|
|
179
|
-
// Use a simpler heuristic: check if the dependency name appears in
|
|
180
|
-
// topic_affinities with positive signals
|
|
181
|
-
let lastEngagement = null;
|
|
182
|
-
try {
|
|
183
|
-
const affinityRow = rawDb
|
|
184
|
-
.prepare(`SELECT topic, positive_signals
|
|
185
|
-
FROM topic_affinities
|
|
186
|
-
WHERE LOWER(topic) = ?
|
|
187
|
-
AND positive_signals > 0`)
|
|
188
|
-
.get(depLower);
|
|
189
|
-
if (affinityRow) {
|
|
190
|
-
continue; // Has engagement, not a blind spot
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
// Ignore
|
|
195
|
-
}
|
|
196
194
|
// Check for any source_items mentioning this dep that have interactions
|
|
195
|
+
// (the old topic_affinities shortcut queried a table dropped in schema
|
|
196
|
+
// 105 — it never fired).
|
|
197
197
|
try {
|
|
198
198
|
// Escape LIKE wildcards so a name containing % or _ matches literally.
|
|
199
199
|
const likeDep = `%${depLower.replace(/[\\%_]/g, (c) => "\\" + c)}%`;
|
|
@@ -218,11 +218,12 @@ export function executeDeveloperDna(db, params) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
else {
|
|
221
|
-
// Never interacted with content about this dependency
|
|
221
|
+
// Never interacted with content about this dependency — null, not
|
|
222
|
+
// a -1 sentinel leaking into user-facing output.
|
|
222
223
|
blindSpots.push({
|
|
223
224
|
dependency: dep.name,
|
|
224
225
|
severity: "low",
|
|
225
|
-
days_stale:
|
|
226
|
+
days_stale: null,
|
|
226
227
|
});
|
|
227
228
|
}
|
|
228
229
|
}
|
|
@@ -294,14 +295,28 @@ export function executeDeveloperDna(db, params) {
|
|
|
294
295
|
catch {
|
|
295
296
|
// table may not exist
|
|
296
297
|
}
|
|
298
|
+
// total_relevant / rejection_rate describe the PIPELINE's judgment, so they
|
|
299
|
+
// must come from its persisted verdicts — the old interaction-based count
|
|
300
|
+
// reported total_relevant: 0 / rejection_rate: 1.0 for any passive user,
|
|
301
|
+
// contradicting a corpus with hundreds of feed-relevant items.
|
|
302
|
+
let judgedCount = 0;
|
|
297
303
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
.
|
|
304
|
-
|
|
304
|
+
if (db.hasColumn("source_items", "feed_relevant")) {
|
|
305
|
+
const row = rawDb
|
|
306
|
+
.prepare(`SELECT COUNT(*) as judged, SUM(CASE WHEN feed_relevant = 1 THEN 1 ELSE 0 END) as relevant
|
|
307
|
+
FROM source_items WHERE feed_relevant IS NOT NULL`)
|
|
308
|
+
.get();
|
|
309
|
+
judgedCount = row.judged;
|
|
310
|
+
totalRelevant = row.relevant ?? 0;
|
|
311
|
+
}
|
|
312
|
+
else if (db.hasColumn("source_items", "relevance_score")) {
|
|
313
|
+
const row = rawDb
|
|
314
|
+
.prepare(`SELECT COUNT(*) as judged, SUM(CASE WHEN relevance_score >= 0.4 THEN 1 ELSE 0 END) as relevant
|
|
315
|
+
FROM source_items WHERE relevance_score IS NOT NULL`)
|
|
316
|
+
.get();
|
|
317
|
+
judgedCount = row.judged;
|
|
318
|
+
totalRelevant = row.relevant ?? 0;
|
|
319
|
+
}
|
|
305
320
|
}
|
|
306
321
|
catch {
|
|
307
322
|
// table may not exist
|
|
@@ -336,8 +351,10 @@ export function executeDeveloperDna(db, params) {
|
|
|
336
351
|
catch {
|
|
337
352
|
// table may not exist
|
|
338
353
|
}
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
// Rejection rate over the JUDGED set — unjudged items are "not yet curated",
|
|
355
|
+
// not rejected.
|
|
356
|
+
const rejectionRate = judgedCount > 0
|
|
357
|
+
? Math.round(((judgedCount - totalRelevant) / judgedCount) * 10000) / 10000
|
|
341
358
|
: 0;
|
|
342
359
|
const stats = {
|
|
343
360
|
total_items_processed: totalItemsProcessed,
|
|
@@ -406,4 +423,3 @@ function buildIdentitySummary(primaryStack, adjacentTech, interests, engagedTopi
|
|
|
406
423
|
}
|
|
407
424
|
return parts.join(". ") + ".";
|
|
408
425
|
}
|
|
409
|
-
//# sourceMappingURL=developer-dna.js.map
|
|
@@ -43,6 +43,11 @@ export declare const ecosystemPulseTool: {
|
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
45
|
};
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Derive the tech stack used to filter HN headlines from the database:
|
|
48
|
+
* explicit tech_stack entries plus the languages of tracked dependencies.
|
|
49
|
+
* Works on both the desktop DB and the standalone minimal schema.
|
|
50
|
+
*/
|
|
51
|
+
export declare function deriveTechStackForHeadlines(db: FourDADatabase): string[];
|
|
52
|
+
export declare function executeEcosystemPulse(db: FourDADatabase, params: EcosystemPulseParams, liveIntel: LiveIntelligence | null): Promise<EcosystemPulseResult>;
|
|
47
53
|
export {};
|
|
48
|
-
//# sourceMappingURL=ecosystem-pulse.d.ts.map
|
|
@@ -23,7 +23,35 @@ export const ecosystemPulseTool = {
|
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Derive the tech stack used to filter HN headlines from the database:
|
|
28
|
+
* explicit tech_stack entries plus the languages of tracked dependencies.
|
|
29
|
+
* Works on both the desktop DB and the standalone minimal schema.
|
|
30
|
+
*/
|
|
31
|
+
export function deriveTechStackForHeadlines(db) {
|
|
32
|
+
const stack = new Set();
|
|
33
|
+
const rawDb = db.getRawDb();
|
|
34
|
+
try {
|
|
35
|
+
for (const row of rawDb.prepare("SELECT technology FROM tech_stack").all()) {
|
|
36
|
+
if (row.technology)
|
|
37
|
+
stack.add(row.technology.toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// tech_stack may not exist on exotic DBs — languages below still apply.
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
for (const row of rawDb.prepare("SELECT DISTINCT language FROM project_dependencies").all()) {
|
|
45
|
+
if (row.language)
|
|
46
|
+
stack.add(row.language.toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// project_dependencies may not exist — an empty stack yields an honest empty result.
|
|
51
|
+
}
|
|
52
|
+
return [...stack];
|
|
53
|
+
}
|
|
54
|
+
export async function executeEcosystemPulse(db, params, liveIntel) {
|
|
27
55
|
if (!liveIntel) {
|
|
28
56
|
return {
|
|
29
57
|
headlines: [],
|
|
@@ -32,7 +60,16 @@ export function executeEcosystemPulse(_db, params, liveIntel) {
|
|
|
32
60
|
note: "Live intelligence not available. Set FOURDA_OFFLINE=false and restart.",
|
|
33
61
|
};
|
|
34
62
|
}
|
|
35
|
-
|
|
63
|
+
// The startup prefetch only ever ran in standalone mode, so full-DB servers
|
|
64
|
+
// returned an empty cache forever. Fetch on demand when the cache is empty —
|
|
65
|
+
// the cache still serves warm repeat calls.
|
|
66
|
+
let headlines = liveIntel.getHeadlines();
|
|
67
|
+
if (headlines.length === 0 && liveIntel.isEnabled()) {
|
|
68
|
+
const techStack = deriveTechStackForHeadlines(db);
|
|
69
|
+
if (techStack.length > 0) {
|
|
70
|
+
headlines = await liveIntel.fetchHeadlines(techStack);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
36
73
|
const minPoints = params.min_points ?? 0;
|
|
37
74
|
const limit = params.limit ?? 15;
|
|
38
75
|
const filtered = headlines
|
|
@@ -57,4 +94,3 @@ export function executeEcosystemPulse(_db, params, liveIntel) {
|
|
|
57
94
|
: `${filtered.length} headline${filtered.length !== 1 ? "s" : ""} relevant to your tech stack.`,
|
|
58
95
|
};
|
|
59
96
|
}
|
|
60
|
-
//# sourceMappingURL=ecosystem-pulse.js.map
|
|
@@ -359,4 +359,3 @@ export function executeGetActionableSignals(db, params) {
|
|
|
359
359
|
...(signals.length === 0 && items.length === 0 ? { note: "No source items in the database. Run the 4DA desktop app to fetch content, or use vulnerability_scan to check dependencies directly." } : {}),
|
|
360
360
|
};
|
|
361
361
|
}
|
|
362
|
-
//# sourceMappingURL=get-actionable-signals.js.map
|
|
@@ -15,7 +15,7 @@ export const getContextTool = {
|
|
|
15
15
|
Returns information about:
|
|
16
16
|
- Static Identity: User-declared role, tech stack, domains, interests, and exclusions
|
|
17
17
|
- ACE Context (optional): Auto-detected technologies and active topics from recent activity
|
|
18
|
-
- Learned Preferences (optional):
|
|
18
|
+
- Learned Preferences (optional): retained compatibility field; implicit preference learning no longer contributes to scoring
|
|
19
19
|
|
|
20
20
|
Use this to understand what the user is working on and interested in.`,
|
|
21
21
|
inputSchema: {
|
|
@@ -28,7 +28,7 @@ Use this to understand what the user is working on and interested in.`,
|
|
|
28
28
|
},
|
|
29
29
|
include_learned: {
|
|
30
30
|
type: "boolean",
|
|
31
|
-
description: "Include learned
|
|
31
|
+
description: "Include the retained learned-preferences compatibility field. Default: true",
|
|
32
32
|
default: true,
|
|
33
33
|
},
|
|
34
34
|
},
|
|
@@ -58,4 +58,3 @@ export function executeGetContext(db, params) {
|
|
|
58
58
|
}
|
|
59
59
|
return context;
|
|
60
60
|
}
|
|
61
|
-
//# sourceMappingURL=get-context.js.map
|
|
@@ -41,4 +41,3 @@ export declare const getRelevantContentTool: {
|
|
|
41
41
|
* Execute the get_relevant_content tool
|
|
42
42
|
*/
|
|
43
43
|
export declare function executeGetRelevantContent(db: FourDADatabase, params: GetRelevantContentParams): RelevantItem[];
|
|
44
|
-
//# sourceMappingURL=get-relevant-content.d.ts.map
|
|
@@ -11,12 +11,11 @@ export const getRelevantContentTool = {
|
|
|
11
11
|
name: "get_relevant_content",
|
|
12
12
|
description: `Get filtered relevant content from 4DA's personalized feed.
|
|
13
13
|
|
|
14
|
-
Returns content items that match the user's interests, tech stack, and
|
|
14
|
+
Returns content items that match the user's explicit interests, tech stack, and ACE-detected context.
|
|
15
15
|
Items are scored based on:
|
|
16
16
|
- Explicit interests declared by the user
|
|
17
17
|
- Tech stack and domains
|
|
18
18
|
- ACE-detected context (recent files, projects, git activity)
|
|
19
|
-
- Learned preferences from past interactions
|
|
20
19
|
|
|
21
20
|
Each item includes necessity fields (necessity_score, necessity_reason, necessity_category, necessity_urgency) indicating how critical the item is — e.g., security vulnerabilities affecting your deps, breaking changes in your stack, or deprecation notices. These are populated from the full PASIFA analysis pipeline when available.
|
|
22
21
|
|
|
@@ -67,4 +66,3 @@ export function executeGetRelevantContent(db, params) {
|
|
|
67
66
|
}
|
|
68
67
|
return items;
|
|
69
68
|
}
|
|
70
|
-
//# sourceMappingURL=get-relevant-content.js.map
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -21,4 +21,3 @@ export { decisionMemoryTool, executeDecisionMemory, } from "./decision-memory.js
|
|
|
21
21
|
export { checkDecisionAlignmentTool, executeCheckDecisionAlignment, } from "./decision-enforcement.js";
|
|
22
22
|
export { agentMemoryTool, executeAgentMemory, } from "./agent-memory.js";
|
|
23
23
|
export { developerDnaTool, executeDeveloperDna, } from "./developer-dna.js";
|
|
24
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/tools/index.js
CHANGED
|
@@ -27,4 +27,3 @@ export { checkDecisionAlignmentTool, executeCheckDecisionAlignment, } from "./de
|
|
|
27
27
|
export { agentMemoryTool, executeAgentMemory, } from "./agent-memory.js";
|
|
28
28
|
// Identity
|
|
29
29
|
export { developerDnaTool, executeDeveloperDna, } from "./developer-dna.js";
|
|
30
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// Word-boundary matching prevents "cve" matching inside "achieve", "receiver", etc.
|
|
8
8
|
function hasWordBoundary(text, term) {
|
|
9
|
-
const regex = new RegExp(`\\b${term}\\b`, 'i');
|
|
9
|
+
const regex = new RegExp(`\\b${escapeRegExp(term)}\\b`, 'i');
|
|
10
|
+
return regex.test(text);
|
|
11
|
+
}
|
|
12
|
+
// Package names can contain regex metacharacters (@scope/name, c++, next.js).
|
|
13
|
+
function escapeRegExp(s) {
|
|
14
|
+
return s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
15
|
+
}
|
|
16
|
+
// True when `text` mentions the package name as a whole word. Word characters
|
|
17
|
+
// for this purpose include - and _ (so dep "hono" does NOT match "hono-shim",
|
|
18
|
+
// and never matches "in HONOr of"). @scope/name matches as the full literal.
|
|
19
|
+
function mentionsPackage(text, pkg) {
|
|
20
|
+
const regex = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(pkg)}($|[^A-Za-z0-9_-])`, "i");
|
|
10
21
|
return regex.test(text);
|
|
11
22
|
}
|
|
12
23
|
// Escape SQL LIKE wildcards so a package name containing % or _ (npm names may
|
|
@@ -36,9 +47,16 @@ export const knowledgeGapsTool = {
|
|
|
36
47
|
};
|
|
37
48
|
export function executeKnowledgeGaps(db, params) {
|
|
38
49
|
const rawDb = db.getRawDb();
|
|
39
|
-
//
|
|
50
|
+
// Feature-detect optional columns: a pure-standalone database has no scoring
|
|
51
|
+
// pipeline (no relevance_score) and may predate content_type — the tool must
|
|
52
|
+
// degrade to word-boundary + recency grounding there, never throw.
|
|
53
|
+
const hasRelevance = db.hasColumn("source_items", "relevance_score");
|
|
54
|
+
const hasContentType = db.hasColumn("source_items", "content_type");
|
|
55
|
+
const hasIsDirect = db.hasColumn("project_dependencies", "is_direct");
|
|
56
|
+
// Direct dependencies only — a transitive dep's news is not the user's
|
|
57
|
+
// reading backlog. Dev deps stay in: a vitest or eslint advisory is real.
|
|
40
58
|
const deps = rawDb
|
|
41
|
-
.prepare(
|
|
59
|
+
.prepare(`SELECT package_name, version, project_path, language FROM project_dependencies ${hasIsDirect ? "WHERE is_direct = 1 " : ""}LIMIT 100`)
|
|
42
60
|
.all();
|
|
43
61
|
if (deps.length === 0) {
|
|
44
62
|
return {
|
|
@@ -47,41 +65,86 @@ export function executeKnowledgeGaps(db, params) {
|
|
|
47
65
|
};
|
|
48
66
|
}
|
|
49
67
|
const gaps = [];
|
|
68
|
+
const seenPackages = new Set();
|
|
50
69
|
for (const dep of deps) {
|
|
51
70
|
// Names shorter than 3 chars (e.g. "c", "go", "ws") match too many unrelated
|
|
52
71
|
// items via substring LIKE to be a trustworthy "mention" signal — skip them.
|
|
53
72
|
if (!dep.package_name || dep.package_name.length < 3)
|
|
54
73
|
continue;
|
|
74
|
+
// One gap per package: the same dep declared by several projects would
|
|
75
|
+
// otherwise repeat identical missed_items once per project_path.
|
|
76
|
+
const pkgKey = dep.package_name.toLowerCase();
|
|
77
|
+
if (seenPackages.has(pkgKey))
|
|
78
|
+
continue;
|
|
79
|
+
seenPackages.add(pkgKey);
|
|
55
80
|
// Find source items mentioning this dependency. Engagement is recorded by the
|
|
56
81
|
// app in interactions.item_id / .action_type (the canonical columns; the older
|
|
57
82
|
// source_item_id / action columns are unused), so the NOT-IN suppression must
|
|
58
83
|
// read those or it silently never fires.
|
|
84
|
+
//
|
|
85
|
+
// Grounding (each guard killed an observed false-positive class):
|
|
86
|
+
// - relevance_score >= 0.2: the scoring pipeline's above-noise band — drops
|
|
87
|
+
// off-topic chatter that merely contains the string (chocolate-bar class).
|
|
88
|
+
// - 30-day window: a "gap" is something you MISSED, not archaeology — a
|
|
89
|
+
// 2014 StackOverflow post is not missed intelligence.
|
|
90
|
+
// - LIKE is only a cheap candidate pre-filter; the real test is the
|
|
91
|
+
// word-boundary check below ("invite" must never evidence a vite gap).
|
|
92
|
+
// - feed_relevant is deliberately NOT filtered here: an item the feed gate
|
|
93
|
+
// rejected can still be a legitimate unread dep mention — surfacing those
|
|
94
|
+
// is this tool's niche. The relevance floor already excludes noise.
|
|
59
95
|
const pattern = `%${escapeLike(dep.package_name)}%`;
|
|
60
|
-
const
|
|
61
|
-
.prepare(`SELECT si.id, si.title, si.url, si.source_type, si.created_at
|
|
96
|
+
const candidates = rawDb
|
|
97
|
+
.prepare(`SELECT si.id, si.title, si.url, si.source_type, ${hasContentType ? "si.content_type" : "NULL AS content_type"}, si.created_at,
|
|
98
|
+
${hasRelevance ? "si.relevance_score" : "NULL AS relevance_score"}, substr(COALESCE(si.content, ''), 1, 2000) AS content_head
|
|
62
99
|
FROM source_items si
|
|
63
100
|
WHERE (si.title LIKE ? ESCAPE '\\' OR si.content LIKE ? ESCAPE '\\')
|
|
101
|
+
${hasRelevance ? "AND si.relevance_score IS NOT NULL AND si.relevance_score >= 0.2" : ""}
|
|
102
|
+
AND si.created_at >= datetime('now', '-30 days')
|
|
64
103
|
AND si.id NOT IN (SELECT item_id FROM interactions WHERE action_type IN ('click', 'save'))
|
|
65
|
-
ORDER BY si.created_at DESC LIMIT
|
|
104
|
+
ORDER BY si.created_at DESC LIMIT 25`)
|
|
66
105
|
.all(pattern, pattern);
|
|
106
|
+
// Word-boundary verification: the mention must be the package name as a
|
|
107
|
+
// whole word in the title or the content head, not a substring.
|
|
108
|
+
const mentionedItems = candidates
|
|
109
|
+
.filter((item) => mentionsPackage(item.title || "", dep.package_name) ||
|
|
110
|
+
mentionsPackage(item.content_head || "", dep.package_name))
|
|
111
|
+
.slice(0, 5);
|
|
67
112
|
if (mentionedItems.length > 0) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
113
|
+
const isAdvisory = (item) => item.source_type === "cve" ||
|
|
114
|
+
item.source_type === "osv" ||
|
|
115
|
+
item.content_type === "security_advisory";
|
|
116
|
+
const securityKeywords = (title) => hasWordBoundary(title, "cve") ||
|
|
117
|
+
hasWordBoundary(title, "security") ||
|
|
118
|
+
hasWordBoundary(title, "vulnerability");
|
|
119
|
+
// Graded honestly:
|
|
120
|
+
// - critical: an actual advisory whose TITLE names this dependency — the
|
|
121
|
+
// advisory is about the dep, not merely co-mentioning it in a body.
|
|
122
|
+
// - high: a security-keyword item whose title names the dep.
|
|
123
|
+
// - medium: 3+ recent unread mentions; low: 1-2.
|
|
124
|
+
const advisoryAboutDep = mentionedItems.some((item) => isAdvisory(item) && mentionsPackage(item.title || "", dep.package_name));
|
|
125
|
+
const securityTitleMention = mentionedItems.some((item) => securityKeywords(item.title || "") &&
|
|
126
|
+
mentionsPackage(item.title || "", dep.package_name));
|
|
127
|
+
const severity = advisoryAboutDep
|
|
73
128
|
? "critical"
|
|
74
|
-
:
|
|
129
|
+
: securityTitleMention
|
|
75
130
|
? "high"
|
|
76
|
-
:
|
|
131
|
+
: mentionedItems.length >= 3
|
|
132
|
+
? "medium"
|
|
133
|
+
: "low";
|
|
77
134
|
gaps.push({
|
|
78
135
|
dependency: dep.package_name,
|
|
79
136
|
version: dep.version,
|
|
80
137
|
project_path: dep.project_path,
|
|
81
138
|
language: dep.language,
|
|
82
|
-
missed_items: mentionedItems.map(item => ({
|
|
83
|
-
|
|
84
|
-
title: item.title && item.title.length > 120
|
|
139
|
+
missed_items: mentionedItems.map((item) => ({
|
|
140
|
+
id: item.id,
|
|
141
|
+
title: item.title && item.title.length > 120
|
|
142
|
+
? item.title.substring(0, 120) + "..."
|
|
143
|
+
: item.title,
|
|
144
|
+
url: item.url,
|
|
145
|
+
source_type: item.source_type,
|
|
146
|
+
created_at: item.created_at,
|
|
147
|
+
relevance_score: item.relevance_score,
|
|
85
148
|
})),
|
|
86
149
|
gap_severity: severity,
|
|
87
150
|
missed_count: mentionedItems.length,
|
|
@@ -101,4 +164,3 @@ export function executeKnowledgeGaps(db, params) {
|
|
|
101
164
|
summary: `${filtered.length} knowledge gaps across ${deps.length} tracked dependencies (showing top ${Math.min(filtered.length, maxGaps)})`,
|
|
102
165
|
};
|
|
103
166
|
}
|
|
104
|
-
//# sourceMappingURL=knowledge-gaps.js.map
|
package/dist/tools/recall.d.ts
CHANGED
package/dist/tools/recall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* record_feedback tool
|
|
3
3
|
*
|
|
4
|
-
* Record user
|
|
4
|
+
* Record explicit user interaction history for an item.
|
|
5
5
|
*/
|
|
6
6
|
import type { FourDADatabase } from "../db.js";
|
|
7
7
|
import type { RecordFeedbackParams, FeedbackResult } from "../types.js";
|
|
@@ -36,4 +36,3 @@ export declare const recordFeedbackTool: {
|
|
|
36
36
|
* Execute the record_feedback tool
|
|
37
37
|
*/
|
|
38
38
|
export declare function executeRecordFeedback(db: FourDADatabase, params: RecordFeedbackParams): FeedbackResult;
|
|
39
|
-
//# sourceMappingURL=record-feedback.d.ts.map
|
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* record_feedback tool
|
|
4
4
|
*
|
|
5
|
-
* Record user
|
|
5
|
+
* Record explicit user interaction history for an item.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Tool definition for MCP registration
|
|
9
9
|
*/
|
|
10
10
|
export const recordFeedbackTool = {
|
|
11
11
|
name: "record_feedback",
|
|
12
|
-
description: `Record user
|
|
12
|
+
description: `Record explicit user interaction history for a content item.
|
|
13
13
|
|
|
14
14
|
Feedback actions:
|
|
15
|
-
- "click": User clicked/opened the item
|
|
16
|
-
- "save": User saved/bookmarked the item
|
|
17
|
-
- "dismiss": User dismissed the item
|
|
18
|
-
- "mark_irrelevant": User marked item as not relevant
|
|
15
|
+
- "click": User clicked/opened the item
|
|
16
|
+
- "save": User saved/bookmarked the item
|
|
17
|
+
- "dismiss": User dismissed the item
|
|
18
|
+
- "mark_irrelevant": User marked item as not relevant
|
|
19
19
|
|
|
20
|
-
This
|
|
20
|
+
This records what happened; it does not train content preferences (interaction counts only graduate scoring's bootstrap gate).`,
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: "object",
|
|
23
23
|
properties: {
|
|
@@ -58,4 +58,3 @@ export function executeRecordFeedback(db, params) {
|
|
|
58
58
|
}
|
|
59
59
|
return db.recordFeedback(params.item_id, params.source_type, params.action);
|
|
60
60
|
}
|
|
61
|
-
//# sourceMappingURL=record-feedback.js.map
|
|
@@ -80,4 +80,3 @@ export declare const upgradePlannerTool: {
|
|
|
80
80
|
};
|
|
81
81
|
export declare function executeUpgradePlanner(_db: FourDADatabase, params: UpgradePlannerParams, liveIntel: LiveIntelligence | null): Promise<UpgradePlanResult>;
|
|
82
82
|
export {};
|
|
83
|
-
//# sourceMappingURL=upgrade-planner.d.ts.map
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - When no vulnerability scan has run, the plan says so instead of silently
|
|
18
18
|
* producing a CVE-blind ranking.
|
|
19
19
|
*/
|
|
20
|
-
import { maxSemver } from "../live/semver-utils.js";
|
|
20
|
+
import { compareSemver, maxSemver } from "../live/semver-utils.js";
|
|
21
21
|
const PROVENANCE_NOTE = "Point-in-time heuristic from local lockfiles plus live registry/OSV lookups. " +
|
|
22
22
|
"Not the 4DA desktop app's cross-project, version-confirmed plan.";
|
|
23
23
|
export const upgradePlannerTool = {
|
|
@@ -136,8 +136,21 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
|
|
|
136
136
|
// Skip if no upgrade needed (no vuln, not deprecated, not behind => no reasons)
|
|
137
137
|
if (reasons.length === 0)
|
|
138
138
|
continue;
|
|
139
|
+
// Never recommend a downgrade: when the current version is a prerelease
|
|
140
|
+
// AHEAD of the latest stable (rsa 0.10.0-rc.18 vs stable 0.9.10), the
|
|
141
|
+
// stable line is not an upgrade target. Fall back to the highest vuln
|
|
142
|
+
// fix version that IS ahead of current, else no target ("review").
|
|
143
|
+
if (targetVersion &&
|
|
144
|
+
dep.currentVersion &&
|
|
145
|
+
compareSemver(dep.currentVersion, targetVersion) >= 0) {
|
|
146
|
+
const vulnFixesAhead = (vulns ?? [])
|
|
147
|
+
.map((v) => v.fixedVersion)
|
|
148
|
+
.filter((f) => Boolean(f))
|
|
149
|
+
.filter((f) => compareSemver(f, dep.currentVersion) > 0);
|
|
150
|
+
targetVersion = vulnFixesAhead.length > 0 ? maxSemver(vulnFixesAhead) : null;
|
|
151
|
+
}
|
|
139
152
|
const label = dep.versionsBehind?.label;
|
|
140
|
-
const upgradeType = !label || label === "up-to-date" ? "
|
|
153
|
+
const upgradeType = !label || label === "up-to-date" ? "unknown" : label;
|
|
141
154
|
recommendations.push({
|
|
142
155
|
package: dep.name,
|
|
143
156
|
ecosystem: dep.ecosystem,
|
|
@@ -245,4 +258,3 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
|
|
|
245
258
|
provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
|
|
246
259
|
};
|
|
247
260
|
}
|
|
248
|
-
//# sourceMappingURL=upgrade-planner.js.map
|
|
@@ -76,4 +76,3 @@ export declare const whatShouldIKnowTool: {
|
|
|
76
76
|
};
|
|
77
77
|
export declare function executeWhatShouldIKnow(db: FourDADatabase, params: WhatShouldIKnowParams): WhatShouldIKnowResult | Promise<WhatShouldIKnowResult>;
|
|
78
78
|
export {};
|
|
79
|
-
//# sourceMappingURL=what-should-i-know.d.ts.map
|
|
@@ -40,9 +40,13 @@ export const whatShouldIKnowTool = {
|
|
|
40
40
|
function getOpenDecisionWindows(db) {
|
|
41
41
|
try {
|
|
42
42
|
const rawDb = db.getRawDb();
|
|
43
|
-
return rawDb.prepare(
|
|
43
|
+
return rawDb.prepare(
|
|
44
|
+
// opened_at, not created_at — the wrong column name made this query
|
|
45
|
+
// throw (swallowed below) on EVERY schema in existence, so the
|
|
46
|
+
// decision-windows section had never returned a row for anyone.
|
|
47
|
+
`SELECT id, title, description, urgency
|
|
44
48
|
FROM decision_windows WHERE status = 'open'
|
|
45
|
-
ORDER BY urgency DESC,
|
|
49
|
+
ORDER BY urgency DESC, opened_at DESC
|
|
46
50
|
LIMIT 20`).all();
|
|
47
51
|
}
|
|
48
52
|
catch {
|
|
@@ -345,4 +349,3 @@ export function executeWhatShouldIKnow(db, params) {
|
|
|
345
349
|
// Provider configured -> hybrid wisdom (returns a Promise; dispatch awaits).
|
|
346
350
|
return getRelevantWisdomHybrid(db, task, files, embedConfig).then(({ wisdom, recall_mode }) => finalize(wisdom, recall_mode));
|
|
347
351
|
}
|
|
348
|
-
//# sourceMappingURL=what-should-i-know.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export interface ActiveTopic {
|
|
|
81
81
|
last_seen: string;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
84
|
+
* Retained compatibility shape for historic topic affinity data.
|
|
85
85
|
*/
|
|
86
86
|
export interface TopicAffinity {
|
|
87
87
|
topic: string;
|
|
@@ -92,7 +92,7 @@ export interface TopicAffinity {
|
|
|
92
92
|
total_exposures: number;
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
95
|
+
* Retained compatibility shape for historic anti-topic data.
|
|
96
96
|
*/
|
|
97
97
|
export interface AntiTopic {
|
|
98
98
|
topic: string;
|
|
@@ -394,4 +394,3 @@ export interface AgentFeedbackTopItemRow {
|
|
|
394
394
|
used_count: number;
|
|
395
395
|
rejected_count: number;
|
|
396
396
|
}
|
|
397
|
-
//# sourceMappingURL=types.d.ts.map
|