@4da/mcp-server 5.0.4 → 6.0.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 +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +233 -37
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
package/dist/tool-dispatch.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Tool Dispatch Registry
|
|
4
4
|
*
|
|
5
|
-
* Map-based dispatch for the
|
|
5
|
+
* Map-based dispatch for the 15 active tools.
|
|
6
6
|
* Adding a new tool = add to this map + schema-registry + barrel export.
|
|
7
7
|
*/
|
|
8
8
|
import { assertToolPermission } from "./auth-context.js";
|
|
9
|
-
import {
|
|
9
|
+
import { checkBuildStaleness } from "./build-staleness.js";
|
|
10
|
+
import { cleanStrings } from "./tool-args.js";
|
|
11
|
+
import { executeGetRelevantContent, executeGetContext, executeRecordFeedback, executeGetActionableSignals, executeKnowledgeGaps, executeDecisionMemory, executeCheckDecisionAlignment, executeAgentMemory, executeDeveloperDna, executeWhatShouldIKnow, executeVulnerabilityScan, executeEcosystemPulse, executeDependencyHealth, executeUpgradePlanner, executeDependencyCheck, executeUpgradeImpact, } from "./tools/index.js";
|
|
10
12
|
import { getLiveIntelligence } from "./live-singleton.js";
|
|
11
13
|
/**
|
|
12
14
|
* Dispatch map — tool name → execute function
|
|
@@ -16,13 +18,15 @@ const DISPATCH_MAP = {
|
|
|
16
18
|
vulnerability_scan: (db, params) => executeVulnerabilityScan(db, params, getLiveIntelligence()),
|
|
17
19
|
dependency_health: (db, params) => executeDependencyHealth(db, params, getLiveIntelligence()),
|
|
18
20
|
upgrade_planner: (db, params) => executeUpgradePlanner(db, params, getLiveIntelligence()),
|
|
21
|
+
dependency_check: (db, params) => executeDependencyCheck(db, params, getLiveIntelligence()),
|
|
22
|
+
upgrade_impact: (db, params) => executeUpgradeImpact(db, params, getLiveIntelligence()),
|
|
19
23
|
// Intelligence
|
|
20
24
|
what_should_i_know: executeWhatShouldIKnow,
|
|
21
25
|
ecosystem_pulse: (db, params) => executeEcosystemPulse(db, params, getLiveIntelligence()),
|
|
22
26
|
get_context: executeGetContext,
|
|
23
27
|
get_relevant_content: executeGetRelevantContent,
|
|
24
28
|
get_actionable_signals: executeGetActionableSignals,
|
|
25
|
-
knowledge_gaps: executeKnowledgeGaps,
|
|
29
|
+
knowledge_gaps: (db, params) => executeKnowledgeGaps(db, params, getLiveIntelligence()),
|
|
26
30
|
record_feedback: executeRecordFeedback,
|
|
27
31
|
// Decisions
|
|
28
32
|
decision_memory: executeDecisionMemory,
|
|
@@ -79,9 +83,24 @@ export async function dispatchTool(name, db, args) {
|
|
|
79
83
|
throw error;
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
|
-
|
|
86
|
+
// Feed freshness describes the desktop app's feed; a standalone install has
|
|
87
|
+
// none, and telling it to "run fourda-engine" is noise.
|
|
88
|
+
const withFreshness = FRESHNESS_TOOLS.has(name) && !db.isStandalone;
|
|
89
|
+
const payload = cleanStrings(withFreshness ? attachFreshness(db, result) : result);
|
|
90
|
+
// An executor that returns `{ error: "<message>" }` failed: say so in the
|
|
91
|
+
// protocol, not only in the body, so the host and model treat it as an error.
|
|
92
|
+
const failed = payload !== null && typeof payload === "object" && typeof payload.error === "string";
|
|
93
|
+
// The same payload as text AND as structuredContent: hosts disagree on which
|
|
94
|
+
// one reaches the model (Claude Code and VS Code read the structured form;
|
|
95
|
+
// Cursor, Zed and Gemini CLI read only the text), so neither may be partial.
|
|
96
|
+
// Compact JSON: indentation was ~20% of every response's tokens.
|
|
97
|
+
const structured = payload !== null && typeof payload === "object" && !Array.isArray(payload)
|
|
98
|
+
? { structuredContent: payload }
|
|
99
|
+
: {};
|
|
83
100
|
return {
|
|
84
|
-
content: [{ type: "text", text: JSON.stringify(payload
|
|
101
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
102
|
+
...structured,
|
|
103
|
+
...(failed ? { isError: true } : {}),
|
|
85
104
|
};
|
|
86
105
|
}
|
|
87
106
|
/**
|
|
@@ -98,13 +117,28 @@ function attachFreshness(db, result) {
|
|
|
98
117
|
catch {
|
|
99
118
|
return result;
|
|
100
119
|
}
|
|
120
|
+
// Repo checkouts only (null in the published package): when the running
|
|
121
|
+
// dist/ predates src/, every DB-backed answer reflects code the repo has
|
|
122
|
+
// already replaced. Saying so on the payload is what would have caught the
|
|
123
|
+
// 2026-08-30 two-day-stale server before its bugs were re-diagnosed.
|
|
124
|
+
let server_build;
|
|
125
|
+
try {
|
|
126
|
+
const staleness = checkBuildStaleness();
|
|
127
|
+
if (staleness?.stale && staleness.note) {
|
|
128
|
+
server_build = { stale: true, note: staleness.note };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Never let the self-check break a tool response.
|
|
133
|
+
}
|
|
134
|
+
const extras = server_build ? { server_build } : {};
|
|
101
135
|
if (Array.isArray(result)) {
|
|
102
|
-
return { data_freshness, item_count: result.length, items: result };
|
|
136
|
+
return { data_freshness, ...extras, item_count: result.length, items: result };
|
|
103
137
|
}
|
|
104
138
|
if (result && typeof result === "object") {
|
|
105
|
-
return { data_freshness, ...result };
|
|
139
|
+
return { data_freshness, ...extras, ...result };
|
|
106
140
|
}
|
|
107
|
-
return { data_freshness, result };
|
|
141
|
+
return { data_freshness, ...extras, result };
|
|
108
142
|
}
|
|
109
143
|
/** Check if a tool exists in the dispatch map */
|
|
110
144
|
export function hasDispatchTool(name) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 4DA app's persisted Upgrade Plan, read for `upgrade_planner` (AD-049).
|
|
3
|
+
*
|
|
4
|
+
* The desktop app writes its ranked plan to `kv_store` under
|
|
5
|
+
* `upgrade_plan_snapshot` after every plan compute (the same envelope
|
|
6
|
+
* `4da plan --json` prints). From schema 4 the envelope carries `steps`: a
|
|
7
|
+
* machine-readable work order keyed by plan item id, built in Rust from the
|
|
8
|
+
* same per-line targets as the item titles. This module only READS it — no
|
|
9
|
+
* range matching, target walking or scope grading is reimplemented here, so
|
|
10
|
+
* the agent gets the app's answer, not a second opinion.
|
|
11
|
+
*
|
|
12
|
+
* Fail-closed like the Rust reader: any schema version other than
|
|
13
|
+
* APP_PLAN_SCHEMA_VERSION is treated as absent and the caller falls back to
|
|
14
|
+
* its standalone heuristic.
|
|
15
|
+
*/
|
|
16
|
+
import type { FourDADatabase } from "../db.js";
|
|
17
|
+
/** The snapshot schema this reader understands (Rust `PLAN_SCHEMA_VERSION`). */
|
|
18
|
+
export declare const APP_PLAN_SCHEMA_VERSION = 4;
|
|
19
|
+
export interface AppPlanSite {
|
|
20
|
+
project: string;
|
|
21
|
+
direct: boolean;
|
|
22
|
+
dev: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface AppPlanLine {
|
|
25
|
+
installed: string;
|
|
26
|
+
target: string | null;
|
|
27
|
+
upgrade_type: "patch" | "minor" | "major" | null;
|
|
28
|
+
clears_all_known: boolean;
|
|
29
|
+
sites: AppPlanSite[];
|
|
30
|
+
}
|
|
31
|
+
export interface AppPlanStep {
|
|
32
|
+
item_id: string;
|
|
33
|
+
ecosystem: string;
|
|
34
|
+
package: string;
|
|
35
|
+
lines: AppPlanLine[];
|
|
36
|
+
mechanism: "manifest_bump" | "lockfile_or_parent_update" | "mixed" | "no_fix";
|
|
37
|
+
advisory_ids: string[];
|
|
38
|
+
verification: string;
|
|
39
|
+
}
|
|
40
|
+
/** A step with the title and urgency of the plan item it belongs to. */
|
|
41
|
+
export interface AppPlanStepView extends AppPlanStep {
|
|
42
|
+
title: string | null;
|
|
43
|
+
urgency: string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface AppPlanSnapshot {
|
|
46
|
+
schemaVersion: number;
|
|
47
|
+
generatedAt: string;
|
|
48
|
+
expiresAt: string;
|
|
49
|
+
generatorVersion: string | null;
|
|
50
|
+
sourceFreshness: string | null;
|
|
51
|
+
multiVersionCoverage: boolean | null;
|
|
52
|
+
validationDropCount: number | null;
|
|
53
|
+
itemCount: number;
|
|
54
|
+
steps: AppPlanStepView[];
|
|
55
|
+
}
|
|
56
|
+
export type AppPlanRead = {
|
|
57
|
+
kind: "plan";
|
|
58
|
+
snapshot: AppPlanSnapshot;
|
|
59
|
+
stale: boolean;
|
|
60
|
+
} | {
|
|
61
|
+
kind: "absent";
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Read the app's plan from the shared database. Never throws: a missing
|
|
66
|
+
* database, a standalone database without `kv_store`, no snapshot, bad JSON
|
|
67
|
+
* or another schema version all come back as `absent` with the reason.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readAppPlan(db: FourDADatabase | null | undefined, now?: Date): AppPlanRead;
|
|
70
|
+
export interface AppPlanResult {
|
|
71
|
+
generatedAt: string;
|
|
72
|
+
expiresAt: string;
|
|
73
|
+
/** True past `expiresAt`: the security data the plan read may have moved on. */
|
|
74
|
+
stale: boolean;
|
|
75
|
+
staleness: string | null;
|
|
76
|
+
totalSteps: number;
|
|
77
|
+
steps: AppPlanStepView[];
|
|
78
|
+
summary: string;
|
|
79
|
+
snapshot: Omit<AppPlanSnapshot, "steps" | "generatedAt" | "expiresAt">;
|
|
80
|
+
provenance: {
|
|
81
|
+
mode: "app_plan";
|
|
82
|
+
note: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Shape the app's plan as the tool result; `risk_threshold` / `max_recommendations` narrow it. */
|
|
86
|
+
export declare function formatAppPlan(read: Extract<AppPlanRead, {
|
|
87
|
+
kind: "plan";
|
|
88
|
+
}>, params: {
|
|
89
|
+
risk_threshold?: string;
|
|
90
|
+
max_recommendations?: number;
|
|
91
|
+
package?: string;
|
|
92
|
+
}): AppPlanResult;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The 4DA app's persisted Upgrade Plan, read for `upgrade_planner` (AD-049).
|
|
4
|
+
*
|
|
5
|
+
* The desktop app writes its ranked plan to `kv_store` under
|
|
6
|
+
* `upgrade_plan_snapshot` after every plan compute (the same envelope
|
|
7
|
+
* `4da plan --json` prints). From schema 4 the envelope carries `steps`: a
|
|
8
|
+
* machine-readable work order keyed by plan item id, built in Rust from the
|
|
9
|
+
* same per-line targets as the item titles. This module only READS it — no
|
|
10
|
+
* range matching, target walking or scope grading is reimplemented here, so
|
|
11
|
+
* the agent gets the app's answer, not a second opinion.
|
|
12
|
+
*
|
|
13
|
+
* Fail-closed like the Rust reader: any schema version other than
|
|
14
|
+
* APP_PLAN_SCHEMA_VERSION is treated as absent and the caller falls back to
|
|
15
|
+
* its standalone heuristic.
|
|
16
|
+
*/
|
|
17
|
+
/** The snapshot schema this reader understands (Rust `PLAN_SCHEMA_VERSION`). */
|
|
18
|
+
export const APP_PLAN_SCHEMA_VERSION = 4;
|
|
19
|
+
const PLAN_KV_KEY = "upgrade_plan_snapshot";
|
|
20
|
+
/**
|
|
21
|
+
* Read the app's plan from the shared database. Never throws: a missing
|
|
22
|
+
* database, a standalone database without `kv_store`, no snapshot, bad JSON
|
|
23
|
+
* or another schema version all come back as `absent` with the reason.
|
|
24
|
+
*/
|
|
25
|
+
export function readAppPlan(db, now = new Date()) {
|
|
26
|
+
if (!db)
|
|
27
|
+
return { kind: "absent", reason: "no 4DA database" };
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
const row = db
|
|
31
|
+
.getRawDb()
|
|
32
|
+
.prepare("SELECT value FROM kv_store WHERE key = ?")
|
|
33
|
+
.get(PLAN_KV_KEY);
|
|
34
|
+
raw = typeof row?.value === "string" ? row.value : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { kind: "absent", reason: "the database has no plan store (the 4DA app has not run on it)" };
|
|
38
|
+
}
|
|
39
|
+
if (raw === undefined) {
|
|
40
|
+
return { kind: "absent", reason: "the 4DA app has not computed an upgrade plan yet" };
|
|
41
|
+
}
|
|
42
|
+
let env;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
return { kind: "absent", reason: "the app's plan snapshot is unreadable" };
|
|
47
|
+
}
|
|
48
|
+
env = parsed;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { kind: "absent", reason: "the app's plan snapshot is unreadable" };
|
|
52
|
+
}
|
|
53
|
+
const version = env.schema_version;
|
|
54
|
+
if (version !== APP_PLAN_SCHEMA_VERSION) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "absent",
|
|
57
|
+
reason: `the app's plan snapshot is schema ${String(version)}, this server reads schema ${APP_PLAN_SCHEMA_VERSION}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(env.steps) || !Array.isArray(env.items)) {
|
|
61
|
+
return { kind: "absent", reason: "the app's plan snapshot has no work order" };
|
|
62
|
+
}
|
|
63
|
+
if (typeof env.generated_at !== "string" || typeof env.expires_at !== "string") {
|
|
64
|
+
return { kind: "absent", reason: "the app's plan snapshot has no freshness stamp" };
|
|
65
|
+
}
|
|
66
|
+
const items = new Map();
|
|
67
|
+
for (const item of env.items) {
|
|
68
|
+
if (item && typeof item.id === "string")
|
|
69
|
+
items.set(item.id, item);
|
|
70
|
+
}
|
|
71
|
+
const steps = env.steps.map((step) => {
|
|
72
|
+
const item = items.get(step.item_id);
|
|
73
|
+
return { ...step, title: item?.title ?? null, urgency: item?.urgency ?? null };
|
|
74
|
+
});
|
|
75
|
+
const expires = Date.parse(env.expires_at);
|
|
76
|
+
// An unparseable horizon cannot prove freshness: treat it as stale.
|
|
77
|
+
const stale = Number.isNaN(expires) || now.getTime() > expires;
|
|
78
|
+
return {
|
|
79
|
+
kind: "plan",
|
|
80
|
+
stale,
|
|
81
|
+
snapshot: {
|
|
82
|
+
schemaVersion: APP_PLAN_SCHEMA_VERSION,
|
|
83
|
+
generatedAt: env.generated_at,
|
|
84
|
+
expiresAt: env.expires_at,
|
|
85
|
+
generatorVersion: typeof env.generator_version === "string" ? env.generator_version : null,
|
|
86
|
+
sourceFreshness: typeof env.source_freshness === "string" ? env.source_freshness : null,
|
|
87
|
+
multiVersionCoverage: typeof env.multi_version_coverage === "boolean" ? env.multi_version_coverage : null,
|
|
88
|
+
validationDropCount: typeof env.validation_drop_count === "number" ? env.validation_drop_count : null,
|
|
89
|
+
itemCount: items.size,
|
|
90
|
+
steps,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const URGENCY_RISK = { critical: 3, high: 2, medium: 1, watch: 0 };
|
|
95
|
+
const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
96
|
+
const APP_PROVENANCE_NOTE = "The 4DA desktop app's own cross-project, version-confirmed Upgrade Plan, read from its database " +
|
|
97
|
+
"(the same plan the app shows and `4da plan --json` prints). Each step is keyed by the app's item id.";
|
|
98
|
+
/** Shape the app's plan as the tool result; `risk_threshold` / `max_recommendations` narrow it. */
|
|
99
|
+
export function formatAppPlan(read, params) {
|
|
100
|
+
const { snapshot, stale } = read;
|
|
101
|
+
const threshold = params.risk_threshold ?? "all";
|
|
102
|
+
let steps = snapshot.steps;
|
|
103
|
+
// `package` narrows the app's plan the same way it narrows the standalone one.
|
|
104
|
+
const wanted = params.package?.trim().toLowerCase();
|
|
105
|
+
if (wanted)
|
|
106
|
+
steps = steps.filter((s) => s.package.toLowerCase() === wanted);
|
|
107
|
+
if (threshold !== "all" && threshold in RISK_LEVELS) {
|
|
108
|
+
steps = steps.filter((s) => (URGENCY_RISK[s.urgency ?? ""] ?? 1) >= RISK_LEVELS[threshold]);
|
|
109
|
+
}
|
|
110
|
+
// The same default as the standalone plan (20). All 53 steps of a real plan
|
|
111
|
+
// came to ~11k tokens, past the size Claude Code warns about (2026-10-03).
|
|
112
|
+
const selected = steps.slice(0, params.max_recommendations ?? 20);
|
|
113
|
+
const count = (m) => selected.filter((s) => s.mechanism === m).length;
|
|
114
|
+
const majors = selected.filter((s) => s.lines.some((l) => l.upgrade_type === "major")).length;
|
|
115
|
+
const parts = [
|
|
116
|
+
`${selected.length} step${selected.length !== 1 ? "s" : ""} from the 4DA app's plan (generated ${snapshot.generatedAt})`,
|
|
117
|
+
];
|
|
118
|
+
if (count("manifest_bump") > 0)
|
|
119
|
+
parts.push(`${count("manifest_bump")} manifest bump(s)`);
|
|
120
|
+
if (count("lockfile_or_parent_update") > 0) {
|
|
121
|
+
parts.push(`${count("lockfile_or_parent_update")} lockfile or parent update(s) (transitive only)`);
|
|
122
|
+
}
|
|
123
|
+
if (count("mixed") > 0)
|
|
124
|
+
parts.push(`${count("mixed")} mixed (direct in some projects, transitive in others)`);
|
|
125
|
+
if (count("no_fix") > 0)
|
|
126
|
+
parts.push(`${count("no_fix")} with no fix published`);
|
|
127
|
+
if (majors > 0)
|
|
128
|
+
parts.push(`${majors} cross a major version`);
|
|
129
|
+
if (selected.length < snapshot.steps.length) {
|
|
130
|
+
parts.push(`${snapshot.steps.length} in the full plan (raise max_recommendations for the rest)`);
|
|
131
|
+
}
|
|
132
|
+
const staleness = stale
|
|
133
|
+
? `STALE: past its freshness horizon (${snapshot.expiresAt}). The advisories or installs may have changed since; open the 4DA app to recompute.`
|
|
134
|
+
: null;
|
|
135
|
+
if (staleness)
|
|
136
|
+
parts.push(staleness);
|
|
137
|
+
return {
|
|
138
|
+
generatedAt: snapshot.generatedAt,
|
|
139
|
+
expiresAt: snapshot.expiresAt,
|
|
140
|
+
stale,
|
|
141
|
+
staleness,
|
|
142
|
+
totalSteps: snapshot.steps.length,
|
|
143
|
+
steps: selected,
|
|
144
|
+
summary: parts.join(". ") + ".",
|
|
145
|
+
snapshot: {
|
|
146
|
+
schemaVersion: snapshot.schemaVersion,
|
|
147
|
+
generatorVersion: snapshot.generatorVersion,
|
|
148
|
+
sourceFreshness: snapshot.sourceFreshness,
|
|
149
|
+
multiVersionCoverage: snapshot.multiVersionCoverage,
|
|
150
|
+
validationDropCount: snapshot.validationDropCount,
|
|
151
|
+
itemCount: snapshot.itemCount,
|
|
152
|
+
},
|
|
153
|
+
provenance: { mode: "app_plan", note: APP_PROVENANCE_NOTE },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory-database rows in the 4DA feed that name a package a task touches.
|
|
3
|
+
*
|
|
4
|
+
* Only the feed's advisory sources count (`cve`, `osv`): their titles name the
|
|
5
|
+
* affected package. A headline that happens to contain a package name is not
|
|
6
|
+
* an advisory — that is how OpenAI-the-company news became "Critical: Security
|
|
7
|
+
* issue affects openai" (2026-10-01). Rows the relevance judge rejected
|
|
8
|
+
* (`feed_relevant = 0`) are left out. These rows are NOT matched to installed
|
|
9
|
+
* versions; the briefing says so, and with a ready scan keeps them only for
|
|
10
|
+
* packages the scan confirms vulnerable.
|
|
11
|
+
*/
|
|
12
|
+
import type { FourDADatabase } from "../db.js";
|
|
13
|
+
import type { TaskPackage } from "./briefing-task-scope.js";
|
|
14
|
+
export interface StoredAdvisory {
|
|
15
|
+
package: string;
|
|
16
|
+
title: string;
|
|
17
|
+
url: string | null;
|
|
18
|
+
priority: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function storedAdvisoriesFor(db: FourDADatabase, packages: TaskPackage[], limit?: number): StoredAdvisory[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Advisory-database rows in the 4DA feed that name a package a task touches.
|
|
4
|
+
*
|
|
5
|
+
* Only the feed's advisory sources count (`cve`, `osv`): their titles name the
|
|
6
|
+
* affected package. A headline that happens to contain a package name is not
|
|
7
|
+
* an advisory — that is how OpenAI-the-company news became "Critical: Security
|
|
8
|
+
* issue affects openai" (2026-10-01). Rows the relevance judge rejected
|
|
9
|
+
* (`feed_relevant = 0`) are left out. These rows are NOT matched to installed
|
|
10
|
+
* versions; the briefing says so, and with a ready scan keeps them only for
|
|
11
|
+
* packages the scan confirms vulnerable.
|
|
12
|
+
*/
|
|
13
|
+
import { normalizeStoredPriority } from "./signal-classifier.js";
|
|
14
|
+
const ADVISORY_SOURCES = ["cve", "osv"];
|
|
15
|
+
export function storedAdvisoriesFor(db, packages, limit = 5) {
|
|
16
|
+
if (packages.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
const raw = db.getRawDb();
|
|
19
|
+
const hasFeedRelevant = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return raw.prepare("PRAGMA table_info(source_items)").all().some((c) => c.name === "feed_relevant");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const pkg of packages) {
|
|
29
|
+
let rows;
|
|
30
|
+
try {
|
|
31
|
+
rows = raw
|
|
32
|
+
.prepare(`SELECT title, url, signal_priority FROM source_items
|
|
33
|
+
WHERE source_type IN (${ADVISORY_SOURCES.map(() => "?").join(", ")})
|
|
34
|
+
AND created_at > datetime('now', '-90 days')
|
|
35
|
+
AND LOWER(title) LIKE ?
|
|
36
|
+
${hasFeedRelevant ? "AND (feed_relevant IS NULL OR feed_relevant = 1)" : ""}
|
|
37
|
+
ORDER BY created_at DESC LIMIT 20`)
|
|
38
|
+
.all(...ADVISORY_SOURCES, `%${pkg.name.toLowerCase()}%`);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const word = new RegExp(`(^|[^\\w@/-])${pkg.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "i");
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
if (!word.test(row.title))
|
|
46
|
+
continue;
|
|
47
|
+
out.push({
|
|
48
|
+
package: pkg.name,
|
|
49
|
+
title: row.title,
|
|
50
|
+
url: row.url,
|
|
51
|
+
priority: row.signal_priority ? normalizeStoredPriority(row.signal_priority) : "medium",
|
|
52
|
+
});
|
|
53
|
+
if (out.length >= limit)
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of this project's dependencies a task touches, and what is known
|
|
3
|
+
* about each — the core of the what_should_i_know briefing.
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-10-01: asked about "bump fastembed 5 -> 7", the briefing
|
|
6
|
+
* returned eleven advisories and none about fastembed. It never asked which
|
|
7
|
+
* packages the task names; it merged feed items by recency and keyword class,
|
|
8
|
+
* so a 7-day-old `fastembed v7.1.0` release row was outside its 72-hour window
|
|
9
|
+
* while an arXiv dataset paper and three OpenAI-company headlines were in.
|
|
10
|
+
* This module starts from the task instead: the dependencies it names (exact
|
|
11
|
+
* package names from the resolved dependency set), then facts about exactly
|
|
12
|
+
* those packages.
|
|
13
|
+
*/
|
|
14
|
+
import type { FourDADatabase } from "../db.js";
|
|
15
|
+
import type { ResolvedDependency, VulnerabilityEntry } from "../live/types.js";
|
|
16
|
+
/** A dependency the task names, with every installed copy. */
|
|
17
|
+
export interface TaskPackage {
|
|
18
|
+
name: string;
|
|
19
|
+
ecosystem: string;
|
|
20
|
+
installed: string[];
|
|
21
|
+
direct: boolean;
|
|
22
|
+
dev: boolean;
|
|
23
|
+
/** Versions the task text names after the package ("5.x to 7.x" -> from 5.x, to 7.x). */
|
|
24
|
+
requested_from: string | null;
|
|
25
|
+
requested_to: string | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Package names that are also everyday words. One of these counts as named
|
|
29
|
+
* only with a qualifier beside it — "the next crate", "`next`", "upgrade
|
|
30
|
+
* next to 15" — so "the next step" does not drag the `next` package in.
|
|
31
|
+
*/
|
|
32
|
+
export declare const COMMON_WORDS: Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* The dependencies a task (and its file list) names. Exact names only; a
|
|
35
|
+
* common-word name needs a qualifier within two tokens or code quoting.
|
|
36
|
+
* Direct dependencies first; at most `limit` packages.
|
|
37
|
+
*/
|
|
38
|
+
export declare function detectTaskPackages(task: string, files: string[], deps: ResolvedDependency[], limit?: number): TaskPackage[];
|
|
39
|
+
/** Major version of a version or a `7.x` / `v7` request; null when unreadable. */
|
|
40
|
+
export declare function majorOf(version: string | null): number | null;
|
|
41
|
+
/** Actionable advisories on the installed copies of one package, highest presented severity first. */
|
|
42
|
+
export declare function advisoriesFor(pkg: TaskPackage, vulns: VulnerabilityEntry[]): {
|
|
43
|
+
id: string;
|
|
44
|
+
installed: string;
|
|
45
|
+
severity: "critical" | "high" | "low" | "medium" | "unknown";
|
|
46
|
+
fixed_version: string | null;
|
|
47
|
+
summary: string;
|
|
48
|
+
}[];
|
|
49
|
+
/**
|
|
50
|
+
* Registry release rows the desktop app's feed holds for one package, newer
|
|
51
|
+
* than the installed version. Matched on the registry's own title format
|
|
52
|
+
* (`crates.io: fastembed v7.1.0`), never on a free-text mention. Empty in
|
|
53
|
+
* standalone mode (no feed) or for ecosystems the feed does not watch.
|
|
54
|
+
*/
|
|
55
|
+
export declare function releasesFor(db: FourDADatabase, pkg: TaskPackage): Array<{
|
|
56
|
+
version: string;
|
|
57
|
+
seen: string;
|
|
58
|
+
url: string | null;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Feed items about the task's packages that the app's relevance judge
|
|
62
|
+
* accepted (latest `llm_judgments` verdict >= 0.5) and that the feed kept
|
|
63
|
+
* (`feed_relevant` = 1). Exact package-name match on a word boundary in the
|
|
64
|
+
* title. Third-party text: reported as data. Empty without the desktop feed.
|
|
65
|
+
*/
|
|
66
|
+
export declare function judgedReadingFor(db: FourDADatabase, packages: TaskPackage[], limit?: number): {
|
|
67
|
+
id: number;
|
|
68
|
+
title: string;
|
|
69
|
+
url: string | null;
|
|
70
|
+
source: string;
|
|
71
|
+
judge_relevance: number;
|
|
72
|
+
package: string;
|
|
73
|
+
}[];
|