Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions packages/flow/src/common/async-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,177 @@ export const PreferencesContext = new AsyncContext<Preferences>(
"PreferencesContext"
);

// =============================================================================
// Runtime Context (MCP vs CLI)
// =============================================================================

export type RuntimeMode = "mcp" | "cli";

export interface RuntimeInfo {
mode: RuntimeMode;
}

/**
* Async context for runtime mode
*/
export const RuntimeContext = new AsyncContext<RuntimeInfo>("RuntimeContext");

/**
* Get current runtime mode, defaults to "cli"
*/
export function getRuntimeMode(): RuntimeMode {
return RuntimeContext.tryGet()?.mode ?? "cli";
}

/**
* Check if running in MCP mode
*/
export function isMcpMode(): boolean {
return getRuntimeMode() === "mcp";
}

/**
* Check if running in CLI mode
*/
export function isCliMode(): boolean {
return getRuntimeMode() === "cli";
}

/**
* Run function within specified runtime mode
* Supports both sync and async functions
*/
export function withRuntimeMode<R>(mode: RuntimeMode, fn: () => R): R {
return RuntimeContext.run({ mode }, fn);
}

/**
* Run async function within specified runtime mode
* Use this for async functions to ensure context is preserved
*/
export async function withRuntimeModeAsync<R>(
mode: RuntimeMode,
fn: () => Promise<R>
): Promise<R> {
return RuntimeContext.run({ mode }, fn);
}

// =============================================================================
// Workflow Name Context
// =============================================================================

/**
* Async context for current workflow name (used by str template)
*/
export const WorkflowNameContext = new AsyncContext<string>("WorkflowNameContext");

/**
* Get current workflow name from context
*/
export function getWorkflowName(): string {
return WorkflowNameContext.tryGet() ?? "workflow";
}

// =============================================================================
// str`` Template Literal (mode-aware string builder)
// =============================================================================

/**
* str`` tagged template literal for mode-aware description generation.
*
* Returns `() => Promise<string>` - an async function that resolves the template.
*
* Values can be:
* - Static values (strings, numbers) - inserted directly
* - Functions `() => string | Promise<string>` - called and result inserted
*
* @example
* ```ts
* const description = str`
* Task management workflow.
*
* ## When to Use
* - Start new task → ${str.scenarios(["start", "--type", "service"])}
* - List labels → ${str.scenarios(["start", "--list-labels"])}
*
* ${str.data("Labels", labels)}
* `;
* ```
*/
export function str(
strings: TemplateStringsArray,
...values: unknown[]
): () => Promise<string> {
return async () => {
const parts: string[] = [];
for (let i = 0; i < strings.length; i++) {
parts.push(strings[i]);
if (i < values.length) {
const value = values[i];
if (typeof value === "function") {
const result = (value as () => unknown)();
parts.push(String(result instanceof Promise ? await result : result));
} else {
parts.push(String(value ?? ""));
}
}
}
return parts.join("");
};
}

/**
* Format a single scenario (command example) based on runtime mode.
* Workflow name is obtained from WorkflowNameContext.
*
* Returns `() => string` - a function that generates the formatted string.
*
* - MCP: `use("workflow", ["arg1", "arg2"])`
* - CLI: `workflow arg1 arg2`
*/
str.scenarios = (args: string[]): (() => string) => {
return () => {
const workflowName = getWorkflowName();
const mode = getRuntimeMode();

if (mode === "mcp") {
return `use("${workflowName}", ${JSON.stringify(args)})`;
} else {
return `${workflowName} ${args.join(" ")}`;
}
};
};

/**
* Format a data section (e.g., labels list) based on runtime mode.
*
* Returns `() => string` - a function that generates the formatted string.
*
* - MCP: `Title: item1, item2, item3`
* - CLI: `Title:\n item1 - description\n item2 - description`
*/
str.data = (
title: string,
items: Array<{ name: string; description?: string; color?: string }>
): (() => string) => {
return () => {
const mode = getRuntimeMode();

if (mode === "mcp") {
return `${title}: ${items.map((i) => i.name).join(", ")}`;
} else {
const lines = [`${title}:`];
for (const item of items) {
const parts = [` ${item.name}`];
if (item.color) parts.push(`(#${item.color})`);
if (item.description) parts.push(`- ${item.description}`);
lines.push(parts.join(" "));
}
return lines.join("\n");
}
};
};

// =============================================================================
// Helpers
// =============================================================================
Expand Down
27 changes: 22 additions & 5 deletions packages/flow/src/common/workflow/base-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { withPreferences } from "../async-context.ts";
import { withPreferences, WorkflowNameContext } from "../async-context.ts";

// =============================================================================
// Types
Expand Down Expand Up @@ -62,9 +62,19 @@ async function buildSubflowMap(subflows: SubflowDef[]): Promise<Map<string, Subf
return map;
}

/** Description can be static string, sync function, or async function */
export type DescriptionProvider = string | (() => string) | (() => Promise<string>);

/** Resolve description to string */
export async function resolveDescription(desc: DescriptionProvider): Promise<string> {
if (typeof desc === "string") return desc;
const result = desc();
return result instanceof Promise ? await result : result;
}

export interface WorkflowConfig<TArgs extends Record<string, ArgConfig>> {
name: string;
description: string;
description: DescriptionProvider;
version?: string;
args?: TArgs;
subflows?: SubflowDef[];
Expand All @@ -76,11 +86,16 @@ export interface WorkflowConfig<TArgs extends Record<string, ArgConfig>> {

export interface WorkflowMeta {
name: string;
description: string;
description: DescriptionProvider;
version: string;
args: Record<string, ArgConfig>;
}

/** Get resolved description string from WorkflowMeta */
export async function getMetaDescription(meta: WorkflowMeta): Promise<string> {
return resolveDescription(meta.description);
}

export interface WorkflowContext {
meta: WorkflowMeta;
path: string[];
Expand Down Expand Up @@ -241,12 +256,14 @@ async function printHelp<TArgs extends Record<string, ArgConfig>>(
}
opts.printed.add(id);

// Set workflow name context for str.scenarios() to use
const description = await WorkflowNameContext.run(meta.name, () => getMetaDescription(meta));
if (opts.indent === 0) {
console.log(`${meta.name} v${meta.version} - ${meta.description}`);
console.log(`${meta.name} v${meta.version} - ${description}`);
console.log();
console.log(`Usage: ${pathStr || meta.name} [subflow...] [options]`);
} else {
console.log(`${prefix}${meta.name} - ${meta.description}`);
console.log(`${prefix}${meta.name} - ${description}`);
}

const argEntries = Object.entries(meta.args);
Expand Down
55 changes: 33 additions & 22 deletions packages/flow/src/meta/meta.mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Meta MCP Server
*
* Provides workflow execution capability to AI agents.
* - workflow(name, args): Execute any workflow
* - use(name, args): Execute any workflow by name
* - reload(): Refresh workflow list and return updated description
* - buildMetaMcp(): Package workflows into an MCP server
*
Expand All @@ -23,6 +23,8 @@ import {
printMcpHelp,
z,
} from "../common/mcp/base-mcp.ts";
import { withRuntimeMode, WorkflowNameContext } from "../common/async-context.ts";
import { resolveDescription } from "../common/workflow/base-workflow.ts";

const MCP_NAME = "meta";

Expand Down Expand Up @@ -144,7 +146,13 @@ async function getWorkflowInfo(
else if (hasAi) mode = "ai";
else if (hasSubflows || hasDirectCommands) mode = "programmatic";

return { description: workflow.meta.description, mode };
// Resolve description with workflow name context
const description = await WorkflowNameContext.run(
workflow.meta.name,
() => resolveDescription(workflow.meta.description)
);

return { description, mode };
}
} catch {
// Ignore import errors
Expand All @@ -168,26 +176,28 @@ async function buildToolDescription(
directories: string[],
filter?: string[]
): Promise<string> {
let workflows = await scanWorkflows(directories);
// Run in MCP mode to get MCP-formatted descriptions
return withRuntimeMode("mcp", async () => {
let workflows = await scanWorkflows(directories);

if (filter && filter.length > 0) {
workflows = workflows.filter((w) => filter.includes(w.name));
}
if (filter && filter.length > 0) {
workflows = workflows.filter((w) => filter.includes(w.name));
}

const workflowList = workflows
.map((w) => {
const tags: string[] = [];
if (w.mode) tags.push(w.mode);
if (w.isUserOverride) tags.push("user");
const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
return `- ${w.name}: ${w.description}${tagStr}`;
})
.join("\n");
const workflowList = workflows
.map((w) => {
const tags: string[] = [];
if (w.mode) tags.push(w.mode);
if (w.isUserOverride) tags.push("user");
const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
return `- ${w.name}: ${w.description}${tagStr}`;
})
.join("\n");

// 动态生成 examples
const examples = workflows.slice(0, 3).map((w) => ` workflow("${w.name}", ["--help"])`).join("\n");
// 动态生成 examples
const examples = workflows.slice(0, 3).map((w) => ` use("${w.name}", ["--help"])`).join("\n");

return `Execute a workflow by name with arguments.
return `Execute a workflow by name with arguments.

## Usage
- Use \`--help\` to get detailed usage for any workflow
Expand All @@ -197,7 +207,8 @@ async function buildToolDescription(
${workflowList || "(none)"}

## Examples
${examples || ' workflow("<name>", ["--help"])'}`;
${examples || ' use("<name>", ["--help"])'}`;
});
}

// =============================================================================
Expand Down Expand Up @@ -295,8 +306,8 @@ async function executeWorkflow(
};
}

// Execute with args
await workflow.run(args);
// Execute with args in MCP mode
await withRuntimeMode("mcp", () => workflow.run(args));

return {
success: true,
Expand Down Expand Up @@ -341,7 +352,7 @@ async function createWorkflowTool(
const description = await buildToolDescription(directories, filter);

return defineTool({
name: "workflow",
name: "use",
description,
inputSchema: z.object({
name: z
Expand Down
2 changes: 1 addition & 1 deletion packages/flow/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

Loading