@leclabs/agent-flow-navigator-mcp 1.3.0 → 1.4.1
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/engine.js +106 -21
- package/index.js +149 -55
- package/package.json +1 -1
- package/store.js +32 -4
- package/types.d.ts +37 -0
package/engine.js
CHANGED
|
@@ -77,7 +77,16 @@ const WORKFLOW_EMOJIS = {
|
|
|
77
77
|
/**
|
|
78
78
|
* Build formatted task subject for write-through
|
|
79
79
|
*/
|
|
80
|
-
export function buildTaskSubject(
|
|
80
|
+
export function buildTaskSubject(
|
|
81
|
+
taskId,
|
|
82
|
+
userDescription,
|
|
83
|
+
workflowType,
|
|
84
|
+
stepId,
|
|
85
|
+
subagent,
|
|
86
|
+
terminal,
|
|
87
|
+
maxRetries,
|
|
88
|
+
retryCount
|
|
89
|
+
) {
|
|
81
90
|
const emoji = WORKFLOW_EMOJIS[workflowType] || "";
|
|
82
91
|
const line1 = `#${taskId} ${userDescription}${emoji ? ` ${emoji}` : ""}`;
|
|
83
92
|
|
|
@@ -118,7 +127,13 @@ export function getBaselineInstructions(stepId, stepName) {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
// Analysis/Requirements steps
|
|
121
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
id.includes("analyze") ||
|
|
132
|
+
id.includes("analysis") ||
|
|
133
|
+
id.includes("parse") ||
|
|
134
|
+
id.includes("requirements") ||
|
|
135
|
+
name.includes("analyze")
|
|
136
|
+
) {
|
|
122
137
|
return "Review the task requirements carefully. Identify key constraints, dependencies, and acceptance criteria. Create a clear plan before proceeding.";
|
|
123
138
|
}
|
|
124
139
|
if (id.includes("plan") || id.includes("design") || name.includes("plan")) {
|
|
@@ -173,13 +188,44 @@ export function getBaselineInstructions(stepId, stepName) {
|
|
|
173
188
|
return "Complete this step thoroughly. Document your findings and any decisions made.";
|
|
174
189
|
}
|
|
175
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a context_files entry to an absolute path.
|
|
193
|
+
*
|
|
194
|
+
* Convention (follows Claude Code plugin path rules):
|
|
195
|
+
* - "./path" → relative to the workflow's source root (plugin root, project root, etc.)
|
|
196
|
+
* - "path" → relative to projectRoot
|
|
197
|
+
*
|
|
198
|
+
* @param {string} file - Context file entry
|
|
199
|
+
* @param {string} projectRoot - Project root directory
|
|
200
|
+
* @param {string|null} sourceRoot - Root directory of the workflow's source
|
|
201
|
+
* @returns {string} Absolute file path
|
|
202
|
+
*/
|
|
203
|
+
export function resolveContextFile(file, projectRoot, sourceRoot) {
|
|
204
|
+
if (file.startsWith("./") && sourceRoot) {
|
|
205
|
+
return join(sourceRoot, file);
|
|
206
|
+
}
|
|
207
|
+
return join(projectRoot, file);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve ./ prefixed paths in prose text against sourceRoot.
|
|
212
|
+
* Leaves text unchanged when sourceRoot is null or text has no ./ references.
|
|
213
|
+
* @param {string|null} text - Prose text that may contain ./ paths
|
|
214
|
+
* @param {string|null} sourceRoot - Root directory for ./ resolution
|
|
215
|
+
* @returns {string|null} Text with ./ paths resolved to absolute paths
|
|
216
|
+
*/
|
|
217
|
+
export function resolveProseRefs(text, sourceRoot) {
|
|
218
|
+
if (!text || !sourceRoot) return text;
|
|
219
|
+
return text.replace(/\.\/[\w\-./]+/g, (match) => join(sourceRoot, match));
|
|
220
|
+
}
|
|
221
|
+
|
|
176
222
|
/**
|
|
177
223
|
* Build context loading instructions from step-level context_files.
|
|
178
224
|
* Returns a markdown section or null if no context declared.
|
|
179
225
|
*/
|
|
180
|
-
export function buildContextInstructions({ contextFiles, projectRoot }) {
|
|
226
|
+
export function buildContextInstructions({ contextFiles, projectRoot, sourceRoot }) {
|
|
181
227
|
if (!contextFiles?.length || !projectRoot) return null;
|
|
182
|
-
const lines = contextFiles.map((file) => `- Read file: ${
|
|
228
|
+
const lines = contextFiles.map((file) => `- Read file: ${resolveContextFile(file, projectRoot, sourceRoot)}`);
|
|
183
229
|
return `## Context\n\nBefore beginning, load the following:\n${lines.join("\n")}`;
|
|
184
230
|
}
|
|
185
231
|
|
|
@@ -187,7 +233,15 @@ export function buildContextInstructions({ contextFiles, projectRoot }) {
|
|
|
187
233
|
* Build orchestrator instructions for task creation/update
|
|
188
234
|
* Returns null for terminal nodes (no further work)
|
|
189
235
|
*/
|
|
190
|
-
function buildOrchestratorInstructions(
|
|
236
|
+
function buildOrchestratorInstructions(
|
|
237
|
+
workflowType,
|
|
238
|
+
stepId,
|
|
239
|
+
stage,
|
|
240
|
+
subagent,
|
|
241
|
+
stepInstructions,
|
|
242
|
+
description,
|
|
243
|
+
contextBlock
|
|
244
|
+
) {
|
|
191
245
|
if (!stepInstructions) return null; // Terminal nodes have no instructions
|
|
192
246
|
|
|
193
247
|
const delegationPrefix = subagent ? `Invoke ${subagent} to complete the following task: ` : "";
|
|
@@ -212,25 +266,27 @@ function buildNavigateResponse(
|
|
|
212
266
|
retryCount = 0,
|
|
213
267
|
description = null,
|
|
214
268
|
resetRetryCount = false,
|
|
215
|
-
projectRoot = null
|
|
269
|
+
projectRoot = null,
|
|
270
|
+
sourceRoot = null
|
|
216
271
|
) {
|
|
217
272
|
const stage = stepDef.stage || null;
|
|
218
273
|
const subagent = stepDef.agent ? toSubagentRef(stepDef.agent) : null;
|
|
219
274
|
|
|
220
275
|
// Build step instructions from workflow definition + baseline
|
|
276
|
+
// Resolve ./ paths in prose fields against sourceRoot (same convention as context_files)
|
|
221
277
|
const isTerminal = isTerminalNode(stepDef);
|
|
222
278
|
const stepInstructions = isTerminal
|
|
223
279
|
? null
|
|
224
280
|
: {
|
|
225
281
|
name: stepDef.name || stepId,
|
|
226
|
-
description: stepDef.description || null,
|
|
227
|
-
guidance: stepDef.instructions || getBaselineInstructions(stepId, stepDef.name),
|
|
282
|
+
description: resolveProseRefs(stepDef.description, sourceRoot) || null,
|
|
283
|
+
guidance: resolveProseRefs(stepDef.instructions, sourceRoot) || getBaselineInstructions(stepId, stepDef.name),
|
|
228
284
|
};
|
|
229
285
|
|
|
230
286
|
// Build context block from step-level context_files
|
|
231
287
|
const contextBlock = isTerminal
|
|
232
288
|
? null
|
|
233
|
-
: buildContextInstructions({ contextFiles: stepDef.context_files, projectRoot });
|
|
289
|
+
: buildContextInstructions({ contextFiles: stepDef.context_files, projectRoot, sourceRoot });
|
|
234
290
|
|
|
235
291
|
// Build orchestrator instructions for all non-terminal actions
|
|
236
292
|
const orchestratorInstructions = isTerminal
|
|
@@ -243,11 +299,7 @@ function buildNavigateResponse(
|
|
|
243
299
|
const metadata = {
|
|
244
300
|
workflowType,
|
|
245
301
|
currentStep: stepId,
|
|
246
|
-
retryCount: retriesIncremented
|
|
247
|
-
? retryCount + 1
|
|
248
|
-
: action === "start" || resetRetryCount
|
|
249
|
-
? 0
|
|
250
|
-
: retryCount,
|
|
302
|
+
retryCount: retriesIncremented ? retryCount + 1 : action === "start" || resetRetryCount ? 0 : retryCount,
|
|
251
303
|
};
|
|
252
304
|
|
|
253
305
|
return {
|
|
@@ -261,6 +313,7 @@ function buildNavigateResponse(
|
|
|
261
313
|
maxRetries: stepDef.maxRetries || 0,
|
|
262
314
|
orchestratorInstructions,
|
|
263
315
|
metadata,
|
|
316
|
+
sourceRoot,
|
|
264
317
|
};
|
|
265
318
|
}
|
|
266
319
|
|
|
@@ -469,6 +522,9 @@ export class WorkflowEngine {
|
|
|
469
522
|
throw new Error(`Workflow '${workflowType}' must have nodes`);
|
|
470
523
|
}
|
|
471
524
|
|
|
525
|
+
// Resolve source root for context_files with ./ prefix
|
|
526
|
+
const sourceRoot = this.store.getSourceRoot?.(workflowType) || null;
|
|
527
|
+
|
|
472
528
|
const { nodes } = wfDef;
|
|
473
529
|
|
|
474
530
|
// Case 1: No currentStep - start at first work step
|
|
@@ -489,7 +545,18 @@ export class WorkflowEngine {
|
|
|
489
545
|
throw new Error(`First step '${firstEdge.to}' not found in workflow`);
|
|
490
546
|
}
|
|
491
547
|
|
|
492
|
-
return buildNavigateResponse(
|
|
548
|
+
return buildNavigateResponse(
|
|
549
|
+
workflowType,
|
|
550
|
+
firstEdge.to,
|
|
551
|
+
firstStepDef,
|
|
552
|
+
"start",
|
|
553
|
+
false,
|
|
554
|
+
0,
|
|
555
|
+
description,
|
|
556
|
+
false,
|
|
557
|
+
projectRoot,
|
|
558
|
+
sourceRoot
|
|
559
|
+
);
|
|
493
560
|
}
|
|
494
561
|
|
|
495
562
|
// Case 2: currentStep but no result - return current state
|
|
@@ -499,7 +566,18 @@ export class WorkflowEngine {
|
|
|
499
566
|
throw new Error(`Step '${currentStep}' not found in workflow '${workflowType}'`);
|
|
500
567
|
}
|
|
501
568
|
|
|
502
|
-
return buildNavigateResponse(
|
|
569
|
+
return buildNavigateResponse(
|
|
570
|
+
workflowType,
|
|
571
|
+
currentStep,
|
|
572
|
+
stepDef,
|
|
573
|
+
"current",
|
|
574
|
+
false,
|
|
575
|
+
retryCount,
|
|
576
|
+
description,
|
|
577
|
+
false,
|
|
578
|
+
projectRoot,
|
|
579
|
+
sourceRoot
|
|
580
|
+
);
|
|
503
581
|
}
|
|
504
582
|
|
|
505
583
|
// Case 3: currentStep and result - advance to next step
|
|
@@ -546,7 +624,8 @@ export class WorkflowEngine {
|
|
|
546
624
|
retryCount,
|
|
547
625
|
description,
|
|
548
626
|
resetRetryCount,
|
|
549
|
-
projectRoot
|
|
627
|
+
projectRoot,
|
|
628
|
+
sourceRoot
|
|
550
629
|
);
|
|
551
630
|
|
|
552
631
|
// Write-through: persist state transition and presentation to task file
|
|
@@ -556,13 +635,19 @@ export class WorkflowEngine {
|
|
|
556
635
|
const userDesc = task.metadata?.userDescription || "";
|
|
557
636
|
task.metadata = { ...task.metadata, ...response.metadata };
|
|
558
637
|
task.subject = buildTaskSubject(
|
|
559
|
-
task.id,
|
|
560
|
-
|
|
561
|
-
response.
|
|
638
|
+
task.id,
|
|
639
|
+
userDesc,
|
|
640
|
+
response.metadata.workflowType,
|
|
641
|
+
response.currentStep,
|
|
642
|
+
response.subagent,
|
|
643
|
+
response.terminal,
|
|
644
|
+
response.maxRetries,
|
|
645
|
+
response.metadata.retryCount
|
|
562
646
|
);
|
|
563
647
|
task.activeForm = buildTaskActiveForm(
|
|
564
648
|
response.stepInstructions?.name || response.currentStep,
|
|
565
|
-
response.subagent,
|
|
649
|
+
response.subagent,
|
|
650
|
+
response.terminal
|
|
566
651
|
);
|
|
567
652
|
if (response.orchestratorInstructions) {
|
|
568
653
|
task.description = response.orchestratorInstructions;
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - ListWorkflows: List available workflows
|
|
11
11
|
* - Diagram: Generate mermaid diagram for workflow
|
|
12
12
|
* - CopyWorkflows: Copy workflows from catalog to project
|
|
13
|
+
* - LoadWorkflows: Load workflows from a directory at runtime (external plugins or project reload)
|
|
13
14
|
* - ListCatalog: List workflows available in catalog
|
|
14
15
|
*/
|
|
15
16
|
|
|
@@ -45,30 +46,25 @@ const store = new WorkflowStore();
|
|
|
45
46
|
const engine = new WorkflowEngine(store);
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* Load workflows from
|
|
49
|
+
* Load workflows from catalog: flat {id}.json files
|
|
49
50
|
*/
|
|
50
|
-
function
|
|
51
|
+
function loadCatalogWorkflows(dirPath) {
|
|
51
52
|
if (!existsSync(dirPath)) return [];
|
|
52
53
|
|
|
53
54
|
const loaded = [];
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
if (!entry.isDirectory()) continue;
|
|
58
|
-
|
|
59
|
-
const id = entry.name;
|
|
60
|
-
const workflowFile = join(dirPath, id, "workflow.json");
|
|
55
|
+
const files = readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const id = file.replace(".json", "");
|
|
63
59
|
|
|
64
60
|
try {
|
|
65
|
-
const content = JSON.parse(readFileSync(
|
|
61
|
+
const content = JSON.parse(readFileSync(join(dirPath, file), "utf-8"));
|
|
66
62
|
if (validateWorkflow(id, content)) {
|
|
67
|
-
store.loadDefinition(id, content, "
|
|
63
|
+
store.loadDefinition(id, content, "catalog");
|
|
68
64
|
loaded.push(id);
|
|
69
65
|
}
|
|
70
66
|
} catch (e) {
|
|
71
|
-
console.error(`Error loading workflow ${id}: ${e.message}`);
|
|
67
|
+
console.error(`Error loading catalog workflow ${id}: ${e.message}`);
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
|
|
@@ -76,25 +72,48 @@ function loadProjectWorkflows(dirPath) {
|
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/**
|
|
79
|
-
* Load workflows from
|
|
75
|
+
* Load external workflows from a directory: flat {id}.json files
|
|
76
|
+
* Used by the LoadWorkflows tool at runtime.
|
|
77
|
+
* @param {string} dirPath - Directory containing {id}.json workflow files
|
|
78
|
+
* @param {string} sourceRoot - Root path for resolving ./ context_files
|
|
79
|
+
* @returns {string[]} Array of loaded workflow IDs
|
|
80
80
|
*/
|
|
81
|
-
function
|
|
81
|
+
function loadExternalWorkflows(dirPath, sourceRoot) {
|
|
82
82
|
if (!existsSync(dirPath)) return [];
|
|
83
83
|
|
|
84
84
|
const loaded = [];
|
|
85
|
-
const
|
|
85
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
86
86
|
|
|
87
|
-
for (const
|
|
88
|
-
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
// Flat format: {id}.json
|
|
89
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
90
|
+
const id = entry.name.replace(".json", "");
|
|
91
|
+
try {
|
|
92
|
+
const content = JSON.parse(readFileSync(join(dirPath, entry.name), "utf-8"));
|
|
93
|
+
if (validateWorkflow(id, content)) {
|
|
94
|
+
store.loadDefinition(id, content, "external", sourceRoot);
|
|
95
|
+
loaded.push(id);
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`Error loading external workflow ${id}: ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
89
102
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
// Directory format: {id}/workflow.json
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
const wfFile = join(dirPath, entry.name, "workflow.json");
|
|
106
|
+
if (!existsSync(wfFile)) continue;
|
|
107
|
+
const id = entry.name;
|
|
108
|
+
try {
|
|
109
|
+
const content = JSON.parse(readFileSync(wfFile, "utf-8"));
|
|
110
|
+
if (validateWorkflow(id, content)) {
|
|
111
|
+
store.loadDefinition(id, content, "external", sourceRoot);
|
|
112
|
+
loaded.push(id);
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error(`Error loading external workflow ${id}: ${e.message}`);
|
|
95
116
|
}
|
|
96
|
-
} catch (e) {
|
|
97
|
-
console.error(`Error loading catalog workflow ${id}: ${e.message}`);
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
|
|
@@ -108,16 +127,9 @@ function loadWorkflows() {
|
|
|
108
127
|
const catalogPath = join(CATALOG_PATH, "workflows");
|
|
109
128
|
const catalogLoaded = loadCatalogWorkflows(catalogPath);
|
|
110
129
|
|
|
111
|
-
const projectLoaded = existsSync(WORKFLOWS_PATH) ? loadProjectWorkflows(WORKFLOWS_PATH) : [];
|
|
112
|
-
|
|
113
|
-
// Determine which IDs came from where (project overwrites catalog)
|
|
114
|
-
const fromCatalog = catalogLoaded.filter((id) => !projectLoaded.includes(id));
|
|
115
|
-
const fromProject = projectLoaded;
|
|
116
|
-
|
|
117
130
|
return {
|
|
118
|
-
catalog:
|
|
119
|
-
|
|
120
|
-
loaded: [...new Set([...catalogLoaded, ...projectLoaded])],
|
|
131
|
+
catalog: catalogLoaded,
|
|
132
|
+
loaded: catalogLoaded,
|
|
121
133
|
};
|
|
122
134
|
}
|
|
123
135
|
|
|
@@ -209,6 +221,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
209
221
|
},
|
|
210
222
|
},
|
|
211
223
|
},
|
|
224
|
+
{
|
|
225
|
+
name: "LoadWorkflows",
|
|
226
|
+
description:
|
|
227
|
+
"Load workflows at runtime. External plugins pass path + sourceRoot. For project workflows, pass workflowIds to load specific workflows from .flow/workflows/.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
path: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Directory containing {id}.json workflow files. For external plugins only.",
|
|
234
|
+
},
|
|
235
|
+
sourceRoot: {
|
|
236
|
+
type: "string",
|
|
237
|
+
description:
|
|
238
|
+
"Root path for resolving ./ context_files entries. Defaults to path. For external plugins only.",
|
|
239
|
+
},
|
|
240
|
+
workflowIds: {
|
|
241
|
+
type: "array",
|
|
242
|
+
items: { type: "string" },
|
|
243
|
+
description:
|
|
244
|
+
"Specific workflow IDs to load from .flow/workflows/. Required when loading project workflows (no path). Omit to list available workflows without loading.",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
212
249
|
{
|
|
213
250
|
name: "ListCatalog",
|
|
214
251
|
description: "List workflows available in the catalog.",
|
|
@@ -337,6 +374,81 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
337
374
|
});
|
|
338
375
|
}
|
|
339
376
|
|
|
377
|
+
case "LoadWorkflows": {
|
|
378
|
+
if (args.path) {
|
|
379
|
+
// External plugin: load all from provided path
|
|
380
|
+
const dirPath = resolve(args.path);
|
|
381
|
+
const root = args.sourceRoot ? resolve(args.sourceRoot) : dirPath;
|
|
382
|
+
const loaded = loadExternalWorkflows(dirPath, root);
|
|
383
|
+
return jsonResponse({
|
|
384
|
+
schemaVersion: 2,
|
|
385
|
+
loaded,
|
|
386
|
+
source: "external",
|
|
387
|
+
sourceRoot: root,
|
|
388
|
+
path: dirPath,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Project: require explicit workflowIds or list available
|
|
393
|
+
if (!existsSync(WORKFLOWS_PATH)) {
|
|
394
|
+
return jsonResponse({
|
|
395
|
+
schemaVersion: 2,
|
|
396
|
+
available: [],
|
|
397
|
+
loaded: [],
|
|
398
|
+
source: "project",
|
|
399
|
+
path: WORKFLOWS_PATH,
|
|
400
|
+
hint: "No .flow/workflows/ directory found. Use /flow:init to set up workflows.",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const available = readdirSync(WORKFLOWS_PATH, { withFileTypes: true })
|
|
405
|
+
.filter((e) => e.isDirectory() && existsSync(join(WORKFLOWS_PATH, e.name, "workflow.json")))
|
|
406
|
+
.map((e) => e.name);
|
|
407
|
+
|
|
408
|
+
if (!args.workflowIds || args.workflowIds.length === 0) {
|
|
409
|
+
// List only — don't load
|
|
410
|
+
return jsonResponse({
|
|
411
|
+
schemaVersion: 2,
|
|
412
|
+
available,
|
|
413
|
+
loaded: [],
|
|
414
|
+
source: "project",
|
|
415
|
+
path: WORKFLOWS_PATH,
|
|
416
|
+
hint: "Pass workflowIds to load specific workflows.",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Load only the requested workflows
|
|
421
|
+
const loaded = [];
|
|
422
|
+
const errors = [];
|
|
423
|
+
for (const id of args.workflowIds) {
|
|
424
|
+
if (!available.includes(id)) {
|
|
425
|
+
errors.push({ id, error: "not found in .flow/workflows/" });
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const wfFile = join(WORKFLOWS_PATH, id, "workflow.json");
|
|
429
|
+
try {
|
|
430
|
+
const content = JSON.parse(readFileSync(wfFile, "utf-8"));
|
|
431
|
+
if (validateWorkflow(id, content)) {
|
|
432
|
+
store.loadDefinition(id, content, "project");
|
|
433
|
+
loaded.push(id);
|
|
434
|
+
} else {
|
|
435
|
+
errors.push({ id, error: "invalid schema" });
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
errors.push({ id, error: e.message });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return jsonResponse({
|
|
443
|
+
schemaVersion: 2,
|
|
444
|
+
available,
|
|
445
|
+
loaded,
|
|
446
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
447
|
+
source: "project",
|
|
448
|
+
path: WORKFLOWS_PATH,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
340
452
|
case "ListCatalog": {
|
|
341
453
|
const catalogPath = join(CATALOG_PATH, "workflows");
|
|
342
454
|
if (!existsSync(catalogPath)) {
|
|
@@ -371,31 +483,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
371
483
|
}
|
|
372
484
|
});
|
|
373
485
|
|
|
374
|
-
|
|
375
|
-
* Ensure .flow/README.md exists (created on MCP server connect)
|
|
376
|
-
*/
|
|
377
|
-
function ensureFlowReadme() {
|
|
378
|
-
const readmePath = join(FLOW_PATH, "README.md");
|
|
379
|
-
if (!existsSync(readmePath)) {
|
|
380
|
-
mkdirSync(FLOW_PATH, { recursive: true });
|
|
381
|
-
writeFileSync(readmePath, generateFlowReadme());
|
|
382
|
-
console.error(` Created: ${readmePath}`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Load workflows and start server
|
|
486
|
+
// Load catalog workflows and start server
|
|
387
487
|
const workflowInfo = loadWorkflows();
|
|
388
|
-
ensureFlowReadme();
|
|
389
488
|
|
|
390
489
|
const transport = new StdioServerTransport();
|
|
391
490
|
await server.connect(transport);
|
|
392
491
|
|
|
393
492
|
console.error(`Navigator MCP Server v2 running (stateless)`);
|
|
394
493
|
console.error(` Project: ${PROJECT_ROOT}`);
|
|
395
|
-
console.error(`
|
|
396
|
-
|
|
397
|
-
console.error(` Catalog: ${workflowInfo.catalog.join(", ")}`);
|
|
398
|
-
}
|
|
399
|
-
if (workflowInfo.project.length > 0) {
|
|
400
|
-
console.error(` Project: ${workflowInfo.project.join(", ")}`);
|
|
401
|
-
}
|
|
494
|
+
console.error(` Catalog: ${workflowInfo.catalog.length} workflows`);
|
|
495
|
+
console.error(` Project/external workflows: load via LoadWorkflows tool`);
|
package/package.json
CHANGED
package/store.js
CHANGED
|
@@ -29,19 +29,26 @@ export function validateWorkflow(id, content) {
|
|
|
29
29
|
export class WorkflowStore {
|
|
30
30
|
constructor() {
|
|
31
31
|
this.workflows = new Map();
|
|
32
|
-
this.sources = new Map(); // Track source: "catalog" | "project"
|
|
32
|
+
this.sources = new Map(); // Track source: "catalog" | "project" | "external"
|
|
33
|
+
this.sourceRoots = new Map(); // Track source root path per workflow (for ./ resolution)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Load a workflow definition into the store
|
|
37
38
|
* @param {string} id - Workflow identifier
|
|
38
39
|
* @param {Object} workflow - Workflow definition
|
|
39
|
-
* @param {string} source - Source: "catalog" | "project"
|
|
40
|
+
* @param {string} source - Source: "catalog" | "project" | "external"
|
|
41
|
+
* @param {string|null} sourceRoot - Root path for resolving ./ context_files
|
|
40
42
|
* @returns {string} The workflow id
|
|
41
43
|
*/
|
|
42
|
-
loadDefinition(id, workflow, source = "catalog") {
|
|
44
|
+
loadDefinition(id, workflow, source = "catalog", sourceRoot = null) {
|
|
43
45
|
this.workflows.set(id, workflow);
|
|
44
46
|
this.sources.set(id, source);
|
|
47
|
+
if (sourceRoot) {
|
|
48
|
+
this.sourceRoots.set(id, sourceRoot);
|
|
49
|
+
} else {
|
|
50
|
+
this.sourceRoots.delete(id);
|
|
51
|
+
}
|
|
45
52
|
return id;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -56,7 +63,7 @@ export class WorkflowStore {
|
|
|
56
63
|
|
|
57
64
|
/**
|
|
58
65
|
* List all loaded workflows with metadata
|
|
59
|
-
* @param {string} filter - Filter by source: "all" | "project" | "catalog"
|
|
66
|
+
* @param {string} filter - Filter by source: "all" | "project" | "catalog" | "external"
|
|
60
67
|
* @returns {Array} Array of workflow summaries
|
|
61
68
|
*/
|
|
62
69
|
listWorkflows(filter = "all") {
|
|
@@ -86,6 +93,17 @@ export class WorkflowStore {
|
|
|
86
93
|
return false;
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Check if any external workflows exist
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
hasExternalWorkflows() {
|
|
101
|
+
for (const source of this.sources.values()) {
|
|
102
|
+
if (source === "external") return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
/**
|
|
90
108
|
* Get the source of a workflow
|
|
91
109
|
* @param {string} id - Workflow identifier
|
|
@@ -95,6 +113,15 @@ export class WorkflowStore {
|
|
|
95
113
|
return this.sources.get(id);
|
|
96
114
|
}
|
|
97
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Get the source root path for a workflow (for ./ context_files resolution)
|
|
118
|
+
* @param {string} id - Workflow identifier
|
|
119
|
+
* @returns {string|undefined} Source root path or undefined
|
|
120
|
+
*/
|
|
121
|
+
getSourceRoot(id) {
|
|
122
|
+
return this.sourceRoots.get(id);
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
/**
|
|
99
126
|
* Check if a workflow exists
|
|
100
127
|
* @param {string} id - Workflow identifier
|
|
@@ -110,6 +137,7 @@ export class WorkflowStore {
|
|
|
110
137
|
clear() {
|
|
111
138
|
this.workflows.clear();
|
|
112
139
|
this.sources.clear();
|
|
140
|
+
this.sourceRoots.clear();
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
/**
|
package/types.d.ts
CHANGED
|
@@ -133,3 +133,40 @@ export type EdgeAction =
|
|
|
133
133
|
| "escalate" // Failed and exceeded retry limit
|
|
134
134
|
| "no_outgoing_edges" // Terminal node (no edges)
|
|
135
135
|
| "no_matching_edge"; // No edge matched the output
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Navigate Options
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
export interface NavigateOptions {
|
|
142
|
+
taskFilePath?: string;
|
|
143
|
+
workflowType?: string;
|
|
144
|
+
result?: "passed" | "failed";
|
|
145
|
+
description?: string;
|
|
146
|
+
projectRoot?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Context Resolution
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve a context_files entry to an absolute path.
|
|
155
|
+
* - "./path" → relative to sourceRoot (the workflow's source directory)
|
|
156
|
+
* - "path" → relative to projectRoot
|
|
157
|
+
*/
|
|
158
|
+
export function resolveContextFile(file: string, projectRoot: string, sourceRoot?: string | null): string;
|
|
159
|
+
|
|
160
|
+
export function resolveProseRefs(text: string | null, sourceRoot: string | null): string | null;
|
|
161
|
+
|
|
162
|
+
export function buildContextInstructions(options: {
|
|
163
|
+
contextFiles?: string[];
|
|
164
|
+
projectRoot?: string | null;
|
|
165
|
+
sourceRoot?: string | null;
|
|
166
|
+
}): string | null;
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Store Types
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
export type WorkflowSource = "catalog" | "project" | "external";
|