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.
Files changed (2) hide show
  1. package/index.js +51 -28
  2. 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 taskDir(workItemId, title) {
846
- const baseDir = resolve(TASKS_DIR);
847
- const prefixes = [workItemId + "-", "WI-" + workItemId + "-"];
848
- if (fs.existsSync(baseDir)) {
849
- const folders = fs.readdirSync(baseDir);
850
- // Find an existing folder that starts with the workItemId
851
- const match = folders.find(f => f === workItemId ||
852
- f === `WI-${workItemId}` ||
853
- prefixes.some(p => f.startsWith(p)));
854
- if (match) {
855
- return resolve(`${TASKS_DIR}/${match}`);
856
- }
857
- }
858
- // If no folder exists, generate a new one
859
- if (title) {
860
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').substring(0, 50);
861
- return resolve(`${TASKS_DIR}/${workItemId}-${slug}`);
862
- }
863
- // Fallback if no title is provided and folder doesn't exist
864
- return resolve(`${TASKS_DIR}/${workItemId}`);
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 dir = taskDir(work_item_id, action === "initialize" ? title : undefined);
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 dir = taskDir(work_item_id);
1036
- if (!fs.existsSync(dir)) {
1037
- return appendTelemetry({ isError: true, content: [{ type: "text", text: `Task WI-${work_item_id} not found in ${resolve(TASKS_DIR)}.` }] }, inputChars);
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 = taskDir(entry["work_item_id"]);
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 dir = taskDir(work_item_id);
1086
- if (!fs.existsSync(dir))
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")) || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",