c0de-agent 1.4.0 → 1.5.0
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/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -1
- package/dist/core/slash.js +66 -4
- package/dist/core/workflow.d.ts +1 -1
- package/dist/core/workflow.js +5 -2
- package/dist/core/workflows/discovery.d.ts +23 -2
- package/dist/core/workflows/discovery.js +38 -2
- package/dist/core/workflows/index.d.ts +3 -2
- package/dist/core/workflows/index.js +2 -2
- package/dist/core/workflows/registry.d.ts +8 -1
- package/dist/core/workflows/registry.js +21 -1
- package/dist/server/routes/workflows.js +33 -0
- package/package.json +1 -1
package/dist/core/index.d.ts
CHANGED
|
@@ -12,5 +12,5 @@ export type { CollectedToolCall, ToolCallResult } from './tool-exec.js';
|
|
|
12
12
|
export { executeToolCall, executeToolCalls, partitionByConflict } from './tool-exec.js';
|
|
13
13
|
export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, CommandContext, CommandResult, LLMSegment, PendingToolCall, ProjectInfo, PromptContext, SlashCommand, TokenBudget, } from './types.js';
|
|
14
14
|
export { buildWorkflowNotice, containsWorkflow, WORKFLOW_NOTICE } from './workflow.js';
|
|
15
|
-
export type { WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowRegistry, WorkflowResult, WorkflowUtils, } from './workflows/index.js';
|
|
16
|
-
export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, } from './workflows/index.js';
|
|
15
|
+
export type { SaveResult, SaveTarget, WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowRegistry, WorkflowResult, WorkflowUtils, } from './workflows/index.js';
|
|
16
|
+
export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
|
package/dist/core/index.js
CHANGED
|
@@ -10,4 +10,4 @@ export { builtinCommands, createSlashRegistry, parseSlashInput } from './slash.j
|
|
|
10
10
|
export { clearSteering, drainSteering, injectSteering } from './steering.js';
|
|
11
11
|
export { executeToolCall, executeToolCalls, partitionByConflict } from './tool-exec.js';
|
|
12
12
|
export { buildWorkflowNotice, containsWorkflow, WORKFLOW_NOTICE } from './workflow.js';
|
|
13
|
-
export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, } from './workflows/index.js';
|
|
13
|
+
export { BUILTIN_WORKFLOWS, buildWorkflowContext, createAndPopulateRegistry, createBuiltinWorkflows, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
|
package/dist/core/slash.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSession } from '../session/session.js';
|
|
2
2
|
import { createAgent } from './agent.js';
|
|
3
|
-
import { BUILTIN_WORKFLOWS, createWorkflowRegistry, executeWorkflow } from './workflows/index.js';
|
|
3
|
+
import { BUILTIN_WORKFLOWS, createWorkflowRegistry, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
|
|
4
4
|
function parseSlashInput(input) {
|
|
5
5
|
const trimmed = input.trim();
|
|
6
6
|
if (!trimmed.startsWith('/'))
|
|
@@ -109,7 +109,7 @@ const configCommand = {
|
|
|
109
109
|
const workflowCommand = {
|
|
110
110
|
name: 'workflow',
|
|
111
111
|
description: 'Manage and run workflows',
|
|
112
|
-
argsHint: '[run|show|
|
|
112
|
+
argsHint: '[list|run|show|create|edit] [name] [args]',
|
|
113
113
|
execute: async (args, ctx) => {
|
|
114
114
|
const parts = args.split(/\s+/).filter(Boolean);
|
|
115
115
|
const subcommand = parts[0] ?? 'list';
|
|
@@ -129,7 +129,11 @@ const workflowCommand = {
|
|
|
129
129
|
lines.push(` /${wf.meta.name}${phases} — ${wf.meta.description} (${wf.source})`);
|
|
130
130
|
}
|
|
131
131
|
lines.push('');
|
|
132
|
-
lines.push('Usage:
|
|
132
|
+
lines.push('Usage:');
|
|
133
|
+
lines.push(' /workflow run <name> [args] — 执行工作流');
|
|
134
|
+
lines.push(' /workflow create <name> --file <path> — 从文件创建工作流');
|
|
135
|
+
lines.push(' /workflow edit <name> — 编辑工作流源码');
|
|
136
|
+
lines.push(' /workflow show <name> — 查看工作流源码');
|
|
133
137
|
return { _tag: 'text', text: lines.join('\n') };
|
|
134
138
|
}
|
|
135
139
|
if (subcommand === 'show') {
|
|
@@ -146,6 +150,64 @@ const workflowCommand = {
|
|
|
146
150
|
text: `// ${wf.meta.name}: ${wf.meta.description}\n\n${code}`,
|
|
147
151
|
};
|
|
148
152
|
}
|
|
153
|
+
if (subcommand === 'create') {
|
|
154
|
+
const name = parts[1];
|
|
155
|
+
if (!name)
|
|
156
|
+
return { _tag: 'error', message: 'Usage: /workflow create <name> --file <path>' };
|
|
157
|
+
// 解析 --file <path> 参数
|
|
158
|
+
const fileIdx = parts.indexOf('--file');
|
|
159
|
+
if (fileIdx === -1 || !parts[fileIdx + 1]) {
|
|
160
|
+
return { _tag: 'error', message: 'Usage: /workflow create <name> --file <path>\nTip: 也可通过 REST API POST /api/workflows { name, source } 创建' };
|
|
161
|
+
}
|
|
162
|
+
const filePath = parts[fileIdx + 1] ?? '';
|
|
163
|
+
let source;
|
|
164
|
+
try {
|
|
165
|
+
source = await import('node:fs/promises').then((fs) => fs.readFile(filePath, 'utf-8'));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { _tag: 'error', message: `Cannot read file: ${filePath}` };
|
|
169
|
+
}
|
|
170
|
+
const result = await saveWorkflow(name, source, 'project', ctx.cwd);
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
return { _tag: 'error', message: result.error };
|
|
173
|
+
}
|
|
174
|
+
// 热重载注册表
|
|
175
|
+
if (ctx.workflowRegistry) {
|
|
176
|
+
await reloadRegistry(ctx.workflowRegistry, ctx.cwd);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
_tag: 'success',
|
|
180
|
+
message: `Workflow "${name}" saved to ${result.filePath}\n现在可以用 /workflow run ${name} 执行,或在对话中输入 /${name} 调用。`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (subcommand === 'edit') {
|
|
184
|
+
const name = parts[1];
|
|
185
|
+
if (!name)
|
|
186
|
+
return { _tag: 'error', message: 'Usage: /workflow edit <name>' };
|
|
187
|
+
const wf = registry.get(name);
|
|
188
|
+
if (!wf) {
|
|
189
|
+
return { _tag: 'error', message: `Unknown workflow: ${name}` };
|
|
190
|
+
}
|
|
191
|
+
if (wf.source === 'builtin') {
|
|
192
|
+
return { _tag: 'error', message: 'Cannot edit builtin workflow. Fork it first: /workflow create <new-name> --file <path>' };
|
|
193
|
+
}
|
|
194
|
+
if (!wf.filePath) {
|
|
195
|
+
return { _tag: 'error', message: `Workflow file path not available for "${name}"` };
|
|
196
|
+
}
|
|
197
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
198
|
+
try {
|
|
199
|
+
const { spawnSync } = await import('node:child_process');
|
|
200
|
+
spawnSync(editor, [wf.filePath], { stdio: 'inherit' });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return { _tag: 'error', message: `Failed to launch editor: ${editor}` };
|
|
204
|
+
}
|
|
205
|
+
// 编辑后热重载
|
|
206
|
+
if (ctx.workflowRegistry) {
|
|
207
|
+
await reloadRegistry(ctx.workflowRegistry, ctx.cwd);
|
|
208
|
+
}
|
|
209
|
+
return { _tag: 'success', message: `Workflow "${name}" reloaded after edit.` };
|
|
210
|
+
}
|
|
149
211
|
if (subcommand === 'run') {
|
|
150
212
|
const name = parts[1];
|
|
151
213
|
if (!name)
|
|
@@ -170,7 +232,7 @@ const workflowCommand = {
|
|
|
170
232
|
}
|
|
171
233
|
return {
|
|
172
234
|
_tag: 'error',
|
|
173
|
-
message: `Unknown subcommand: ${subcommand}. Use: list, run, show`,
|
|
235
|
+
message: `Unknown subcommand: ${subcommand}. Use: list, run, show, create, edit`,
|
|
174
236
|
};
|
|
175
237
|
},
|
|
176
238
|
};
|
package/dist/core/workflow.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare function containsWorkflow(text: string): boolean;
|
|
|
25
25
|
*
|
|
26
26
|
* 适配 c0de-agent 的 task 工具 schema(subagent_type + context + tasks[])。
|
|
27
27
|
*/
|
|
28
|
-
export declare const WORKFLOW_NOTICE = "<workflow-notice>\nThe user's message contains the **workflowz** keyword: you MUST drive this task as a deterministic multi-subagent workflow using the `task` tool for batched fan-out. This is not optional \u2014 the keyword signals the user explicitly wants parallel sub-agent decomposition, not inline work. This overrides any default tendency to do the whole task inline.\n\n<when>\nThe user typed workflowz intentionally \u2014 they want fan-out. You may do a brief inline scout (1-2 tool calls to list files/scope the work), but your NEXT action after scouting MUST be a `task` tool call to fan out. Do NOT spend the entire turn reading files inline \u2014 that defeats the purpose. Common shapes:\n- **Understand** \u2014 parallel readers over subsystems \u2192 structured map.\n- **Design** \u2014 independent approaches \u2192 scored synthesis.\n- **Review** \u2014 split dimensions \u2192 find per dimension \u2192 adversarially verify each finding.\n- **Research** \u2014 multi-modal sweep \u2192 deep-read the hits \u2192 synthesize.\n- **Migrate** \u2014 discover sites \u2192 transform each \u2192 verify.\n</when>\n\n<task-contract>\nCall `task` once per independent fan-out batch using the batch form:\n\n task({\n subagent_type: \"coder\",\n context: \"shared background all subagents need\",\n tasks: [\n { role: \"coder\", assignment: \"specific assignment for agent 1\", description: \"short label\" },\n { role: \"reviewer\", assignment: \"specific assignment for agent 2\", description: \"short label\" },\n ]\n })\n\nAvailable subagent types: `general` (full tools, recursive), `coder` (implementation), `researcher` (read-only scout), `reviewer` (code review). Pick the type that matches each task's intent.\n\n`context` carries shared background prepended to every subagent's prompt \u2014 put the shared contract, conventions, and coordination rules here.\n\nEach task in `tasks[]` must be self-contained:\n- `role`: specialist type for this sub-task (e.g. coder, reviewer, researcher)\n- `assignment`: exact target (files, symbols, subsystem) + what to do + acceptance criteria\n- `description`: short label for the UI\n\nEach subagent runs in an isolated session and returns its result via the `yield` tool. Subagents skip formatters, linters, and project-wide tests \u2014 the parent runs shared proof once after all results return.\n</task-contract>\n\n<structure>\nDecompose first, then batch the independent leaves:\n\n task({\n subagent_type: \"coder\",\n context: \"# Goal\nImplement feature X across the codebase\n# Constraints\nFollow existing patterns...\n# Contract\nReturn findings as structured data...\",\n tasks: [\n { role: \"coder\", assignment: \"# Target\nsrc/auth/login.ts\n# Change\nAdd rate limiting to login endpoint\n# Acceptance\nRate limiter works, tests pass\", description: \"Login rate limiting\" },\n { role: \"coder\", assignment: \"# Target\nsrc/auth/signup.ts\n# Change\nAdd input validation\n# Acceptance\nValidation blocks invalid emails\", description: \"Signup validation\" },\n ]\n })\n\nPrefer one wide batch over serial calls when work items do not share files. If tasks overlap, have agents coordinate before editing.\n</structure>\n\n<patterns>\n- **Adversarial verify** \u2014 dispatch skeptical reviewers with distinct targets, then keep only findings you can verify against source.\n- **Perspective-diverse review** \u2014 use separate correctness, security, performance roles instead of identical reviewers.\n- **Completeness critic** \u2014 after the first batch, dispatch one read-only critic that asks what was missed.\n- **No silent caps** \u2014 if you bound coverage (top-N, sampling), state what was dropped and why.\n- **Parent owns closure** \u2014 subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.\n</patterns>\n\n<execution>\n- **You MUST call the `task` tool** \u2014 this is the defining action of a workflowz request. A response that only does inline read/glob/edit without dispatching sub-agents is a failure.\n- Capture multi-phase workflow state in the visible todo system when available.\n- Batch independent subagents in one `task` call.\n- Give every subagent a narrow target, explicit non-goals, and a concrete return packet.\n- After fan-out returns, read the results, patch or decide, and run the shared gate.\n- Keep going until the task is closed \u2014 returned fan-out is a step, not a stopping point.\n</execution>\n</workflow-notice>";
|
|
28
|
+
export declare const WORKFLOW_NOTICE = "<workflow-notice>\nThe user's message contains the **workflowz** keyword: you MUST drive this task as a deterministic multi-subagent workflow using the `task` tool for batched fan-out. This is not optional \u2014 the keyword signals the user explicitly wants parallel sub-agent decomposition, not inline work. This overrides any default tendency to do the whole task inline.\n\n<when>\nThe user typed workflowz intentionally \u2014 they want fan-out. You may do a brief inline scout (1-2 tool calls to list files/scope the work), but your NEXT action after scouting MUST be a `task` tool call to fan out. Do NOT spend the entire turn reading files inline \u2014 that defeats the purpose. Common shapes:\n- **Understand** \u2014 parallel readers over subsystems \u2192 structured map.\n- **Design** \u2014 independent approaches \u2192 scored synthesis.\n- **Review** \u2014 split dimensions \u2192 find per dimension \u2192 adversarially verify each finding.\n- **Research** \u2014 multi-modal sweep \u2192 deep-read the hits \u2192 synthesize.\n- **Migrate** \u2014 discover sites \u2192 transform each \u2192 verify.\n</when>\n\n<task-contract>\nCall `task` once per independent fan-out batch using the batch form:\n\n task({\n subagent_type: \"coder\",\n context: \"shared background all subagents need\",\n tasks: [\n { role: \"coder\", assignment: \"specific assignment for agent 1\", description: \"short label\" },\n { role: \"reviewer\", assignment: \"specific assignment for agent 2\", description: \"short label\" },\n ]\n })\n\nAvailable subagent types: `general` (full tools, recursive), `coder` (implementation), `researcher` (read-only scout), `reviewer` (code review). Pick the type that matches each task's intent.\n\n`context` carries shared background prepended to every subagent's prompt \u2014 put the shared contract, conventions, and coordination rules here.\n\nEach task in `tasks[]` must be self-contained:\n- `role`: specialist type for this sub-task (e.g. coder, reviewer, researcher)\n- `assignment`: exact target (files, symbols, subsystem) + what to do + acceptance criteria\n- `description`: short label for the UI\n\nEach subagent runs in an isolated session and returns its result via the `yield` tool. Subagents skip formatters, linters, and project-wide tests \u2014 the parent runs shared proof once after all results return.\n</task-contract>\n\n<structure>\nDecompose first, then batch the independent leaves:\n\n task({\n subagent_type: \"coder\",\n context: \"# Goal\nImplement feature X across the codebase\n# Constraints\nFollow existing patterns...\n# Contract\nReturn findings as structured data...\",\n tasks: [\n { role: \"coder\", assignment: \"# Target\nsrc/auth/login.ts\n# Change\nAdd rate limiting to login endpoint\n# Acceptance\nRate limiter works, tests pass\", description: \"Login rate limiting\" },\n { role: \"coder\", assignment: \"# Target\nsrc/auth/signup.ts\n# Change\nAdd input validation\n# Acceptance\nValidation blocks invalid emails\", description: \"Signup validation\" },\n ]\n })\n\nPrefer one wide batch over serial calls when work items do not share files. If tasks overlap, have agents coordinate before editing.\n</structure>\n\n<patterns>\n- **Adversarial verify** \u2014 dispatch skeptical reviewers with distinct targets, then keep only findings you can verify against source.\n- **Perspective-diverse review** \u2014 use separate correctness, security, performance roles instead of identical reviewers.\n- **Completeness critic** \u2014 after the first batch, dispatch one read-only critic that asks what was missed.\n- **No silent caps** \u2014 if you bound coverage (top-N, sampling), state what was dropped and why.\n- **Parent owns closure** \u2014 subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.\n- **Save reusable workflows** \u2014 if the fan-out pattern is reusable, save it as a `/workflow create <name>` command. Generate a JS file with `export const meta = { name, description, phases }` and a default async function that orchestrates via `ctx.runSubagents`. Use the REST API `POST /api/workflows` with body `{ name, source }` to save it, or write the file directly to `.c0de/workflows/<name>.js`. Saved workflows become slash commands (`/<name>`) that can be re-run anytime.\n</patterns>\n\n<execution>\n- **You MUST call the `task` tool** \u2014 this is the defining action of a workflowz request. A response that only does inline read/glob/edit without dispatching sub-agents is a failure.\n- Capture multi-phase workflow state in the visible todo system when available.\n- Batch independent subagents in one `task` call.\n- Give every subagent a narrow target, explicit non-goals, and a concrete return packet.\n- After fan-out returns, read the results, patch or decide, and run the shared gate.\n- Keep going until the task is closed \u2014 returned fan-out is a step, not a stopping point.\n</execution>\n</workflow-notice>";
|
|
29
29
|
/**
|
|
30
30
|
* 构建工作流 steering 通知:基础通知 + 可选的已注册工作流列表。
|
|
31
31
|
* 无工作流时退化为纯基础通知(向后兼容)。
|
package/dist/core/workflow.js
CHANGED
|
@@ -93,6 +93,7 @@ Prefer one wide batch over serial calls when work items do not share files. If t
|
|
|
93
93
|
- **Completeness critic** — after the first batch, dispatch one read-only critic that asks what was missed.
|
|
94
94
|
- **No silent caps** — if you bound coverage (top-N, sampling), state what was dropped and why.
|
|
95
95
|
- **Parent owns closure** — subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.
|
|
96
|
+
- **Save reusable workflows** — if the fan-out pattern is reusable, save it as a \`/workflow create <name>\` command. Generate a JS file with \`export const meta = { name, description, phases }\` and a default async function that orchestrates via \`ctx.runSubagents\`. Use the REST API \`POST /api/workflows\` with body \`{ name, source }\` to save it, or write the file directly to \`.c0de/workflows/<name>.js\`. Saved workflows become slash commands (\`/<name>\`) that can be re-run anytime.
|
|
96
97
|
</patterns>
|
|
97
98
|
|
|
98
99
|
<execution>
|
|
@@ -113,8 +114,10 @@ export function buildWorkflowNotice(workflows) {
|
|
|
113
114
|
? `\n<registered-workflows>
|
|
114
115
|
Available workflow templates you can invoke with the task tool's workflow parameter:
|
|
115
116
|
${workflows.map((w) => `- ${w.name}: ${w.description}`).join('\n')}
|
|
116
|
-
If none fit, orchestrate inline using runSubagent fan-out as described below.
|
|
117
|
+
If none fit, orchestrate inline using runSubagent fan-out as described below, then save your workflow with POST /api/workflows { name, source } for reuse.
|
|
117
118
|
</registered-workflows>`
|
|
118
|
-
:
|
|
119
|
+
: `\n<registered-workflows>
|
|
120
|
+
No saved workflows yet. After orchestrating a successful fan-out, save it with POST /api/workflows { name, source } for reuse.
|
|
121
|
+
</registered-workflows>`;
|
|
119
122
|
return WORKFLOW_NOTICE + registeredSection;
|
|
120
123
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkflowEntry } from './types.js';
|
|
1
|
+
import type { WorkflowEntry, WorkflowMeta } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* 扫描项目目录下的 `.c0de/workflows/*.js` 文件,source 标记为 'project'。
|
|
4
4
|
*/
|
|
@@ -8,4 +8,25 @@ declare function discoverWorkflows(projectDir: string): Promise<WorkflowEntry[]>
|
|
|
8
8
|
* 目录不存在时返回空数组(与项目级一致)。
|
|
9
9
|
*/
|
|
10
10
|
declare function discoverGlobalWorkflows(): Promise<WorkflowEntry[]>;
|
|
11
|
-
|
|
11
|
+
/** saveWorkflow 的目标层级。 */
|
|
12
|
+
type SaveTarget = 'project' | 'user';
|
|
13
|
+
/** saveWorkflow 的结果。 */
|
|
14
|
+
type SaveResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
filePath: string;
|
|
17
|
+
meta: WorkflowMeta;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 将工作流源码保存到磁盘并验证可加载。
|
|
24
|
+
*
|
|
25
|
+
* - name 必须匹配 `[a-z0-9-]+`
|
|
26
|
+
* - 写入 `<projectDir>/.c0de/workflows/<name>.js`(project)或 `~/.c0de/workflows/<name>.js`(user)
|
|
27
|
+
* - 写入后 dynamic import 验证 meta + default 导出存在;验证失败则删除文件
|
|
28
|
+
* - 返回 { ok, filePath, meta } 或 { ok: false, error }
|
|
29
|
+
*/
|
|
30
|
+
declare function saveWorkflow(name: string, source: string, target?: SaveTarget, projectDir?: string): Promise<SaveResult>;
|
|
31
|
+
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
|
|
32
|
+
export type { SaveResult, SaveTarget };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
@@ -66,4 +66,40 @@ async function discoverWorkflows(projectDir) {
|
|
|
66
66
|
async function discoverGlobalWorkflows() {
|
|
67
67
|
return discoverFromDir(join(homedir(), GLOBAL_WORKFLOWS_DIR), 'user');
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
/** 合法工作流名称:仅小写字母、数字、连字符。 */
|
|
70
|
+
const WORKFLOW_NAME_RE = /^[a-z0-9-]+$/;
|
|
71
|
+
/**
|
|
72
|
+
* 将工作流源码保存到磁盘并验证可加载。
|
|
73
|
+
*
|
|
74
|
+
* - name 必须匹配 `[a-z0-9-]+`
|
|
75
|
+
* - 写入 `<projectDir>/.c0de/workflows/<name>.js`(project)或 `~/.c0de/workflows/<name>.js`(user)
|
|
76
|
+
* - 写入后 dynamic import 验证 meta + default 导出存在;验证失败则删除文件
|
|
77
|
+
* - 返回 { ok, filePath, meta } 或 { ok: false, error }
|
|
78
|
+
*/
|
|
79
|
+
async function saveWorkflow(name, source, target = 'project', projectDir) {
|
|
80
|
+
if (!WORKFLOW_NAME_RE.test(name)) {
|
|
81
|
+
return { ok: false, error: `Invalid workflow name "${name}": must match [a-z0-9-]+` };
|
|
82
|
+
}
|
|
83
|
+
const dir = target === 'project'
|
|
84
|
+
? join(projectDir ?? '.', PROJECT_WORKFLOWS_DIR)
|
|
85
|
+
: join(homedir(), GLOBAL_WORKFLOWS_DIR);
|
|
86
|
+
await mkdir(dir, { recursive: true });
|
|
87
|
+
const filePath = join(dir, `${name}.js`);
|
|
88
|
+
await writeFile(filePath, source, 'utf-8');
|
|
89
|
+
// 验证:dynamic import 检查 meta + default
|
|
90
|
+
try {
|
|
91
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
92
|
+
const mod = (await import(`${fileUrl}#${Date.now()}`));
|
|
93
|
+
if (!mod.meta || typeof mod.default !== 'function') {
|
|
94
|
+
await unlink(filePath);
|
|
95
|
+
return { ok: false, error: 'Workflow source missing `export const meta` or default export' };
|
|
96
|
+
}
|
|
97
|
+
const meta = { ...mod.meta, name: mod.meta.name ?? name };
|
|
98
|
+
return { ok: true, filePath, meta };
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
await unlink(filePath);
|
|
102
|
+
return { ok: false, error: `Failed to load workflow: ${e instanceof Error ? e.message : String(e)}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { BUILTIN_WORKFLOWS, createBuiltinWorkflows } from './builtins.js';
|
|
2
2
|
export { buildWorkflowContext } from './context.js';
|
|
3
|
-
export { discoverGlobalWorkflows, discoverWorkflows } from './discovery.js';
|
|
3
|
+
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
|
|
4
|
+
export type { SaveResult, SaveTarget } from './discovery.js';
|
|
4
5
|
export type { WorkflowRegistry } from './registry.js';
|
|
5
|
-
export { createAndPopulateRegistry, createWorkflowRegistry } from './registry.js';
|
|
6
|
+
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
|
|
6
7
|
export { executeWorkflow } from './runtime.js';
|
|
7
8
|
export type { WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowModule, WorkflowResult, WorkflowSource, WorkflowUtils, } from './types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { BUILTIN_WORKFLOWS, createBuiltinWorkflows } from './builtins.js';
|
|
2
2
|
export { buildWorkflowContext } from './context.js';
|
|
3
|
-
export { discoverGlobalWorkflows, discoverWorkflows } from './discovery.js';
|
|
4
|
-
export { createAndPopulateRegistry, createWorkflowRegistry } from './registry.js';
|
|
3
|
+
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
|
|
4
|
+
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
|
|
5
5
|
export { executeWorkflow } from './runtime.js';
|
|
@@ -6,6 +6,8 @@ declare function createWorkflowRegistry(): {
|
|
|
6
6
|
list(): WorkflowEntry[];
|
|
7
7
|
has(name: string): boolean;
|
|
8
8
|
delete(name: string): boolean;
|
|
9
|
+
/** 清空注册表(热重载前调用)。 */
|
|
10
|
+
clear(): void;
|
|
9
11
|
};
|
|
10
12
|
type WorkflowRegistry = ReturnType<typeof createWorkflowRegistry>;
|
|
11
13
|
/**
|
|
@@ -16,5 +18,10 @@ type WorkflowRegistry = ReturnType<typeof createWorkflowRegistry>;
|
|
|
16
18
|
* 覆盖优先级:project > user > builtin。
|
|
17
19
|
*/
|
|
18
20
|
declare function createAndPopulateRegistry(projectDir: string): Promise<WorkflowRegistry>;
|
|
21
|
+
/**
|
|
22
|
+
* 重新填充已有注册表:清空 → 三级发现 → 注册。
|
|
23
|
+
* 用于 create/save 后热重载,保留同一 registry 引用。
|
|
24
|
+
*/
|
|
25
|
+
declare function reloadRegistry(registry: WorkflowRegistry, projectDir: string): Promise<void>;
|
|
19
26
|
export type { WorkflowRegistry };
|
|
20
|
-
export { createAndPopulateRegistry, createWorkflowRegistry };
|
|
27
|
+
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry };
|
|
@@ -19,6 +19,10 @@ function createWorkflowRegistry() {
|
|
|
19
19
|
delete(name) {
|
|
20
20
|
return entries.delete(name);
|
|
21
21
|
},
|
|
22
|
+
/** 清空注册表(热重载前调用)。 */
|
|
23
|
+
clear() {
|
|
24
|
+
entries.clear();
|
|
25
|
+
},
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
@@ -46,4 +50,20 @@ async function createAndPopulateRegistry(projectDir) {
|
|
|
46
50
|
}
|
|
47
51
|
return registry;
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
/**
|
|
54
|
+
* 重新填充已有注册表:清空 → 三级发现 → 注册。
|
|
55
|
+
* 用于 create/save 后热重载,保留同一 registry 引用。
|
|
56
|
+
*/
|
|
57
|
+
async function reloadRegistry(registry, projectDir) {
|
|
58
|
+
registry.clear();
|
|
59
|
+
for (const wf of await createBuiltinWorkflows()) {
|
|
60
|
+
registry.register(wf);
|
|
61
|
+
}
|
|
62
|
+
for (const wf of await discoverGlobalWorkflows()) {
|
|
63
|
+
registry.register(wf);
|
|
64
|
+
}
|
|
65
|
+
for (const wf of await discoverWorkflows(projectDir)) {
|
|
66
|
+
registry.register(wf);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry };
|
|
@@ -3,6 +3,8 @@ import { Hono } from 'hono';
|
|
|
3
3
|
import { streamSSE } from 'hono/streaming';
|
|
4
4
|
import { createAgent } from '../../core/agent.js';
|
|
5
5
|
import { executeWorkflow } from '../../core/workflows/runtime.js';
|
|
6
|
+
import { reloadRegistry } from '../../core/workflows/registry.js';
|
|
7
|
+
import { saveWorkflow } from '../../core/workflows/discovery.js';
|
|
6
8
|
import { createSession } from '../../session/session.js';
|
|
7
9
|
import { autoAllowChecker } from '../../tools/permission.js';
|
|
8
10
|
import { apiError } from '../middleware/error.js';
|
|
@@ -24,6 +26,37 @@ function createWorkflowsRoute(ctx) {
|
|
|
24
26
|
}));
|
|
25
27
|
return c.json({ workflows });
|
|
26
28
|
});
|
|
29
|
+
// POST / — 创建/保存工作流(写入 .c0de/workflows/<name>.js,验证后热重载注册表)
|
|
30
|
+
app.post('/', async (c) => {
|
|
31
|
+
const registry = ctx.workflowRegistry;
|
|
32
|
+
if (!registry) {
|
|
33
|
+
return apiError(c, 500, 'NOT_INITIALIZED', 'Workflow registry not initialized');
|
|
34
|
+
}
|
|
35
|
+
const body = await c.req.json().catch(() => ({}));
|
|
36
|
+
const { name, source, target } = body;
|
|
37
|
+
if (!name || typeof name !== 'string') {
|
|
38
|
+
return apiError(c, 400, 'BAD_REQUEST', 'Missing required field: name');
|
|
39
|
+
}
|
|
40
|
+
if (!source || typeof source !== 'string') {
|
|
41
|
+
return apiError(c, 400, 'BAD_REQUEST', 'Missing required field: source');
|
|
42
|
+
}
|
|
43
|
+
// 保存到磁盘 + dynamic import 验证
|
|
44
|
+
const result = await saveWorkflow(name, source, target ?? 'project', ctx.cwd);
|
|
45
|
+
if (!result.ok) {
|
|
46
|
+
return apiError(c, 400, 'SAVE_FAILED', result.error);
|
|
47
|
+
}
|
|
48
|
+
// 热重载注册表(清空 → 三级重新发现)
|
|
49
|
+
await reloadRegistry(registry, ctx.cwd);
|
|
50
|
+
const entry = registry.get(name);
|
|
51
|
+
return c.json({
|
|
52
|
+
ok: true,
|
|
53
|
+
name: result.meta.name,
|
|
54
|
+
description: result.meta.description,
|
|
55
|
+
filePath: result.filePath,
|
|
56
|
+
phases: entry?.meta.phases,
|
|
57
|
+
source: entry?.source ?? 'project',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
27
60
|
// GET /:name — 元数据 + 源码
|
|
28
61
|
app.get('/:name', (c) => {
|
|
29
62
|
const name = c.req.param('name');
|