secufusion-mcp 1.0.16 → 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 -28
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -842,26 +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
|
-
const
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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}`);
|
|
865
873
|
}
|
|
866
874
|
function readJson(filePath) {
|
|
867
875
|
const raw = readFileSafe(filePath);
|
|
@@ -910,7 +918,17 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
910
918
|
scenario_type: z.enum(["unit", "integration", "e2e", "manual"]).optional().describe("Test scenario type. Used with 'add_scenario'."),
|
|
911
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 }) => {
|
|
912
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;
|
|
913
|
-
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
|
+
}
|
|
914
932
|
const specPath = path.join(dir, "spec.json");
|
|
915
933
|
const progressPath = path.join(dir, "progress.json");
|
|
916
934
|
const decisionsPath = path.join(dir, "decisions.json");
|
|
@@ -930,7 +948,7 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
930
948
|
writeFile(filesPath, JSON.stringify([], null, 2));
|
|
931
949
|
writeFile(scenariosPath, JSON.stringify([], null, 2));
|
|
932
950
|
}
|
|
933
|
-
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) });
|
|
934
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);
|
|
935
953
|
}
|
|
936
954
|
if (!fs.existsSync(specPath)) {
|
|
@@ -1032,9 +1050,10 @@ server.tool("manage_task", "Manages structured task records in .secufusion/tasks
|
|
|
1032
1050
|
// ─── Tool 6: get_task_history ─────────────────────────────────────────────────
|
|
1033
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 }) => {
|
|
1034
1052
|
const inputChars = JSON.stringify({ work_item_id }).length;
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
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);
|
|
1038
1057
|
}
|
|
1039
1058
|
const history = {
|
|
1040
1059
|
spec: readJson(path.join(dir, "spec.json")),
|
|
@@ -1056,10 +1075,13 @@ server.tool("search_tasks", "Search all past tasks by keyword. Scans registry an
|
|
|
1056
1075
|
return appendTelemetry({ content: [{ type: "text", text: "No tasks found in registry. Initialize tasks first." }] }, inputChars);
|
|
1057
1076
|
const terms = keywords.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1058
1077
|
const matches = [];
|
|
1078
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
1059
1079
|
for (const entry of registry) {
|
|
1060
1080
|
if (status_filter !== "any" && entry["status"] !== status_filter)
|
|
1061
1081
|
continue;
|
|
1062
|
-
const dir =
|
|
1082
|
+
const dir = resolveTaskFolder(entry["work_item_id"], tasksRoot, registry);
|
|
1083
|
+
if (!dir)
|
|
1084
|
+
continue;
|
|
1063
1085
|
const blobs = [
|
|
1064
1086
|
JSON.stringify(entry),
|
|
1065
1087
|
readFileSafe(path.join(dir, "spec.json")) || "",
|
|
@@ -1082,8 +1104,9 @@ server.tool("search_tasks", "Search all past tasks by keyword. Scans registry an
|
|
|
1082
1104
|
// ─── Tool 8: get_pattern_from_task ────────────────────────────────────────────
|
|
1083
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 }) => {
|
|
1084
1106
|
const inputChars = JSON.stringify({ work_item_id }).length;
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1107
|
+
const tasksRoot = resolve(TASKS_DIR);
|
|
1108
|
+
const dir = resolveTaskFolder(work_item_id, tasksRoot);
|
|
1109
|
+
if (!dir)
|
|
1087
1110
|
return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found. Cannot extract patterns.` }] }, inputChars);
|
|
1088
1111
|
const spec = readJson(path.join(dir, "spec.json")) || {};
|
|
1089
1112
|
const decisions = readJson(path.join(dir, "decisions.json")) || [];
|