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.
Files changed (2) hide show
  1. package/index.js +51 -10
  2. 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 taskDir(workItemId) {
846
- return resolve(`${TASKS_DIR}/WI-${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}`);
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 dir = taskDir(work_item_id);
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 dir = taskDir(work_item_id);
1018
- if (!fs.existsSync(dir)) {
1019
- 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);
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 = taskDir(entry["work_item_id"]);
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 dir = taskDir(work_item_id);
1068
- if (!fs.existsSync(dir))
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")) || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.15",
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",