secufusion-mcp 1.0.15 → 1.0.17
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/index.js +51 -10
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -842,8 +842,34 @@ server.tool("manage_project_spec", "Manages the .secufusion-project-spec.json fi
|
|
|
842
842
|
// ─── Task management helpers ──────────────────────────────────────────────────
|
|
843
843
|
const TASKS_DIR = ".secufusion/tasks";
|
|
844
844
|
const REGISTRY_FILE = ".secufusion/registry.json";
|
|
845
|
-
function
|
|
846
|
-
|
|
845
|
+
function resolveTaskFolder(workItemId, tasksRoot, registryData) {
|
|
846
|
+
const registry = registryData || readRegistry();
|
|
847
|
+
const entry = registry.find(t => t["work_item_id"] === workItemId);
|
|
848
|
+
// 1. Try registry first
|
|
849
|
+
if (entry && entry["folder"]) {
|
|
850
|
+
const folderPath = path.join(tasksRoot, entry["folder"]);
|
|
851
|
+
if (fs.existsSync(folderPath))
|
|
852
|
+
return folderPath;
|
|
853
|
+
}
|
|
854
|
+
// 2. Fallback: scan directory
|
|
855
|
+
if (fs.existsSync(tasksRoot)) {
|
|
856
|
+
const taskFolders = fs.readdirSync(tasksRoot);
|
|
857
|
+
const match = taskFolders.find(f => f.startsWith(workItemId + "-") || f === workItemId || f === `WI-${workItemId}`);
|
|
858
|
+
if (match)
|
|
859
|
+
return path.join(tasksRoot, match);
|
|
860
|
+
}
|
|
861
|
+
// 3. Not found
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
function generateNewTaskFolder(workItemId, title, tasksRoot) {
|
|
865
|
+
const slug = title.toLowerCase()
|
|
866
|
+
.replace(/[:\s]+/g, '-')
|
|
867
|
+
.replace(/[^a-z0-9-]+/g, '')
|
|
868
|
+
.replace(/-+/g, '-')
|
|
869
|
+
.replace(/^-|-$/g, '')
|
|
870
|
+
.slice(0, 50)
|
|
871
|
+
.replace(/-+$/, '');
|
|
872
|
+
return path.join(tasksRoot, `${workItemId}-${slug}`);
|
|
847
873
|
}
|
|
848
874
|
function readJson(filePath) {
|
|
849
875
|
const raw = readFileSafe(filePath);
|
|
@@ -892,7 +918,17 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
892
918
|
scenario_type: z.enum(["unit", "integration", "e2e", "manual"]).optional().describe("Test scenario type. Used with 'add_scenario'."),
|
|
893
919
|
}, async ({ action, work_item_id, title, description, acceptance_criteria, pending_acs, completed_acs, next_step, tags, file_path, change_summary, decision, rationale, scenario, scenario_type }) => {
|
|
894
920
|
const inputChars = JSON.stringify({ action, work_item_id, title, description, acceptance_criteria, pending_acs, completed_acs, next_step, tags, file_path, change_summary, decision, rationale, scenario, scenario_type }).length;
|
|
895
|
-
const
|
|
921
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
922
|
+
let dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
923
|
+
if (action === "initialize") {
|
|
924
|
+
if (!title)
|
|
925
|
+
return appendTelemetry({ isError: true, content: [{ type: "text", text: "ERROR: 'title' is required for action=initialize." }] }, inputChars);
|
|
926
|
+
if (!dir)
|
|
927
|
+
dir = generateNewTaskFolder(work_item_id, title, tasksRoot);
|
|
928
|
+
}
|
|
929
|
+
if (!dir && action !== "initialize") {
|
|
930
|
+
return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found in ${tasksRoot}. Has it been initialized?` }] }, inputChars);
|
|
931
|
+
}
|
|
896
932
|
const specPath = path.join(dir, "spec.json");
|
|
897
933
|
const progressPath = path.join(dir, "progress.json");
|
|
898
934
|
const decisionsPath = path.join(dir, "decisions.json");
|
|
@@ -912,7 +948,7 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
912
948
|
writeFile(filesPath, JSON.stringify([], null, 2));
|
|
913
949
|
writeFile(scenariosPath, JSON.stringify([], null, 2));
|
|
914
950
|
}
|
|
915
|
-
upsertRegistry({ work_item_id, title, status: "active", created_at: now, tags: tags || [] });
|
|
951
|
+
upsertRegistry({ work_item_id, title, status: "active", created_at: now, tags: tags || [], folder: path.basename(dir) });
|
|
916
952
|
return appendTelemetry({ content: [{ type: "text", text: `✅ Task WI-${work_item_id} initialized.\n\n**Folder:** ${dir}\n**Title:** ${title}\n**ACs:** ${(acceptance_criteria || []).length}\n\n${JSON.stringify(spec, null, 2)}` }] }, inputChars);
|
|
917
953
|
}
|
|
918
954
|
if (!fs.existsSync(specPath)) {
|
|
@@ -1014,9 +1050,10 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
1014
1050
|
// ─── Tool 6: get_task_history ─────────────────────────────────────────────────
|
|
1015
1051
|
server.tool("get_task_history", "Retrieve the full history of a past task — spec, decisions, files touched, and scenarios. Use before starting similar work to understand how it was done before.", { work_item_id: z.string().describe("Work item ID of the past task to retrieve.") }, async ({ work_item_id }) => {
|
|
1016
1052
|
const inputChars = JSON.stringify({ work_item_id }).length;
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1053
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
1054
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
1055
|
+
if (!dir) {
|
|
1056
|
+
return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found in ${tasksRoot}. Has it been initialized? Run manage_task action=initialize first.` }] }, inputChars);
|
|
1020
1057
|
}
|
|
1021
1058
|
const history = {
|
|
1022
1059
|
spec: readJson(path.join(dir, "spec.json")),
|
|
@@ -1038,10 +1075,13 @@ server.tool("search_tasks", "Search all past tasks by keyword. Scans registry an
|
|
|
1038
1075
|
return appendTelemetry({ content: [{ type: "text", text: "No tasks found in registry. Initialize tasks first." }] }, inputChars);
|
|
1039
1076
|
const terms = keywords.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1040
1077
|
const matches = [];
|
|
1078
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
1041
1079
|
for (const entry of registry) {
|
|
1042
1080
|
if (status_filter !== "any" && entry["status"] !== status_filter)
|
|
1043
1081
|
continue;
|
|
1044
|
-
const dir =
|
|
1082
|
+
const dir = resolveTaskFolder(entry["work_item_id"], tasksRoot, registry);
|
|
1083
|
+
if (!dir)
|
|
1084
|
+
continue;
|
|
1045
1085
|
const blobs = [
|
|
1046
1086
|
JSON.stringify(entry),
|
|
1047
1087
|
readFileSafe(path.join(dir, "spec.json")) || "",
|
|
@@ -1064,8 +1104,9 @@ server.tool("search_tasks", "Search all past tasks by keyword. Scans registry an
|
|
|
1064
1104
|
// ─── Tool 8: get_pattern_from_task ────────────────────────────────────────────
|
|
1065
1105
|
server.tool("get_pattern_from_task", "Extract reusable implementation patterns from a completed task — decisions made, files modified, and test scenarios used. Call this before implementing something similar to a past feature.", { work_item_id: z.string().describe("Work item ID of the past task to extract patterns from.") }, async ({ work_item_id }) => {
|
|
1066
1106
|
const inputChars = JSON.stringify({ work_item_id }).length;
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1107
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
1108
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
1109
|
+
if (!dir)
|
|
1069
1110
|
return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found. Cannot extract patterns.` }] }, inputChars);
|
|
1070
1111
|
const spec = readJson(path.join(dir, "spec.json")) || {};
|
|
1071
1112
|
const decisions = readJson(path.join(dir, "decisions.json")) || [];
|