c0de-agent 1.4.0 → 1.6.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.
Files changed (44) hide show
  1. package/dist/core/agent.js +4 -0
  2. package/dist/core/config.js +1 -1
  3. package/dist/core/index.d.ts +3 -3
  4. package/dist/core/index.js +1 -1
  5. package/dist/core/loop.js +9 -0
  6. package/dist/core/prompt-registry.d.ts +2 -2
  7. package/dist/core/prompt-registry.js +42 -3
  8. package/dist/core/slash.js +102 -6
  9. package/dist/core/types.d.ts +10 -1
  10. package/dist/core/workflow.d.ts +1 -1
  11. package/dist/core/workflow.js +56 -5
  12. package/dist/core/workflows/discovery.d.ts +23 -2
  13. package/dist/core/workflows/discovery.js +38 -2
  14. package/dist/core/workflows/index.d.ts +3 -2
  15. package/dist/core/workflows/index.js +2 -2
  16. package/dist/core/workflows/registry.d.ts +8 -1
  17. package/dist/core/workflows/registry.js +21 -1
  18. package/dist/core/workflows/runtime.d.ts +3 -0
  19. package/dist/core/workflows/runtime.js +2 -2
  20. package/dist/project/resolve.d.ts +75 -0
  21. package/dist/project/resolve.js +253 -1
  22. package/dist/server/app.js +3 -0
  23. package/dist/server/context.js +2 -1
  24. package/dist/server/dev.js +3 -1
  25. package/dist/server/routes/chat.js +12 -0
  26. package/dist/server/routes/commands.js +1 -0
  27. package/dist/server/routes/files.js +252 -4
  28. package/dist/server/routes/terminal.js +2 -1
  29. package/dist/server/routes/todo.d.ts +4 -0
  30. package/dist/server/routes/todo.js +107 -0
  31. package/dist/server/routes/workflows.js +83 -10
  32. package/dist/server/server.d.ts +8 -1
  33. package/dist/server/server.js +113 -23
  34. package/dist/server/terminal/pty-manager.d.ts +14 -0
  35. package/dist/server/terminal/pty-manager.js +105 -7
  36. package/dist/shared/types/agent.d.ts +9 -0
  37. package/dist/shared/types/config.d.ts +6 -0
  38. package/dist/shared/types/tool.d.ts +13 -0
  39. package/dist/tools/builtin/todo.d.ts +67 -0
  40. package/dist/tools/builtin/todo.js +517 -0
  41. package/dist/tools/index.d.ts +2 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/types.d.ts +32 -1
  44. package/package.json +2 -1
@@ -1,9 +1,14 @@
1
- import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join, relative } from 'node:path';
3
3
  import { Hono } from 'hono';
4
+ import trash from 'trash';
5
+ import { createSummarizer } from '../../core/compact.js';
4
6
  import { getProject } from '../../project/project.js';
7
+ import { appendToGitignore, checkIgnored, checkoutGitBranch, createGitBranch, getGitBranch, getGitDiffSummary, getGitLastCommit, getGitStatus, listGitBranches, performGitCommit, } from '../../project/resolve.js';
5
8
  import { apiError } from '../middleware/error.js';
6
9
  import { safeResolve } from '../util/safe-path.js';
10
+ /** 递归搜索时跳过的目录(体积大/为元数据噪音,避免递归进入)。 */
11
+ const SEARCH_SKIP_DIRS = new Set(['.git', 'node_modules']);
7
12
  /** 递归收集文件列表(用于搜索)。 */
8
13
  async function collectFiles(dir, basePath, maxDepth = 5) {
9
14
  if (maxDepth < 0)
@@ -17,7 +22,7 @@ async function collectFiles(dir, basePath, maxDepth = 5) {
17
22
  return [];
18
23
  }
19
24
  for (const entry of entries) {
20
- if (entry.name.startsWith('.') || entry.name === 'node_modules')
25
+ if (entry.isDirectory() && SEARCH_SKIP_DIRS.has(entry.name))
21
26
  continue;
22
27
  const fullPath = join(dir, entry.name);
23
28
  const relPath = relative(basePath, fullPath);
@@ -60,6 +65,211 @@ function contentTypeFor(name) {
60
65
  }
61
66
  function createFilesRoute(ctx) {
62
67
  const app = new Hono();
68
+ // git 状态:返回 path → 状态分类 的映射(非 git 返回空对象)
69
+ app.get('/git-status', async (c) => {
70
+ const projectId = c.req.query('projectId');
71
+ let root = ctx.cwd;
72
+ if (projectId) {
73
+ const project = await getProject(ctx.db, projectId);
74
+ if (!project) {
75
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
76
+ }
77
+ root = project.worktree;
78
+ }
79
+ return c.json(getGitStatus(root) ?? {});
80
+ });
81
+ // 当前分支名(非 git 仓库返回 null)
82
+ app.get('/git-branch', async (c) => {
83
+ const projectId = c.req.query('projectId');
84
+ let root = ctx.cwd;
85
+ if (projectId) {
86
+ const project = await getProject(ctx.db, projectId);
87
+ if (!project) {
88
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
89
+ }
90
+ root = project.worktree;
91
+ }
92
+ return c.json({ branch: getGitBranch(root) });
93
+ });
94
+ // 最后一次提交信息(供分支名 hover tooltip)。非 git 仓库或无提交返回 commit null。
95
+ app.get('/git-last-commit', async (c) => {
96
+ const projectId = c.req.query('projectId');
97
+ let root = ctx.cwd;
98
+ if (projectId) {
99
+ const project = await getProject(ctx.db, projectId);
100
+ if (!project) {
101
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
102
+ }
103
+ root = project.worktree;
104
+ }
105
+ return c.json({ commit: getGitLastCommit(root) });
106
+ });
107
+ // 一键提交:用 LLM 生成 commit message + 检查可疑文件,支持 force/append-ignore 模式
108
+ app.post('/git-commit', async (c) => {
109
+ const projectId = c.req.query('projectId');
110
+ let root = ctx.cwd;
111
+ if (projectId) {
112
+ const project = await getProject(ctx.db, projectId);
113
+ if (!project) {
114
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
115
+ }
116
+ root = project.worktree;
117
+ }
118
+ const summary = getGitDiffSummary(root);
119
+ if (!summary) {
120
+ return apiError(c, 400, 'NO_CHANGES', 'No changes to commit');
121
+ }
122
+ // 可选 body:mode / message / suggestions
123
+ const body = await c.req
124
+ .json()
125
+ .catch(() => ({}));
126
+ // --- mode: force — 跳过检查,用传入 message 直接提交 ---
127
+ if (body.mode === 'force') {
128
+ if (!body.message) {
129
+ return apiError(c, 400, 'MISSING_MESSAGE', 'mode=force requires a message');
130
+ }
131
+ const result = performGitCommit(root, body.message);
132
+ if ('error' in result) {
133
+ return apiError(c, 500, 'COMMIT_FAILED', result.error);
134
+ }
135
+ return c.json({
136
+ committed: true,
137
+ message: body.message,
138
+ hash: result.hash,
139
+ fileCount: summary.fileCount,
140
+ });
141
+ }
142
+ // --- mode: append-ignore — 追加 .gitignore 后提交 ---
143
+ if (body.mode === 'append-ignore') {
144
+ if (!body.message) {
145
+ return apiError(c, 400, 'MISSING_MESSAGE', 'mode=append-ignore requires a message');
146
+ }
147
+ if (!body.suggestions || body.suggestions.length === 0) {
148
+ return apiError(c, 400, 'MISSING_SUGGESTIONS', 'mode=append-ignore requires suggestions');
149
+ }
150
+ appendToGitignore(root, body.suggestions);
151
+ const result = performGitCommit(root, body.message);
152
+ if ('error' in result) {
153
+ return apiError(c, 500, 'COMMIT_FAILED', result.error);
154
+ }
155
+ return c.json({
156
+ committed: true,
157
+ message: body.message,
158
+ hash: result.hash,
159
+ fileCount: summary.fileCount,
160
+ });
161
+ }
162
+ // --- 默认模式:LLM 生成 message + 检查可疑文件 ---
163
+ const cm = ctx.config.commitModel;
164
+ const provider = cm?.provider ?? ctx.config.defaultProvider;
165
+ const model = cm?.model ?? ctx.config.defaultModel;
166
+ const prompt = `Based on the following git diff, generate a concise commit message in conventional-commits format (e.g. "feat: add login page").
167
+
168
+ ALSO review the changed/new files: are any of them files that SHOULD be in .gitignore but are currently missing? (e.g. secrets, .env, build output, dependencies, temp files, large binaries)
169
+
170
+ Reply as JSON ONLY:
171
+ {"message": "<commit message>", "ignoreSuggestions": ["<path>", ...]}
172
+
173
+ If no files need ignoring, return an empty array for ignoreSuggestions.
174
+
175
+ ${summary.diff.slice(0, 8000)}`;
176
+ let raw;
177
+ try {
178
+ const summarizer = createSummarizer(ctx.llmRegistry, provider, model, { maxTokens: 400 });
179
+ raw = (await summarizer(prompt)).trim();
180
+ }
181
+ catch (err) {
182
+ return apiError(c, 502, 'LLM_ERROR', `Failed to generate commit message: ${String(err)}`);
183
+ }
184
+ // LLM 返回可能含 markdown 代码块包裹,去掉
185
+ raw = raw
186
+ .replace(/^```[a-z]*\n?/m, '')
187
+ .replace(/\n?```$/m, '')
188
+ .trim();
189
+ // JSON 解析(fail-closed:无法解析 → 报错阻断,不提交)
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(raw);
193
+ }
194
+ catch {
195
+ return apiError(c, 502, 'CHECK_PARSE_ERROR', 'Commit ignore check failed: LLM returned unparseable response');
196
+ }
197
+ const message = (parsed.message ?? '').trim();
198
+ if (!message) {
199
+ return apiError(c, 502, 'EMPTY_MESSAGE', 'LLM returned empty commit message');
200
+ }
201
+ const suggestions = Array.isArray(parsed.ignoreSuggestions) ? parsed.ignoreSuggestions : [];
202
+ // LLM 检测到可疑文件 → 阻断提交,返回供前端审查
203
+ if (suggestions.length > 0) {
204
+ return c.json({ needsReview: true, message, suggestions });
205
+ }
206
+ // 无可疑文件 → 直接提交
207
+ const result = performGitCommit(root, message);
208
+ if ('error' in result) {
209
+ return apiError(c, 500, 'COMMIT_FAILED', result.error);
210
+ }
211
+ return c.json({
212
+ committed: true,
213
+ message,
214
+ hash: result.hash,
215
+ fileCount: summary.fileCount,
216
+ });
217
+ });
218
+ // 列出本地分支(非 git 仓库返回空数组)
219
+ app.get('/git-branches', async (c) => {
220
+ const projectId = c.req.query('projectId');
221
+ let root = ctx.cwd;
222
+ if (projectId) {
223
+ const project = await getProject(ctx.db, projectId);
224
+ if (!project) {
225
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
226
+ }
227
+ root = project.worktree;
228
+ }
229
+ return c.json({ branches: listGitBranches(root) ?? [] });
230
+ });
231
+ // 切换分支(git checkout)
232
+ app.post('/git-checkout', async (c) => {
233
+ const projectId = c.req.query('projectId');
234
+ let root = ctx.cwd;
235
+ if (projectId) {
236
+ const project = await getProject(ctx.db, projectId);
237
+ if (!project) {
238
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
239
+ }
240
+ root = project.worktree;
241
+ }
242
+ const body = await c.req.json().catch(() => ({}));
243
+ const branch = body.branch;
244
+ if (!branch)
245
+ return apiError(c, 400, 'BAD_REQUEST', 'branch is required');
246
+ const result = checkoutGitBranch(root, branch);
247
+ if ('error' in result) {
248
+ return apiError(c, 500, 'CHECKOUT_FAILED', result.error);
249
+ }
250
+ return c.json({ branch: result.branch });
251
+ });
252
+ // 创建并切换到新分支(git checkout -b)
253
+ app.post('/git-branch-create', async (c) => {
254
+ const projectId = c.req.query('projectId');
255
+ let root = ctx.cwd;
256
+ if (projectId) {
257
+ const project = await getProject(ctx.db, projectId);
258
+ if (!project) {
259
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
260
+ }
261
+ root = project.worktree;
262
+ }
263
+ const body = await c.req.json().catch(() => ({}));
264
+ const name = body.name;
265
+ if (!name)
266
+ return apiError(c, 400, 'BAD_REQUEST', 'name is required');
267
+ const result = createGitBranch(root, name);
268
+ if ('error' in result) {
269
+ return apiError(c, 500, 'BRANCH_CREATE_FAILED', result.error);
270
+ }
271
+ return c.json({ branch: result.branch });
272
+ });
63
273
  // 列出目录
64
274
  // projectId 指定时按对应项目 worktree 列出,否则回退 ctx.cwd(向后兼容)。
65
275
  app.get('/', async (c) => {
@@ -79,8 +289,7 @@ function createFilesRoute(ctx) {
79
289
  }
80
290
  try {
81
291
  const entries = await readdir(resolved, { withFileTypes: true });
82
- const result = entries
83
- .filter((e) => !e.name.startsWith('.'))
292
+ const sorted = entries
84
293
  .map((e) => ({
85
294
  name: e.name,
86
295
  type: (e.isDirectory() ? 'directory' : 'file'),
@@ -90,6 +299,14 @@ function createFilesRoute(ctx) {
90
299
  return a.type === 'directory' ? -1 : 1;
91
300
  return a.name.localeCompare(b.name);
92
301
  });
302
+ // git check-ignore:只检查当前目录直接子项,标记被忽略的文件/目录(灰显用)
303
+ const prefix = queryPath === '.' ? '' : `${queryPath}/`;
304
+ const checkPaths = sorted.map((e) => `${prefix}${e.name}`);
305
+ const ignoredSet = checkIgnored(root, checkPaths);
306
+ const result = sorted.map((e) => ({
307
+ ...e,
308
+ ...(ignoredSet.has(`${prefix}${e.name}`) ? { ignored: true } : {}),
309
+ }));
93
310
  return c.json(result);
94
311
  }
95
312
  catch {
@@ -178,6 +395,37 @@ function createFilesRoute(ctx) {
178
395
  return apiError(c, 500, 'WRITE_ERROR', `Failed to write file: ${String(err)}`);
179
396
  }
180
397
  });
398
+ // 删除文件/目录(移入系统回收站)
399
+ // projectId 指定时按对应项目 worktree 解析,否则回退 ctx.cwd(向后兼容)。
400
+ app.delete('/*', async (c) => {
401
+ const path = c.req.path.replace(/^\/api\/files\//, '').replace(/^\//, '');
402
+ const projectId = c.req.query('projectId');
403
+ let root = ctx.cwd;
404
+ if (projectId) {
405
+ const project = await getProject(ctx.db, projectId);
406
+ if (!project) {
407
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
408
+ }
409
+ root = project.worktree;
410
+ }
411
+ const resolved = safeResolve(root, path);
412
+ if (!resolved) {
413
+ return apiError(c, 403, 'FORBIDDEN', 'Path outside workspace');
414
+ }
415
+ try {
416
+ await access(resolved);
417
+ }
418
+ catch {
419
+ return apiError(c, 404, 'NOT_FOUND', 'File not found');
420
+ }
421
+ try {
422
+ await trash(resolved);
423
+ return c.json({ path, trashed: true });
424
+ }
425
+ catch (err) {
426
+ return apiError(c, 500, 'DELETE_ERROR', `Failed to delete file: ${String(err)}`);
427
+ }
428
+ });
181
429
  return app;
182
430
  }
183
431
  export { createFilesRoute };
@@ -17,8 +17,9 @@ function createTerminalRoute(ctx) {
17
17
  const rows = Number.isFinite(body.rows) ? Number(body.rows) : undefined;
18
18
  const title = typeof body.title === 'string' ? body.title : undefined;
19
19
  const shell = typeof body.shell === 'string' && body.shell.length > 0 ? body.shell : undefined;
20
+ const projectId = typeof body.projectId === 'string' && body.projectId.length > 0 ? body.projectId : undefined;
20
21
  try {
21
- const info = mgr.create({ cwd, cols, rows, title, shell });
22
+ const info = mgr.create({ cwd, cols, rows, title, shell, projectId });
22
23
  return c.json(info, 201);
23
24
  }
24
25
  catch (err) {
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import type { ServerContext } from '../types.js';
3
+ declare function createTodoRoute(ctx: ServerContext): Hono;
4
+ export { createTodoRoute };
@@ -0,0 +1,107 @@
1
+ // src/server/routes/todo.ts
2
+ import { Hono } from 'hono';
3
+ import { getMessages, appendMessage } from '../../session/message.js';
4
+ import { getSession } from '../../session/session.js';
5
+ import { generateId } from '../../shared/index.js';
6
+ import { todoTool, getLatestTodoPhasesFromMessages } from '../../tools/builtin/todo.js';
7
+ import { apiError } from '../middleware/error.js';
8
+ /** 构造仅供 todo tool execute 使用的最小 ToolContext。 */
9
+ function makeTodoCtx(phases, abort) {
10
+ let state = phases;
11
+ return {
12
+ cwd: '/',
13
+ session: { id: '', cwd: '/' },
14
+ abort,
15
+ todoState: {
16
+ get: () => state,
17
+ set: (p) => {
18
+ state = p;
19
+ },
20
+ },
21
+ };
22
+ }
23
+ function createTodoRoute(ctx) {
24
+ const app = new Hono();
25
+ // GET /:sessionId — 获取当前 todo 状态
26
+ app.get('/:sessionId', async (c) => {
27
+ const sessionId = c.req.param('sessionId');
28
+ const run = ctx.agentManager.get(sessionId);
29
+ let phases;
30
+ if (run) {
31
+ // 活跃 agent:直接从内存状态读取
32
+ phases = run.state.todoPhases;
33
+ }
34
+ else {
35
+ // 无活跃 agent:从历史消息恢复
36
+ const messages = await getMessages(ctx.db, sessionId);
37
+ phases = getLatestTodoPhasesFromMessages(messages);
38
+ }
39
+ return c.json({ phases });
40
+ });
41
+ // POST /:sessionId — 执行一个 todo 操作(UI 手动操作入口)
42
+ app.post('/:sessionId', async (c) => {
43
+ const sessionId = c.req.param('sessionId');
44
+ const body = await c.req.json().catch(() => ({}));
45
+ // 校验 session 存在
46
+ const session = await getSession(ctx.db, sessionId);
47
+ if (!session) {
48
+ return apiError(c, 404, 'NOT_FOUND', 'Session not found');
49
+ }
50
+ // 恢复当前 phases
51
+ const run = ctx.agentManager.get(sessionId);
52
+ let currentPhases;
53
+ if (run) {
54
+ currentPhases = run.state.todoPhases;
55
+ }
56
+ else {
57
+ const messages = await getMessages(ctx.db, sessionId);
58
+ currentPhases = getLatestTodoPhasesFromMessages(messages);
59
+ }
60
+ // 执行 todo 操作
61
+ const todoCtx = makeTodoCtx(currentPhases, c.req.raw.signal);
62
+ const result = await todoTool.execute(body, todoCtx);
63
+ // 从 todoState 读取操作后的 phases
64
+ const updatedPhases = todoCtx.todoState.get();
65
+ // 如果操作成功且 phases 有变化,持久化为 tool 消息
66
+ if (result._tag === 'success' && body.op !== 'view') {
67
+ // 构造 tool_result 消息内容
68
+ const toolResultContent = {
69
+ _tag: 'tool_result',
70
+ id: generateId(),
71
+ tool: 'todo',
72
+ output: {
73
+ _tag: 'success',
74
+ output: result.output,
75
+ metadata: result.metadata,
76
+ },
77
+ };
78
+ const toolCallContent = {
79
+ _tag: 'tool_call',
80
+ id: generateId(),
81
+ tool: 'todo',
82
+ input: body,
83
+ };
84
+ // 存储为一条 tool 消息(tool_call + tool_result 合并)
85
+ await appendMessage(ctx.db, sessionId, {
86
+ role: 'tool',
87
+ content: [toolCallContent, toolResultContent],
88
+ });
89
+ // 更新活跃 agent 的内存状态
90
+ if (run) {
91
+ run.state.todoPhases = updatedPhases;
92
+ }
93
+ }
94
+ return c.json({
95
+ phases: updatedPhases,
96
+ output: result._tag === 'success'
97
+ ? result.output
98
+ : result._tag === 'error'
99
+ ? result.error
100
+ : result._tag === 'permission_required'
101
+ ? result.reason
102
+ : 'truncated',
103
+ });
104
+ });
105
+ return app;
106
+ }
107
+ export { createTodoRoute };
@@ -3,19 +3,38 @@ 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 { discoverWorkflows, saveWorkflow } from '../../core/workflows/discovery.js';
8
+ import { getProject } from '../../project/project.js';
6
9
  import { createSession } from '../../session/session.js';
7
10
  import { autoAllowChecker } from '../../tools/permission.js';
8
11
  import { apiError } from '../middleware/error.js';
9
12
  /** 创建工作流 REST API 路由。 */
10
13
  function createWorkflowsRoute(ctx) {
11
14
  const app = new Hono();
12
- // GET / — 列出所有工作流
13
- app.get('/', (c) => {
15
+ // GET / — 列出所有工作流。可选 ?projectId=xxx 合并项目级 .c0de/workflows/*.js。
16
+ app.get('/', async (c) => {
14
17
  const registry = ctx.workflowRegistry;
15
18
  if (!registry) {
16
19
  return c.json({ workflows: [] });
17
20
  }
18
- const workflows = registry.list().map((entry) => ({
21
+ // 注册表已有 builtin + global + server-cwd;以 name 为 key 去重。
22
+ const byName = new Map();
23
+ for (const entry of registry.list()) {
24
+ byName.set(entry.meta.name, entry);
25
+ }
26
+ // 项目级工作流:从 project.worktree/.c0de/workflows/ 动态发现,同名覆盖。
27
+ const projectId = c.req.query('projectId');
28
+ if (projectId) {
29
+ const project = await getProject(ctx.db, projectId);
30
+ if (project) {
31
+ const projectWorkflows = await discoverWorkflows(project.worktree);
32
+ for (const wf of projectWorkflows) {
33
+ byName.set(wf.meta.name, wf);
34
+ }
35
+ }
36
+ }
37
+ const workflows = Array.from(byName.values()).map((entry) => ({
19
38
  name: entry.meta.name,
20
39
  description: entry.meta.description,
21
40
  argsHint: entry.meta.argsHint,
@@ -24,14 +43,56 @@ function createWorkflowsRoute(ctx) {
24
43
  }));
25
44
  return c.json({ workflows });
26
45
  });
27
- // GET /:name元数据 + 源码
28
- app.get('/:name', (c) => {
29
- const name = c.req.param('name');
46
+ // POST /创建/保存工作流(写入 .c0de/workflows/<name>.js,验证后热重载注册表)
47
+ app.post('/', async (c) => {
30
48
  const registry = ctx.workflowRegistry;
31
49
  if (!registry) {
32
50
  return apiError(c, 500, 'NOT_INITIALIZED', 'Workflow registry not initialized');
33
51
  }
52
+ const body = await c.req.json().catch(() => ({}));
53
+ const { name, source, target } = body;
54
+ if (!name || typeof name !== 'string') {
55
+ return apiError(c, 400, 'BAD_REQUEST', 'Missing required field: name');
56
+ }
57
+ if (!source || typeof source !== 'string') {
58
+ return apiError(c, 400, 'BAD_REQUEST', 'Missing required field: source');
59
+ }
60
+ // 保存到磁盘 + dynamic import 验证
61
+ const result = await saveWorkflow(name, source, target ?? 'project', ctx.cwd);
62
+ if (!result.ok) {
63
+ return apiError(c, 400, 'SAVE_FAILED', result.error);
64
+ }
65
+ // 热重载注册表(清空 → 三级重新发现)
66
+ await reloadRegistry(registry, ctx.cwd);
34
67
  const entry = registry.get(name);
68
+ return c.json({
69
+ ok: true,
70
+ name: result.meta.name,
71
+ description: result.meta.description,
72
+ filePath: result.filePath,
73
+ phases: entry?.meta.phases,
74
+ source: entry?.source ?? 'project',
75
+ });
76
+ });
77
+ // GET /:name — 元数据 + 源码。可选 ?projectId=xxx 查找项目级工作流。
78
+ app.get('/:name', async (c) => {
79
+ const name = c.req.param('name');
80
+ const registry = ctx.workflowRegistry;
81
+ if (!registry) {
82
+ return apiError(c, 500, 'NOT_INITIALIZED', 'Workflow registry not initialized');
83
+ }
84
+ let entry = registry.get(name);
85
+ // 项目级 fallback
86
+ if (!entry) {
87
+ const projectId = c.req.query('projectId');
88
+ if (projectId) {
89
+ const project = await getProject(ctx.db, projectId);
90
+ if (project) {
91
+ const discovered = await discoverWorkflows(project.worktree);
92
+ entry = discovered.find((w) => w.meta.name === name);
93
+ }
94
+ }
95
+ }
35
96
  if (!entry) {
36
97
  return apiError(c, 404, 'NOT_FOUND', `Workflow "${name}" not found`);
37
98
  }
@@ -44,14 +105,25 @@ function createWorkflowsRoute(ctx) {
44
105
  sourceCode: entry.sourceCode,
45
106
  });
46
107
  });
47
- // POST /:name/run — 执行工作流(SSE 推送进度)
108
+ // POST /:name/run — 执行工作流(SSE 推送进度)。可选 ?projectId=xxx 执行项目级工作流。
48
109
  app.post('/:name/run', async (c) => {
49
110
  const name = c.req.param('name');
50
111
  const registry = ctx.workflowRegistry;
51
112
  if (!registry) {
52
113
  return apiError(c, 500, 'NOT_INITIALIZED', 'Workflow registry not initialized');
53
114
  }
54
- const entry = registry.get(name);
115
+ let entry = registry.get(name);
116
+ // 项目级 fallback + 解析项目 worktree 作为 agent cwd
117
+ let agentCwd = ctx.cwd;
118
+ const projectId = c.req.query('projectId');
119
+ if (!entry && projectId) {
120
+ const project = await getProject(ctx.db, projectId);
121
+ if (project) {
122
+ agentCwd = project.worktree;
123
+ const discovered = await discoverWorkflows(project.worktree);
124
+ entry = discovered.find((w) => w.meta.name === name);
125
+ }
126
+ }
55
127
  if (!entry) {
56
128
  return apiError(c, 404, 'NOT_FOUND', `Workflow "${name}" not found`);
57
129
  }
@@ -71,7 +143,7 @@ function createWorkflowsRoute(ctx) {
71
143
  toolRegistry: ctx.toolRegistry,
72
144
  permission: autoAllowChecker,
73
145
  config: ctx.config,
74
- cwd: ctx.cwd,
146
+ cwd: agentCwd,
75
147
  agentRegistry: ctx.agentRegistry,
76
148
  });
77
149
  return streamSSE(c, async (stream) => {
@@ -81,13 +153,14 @@ function createWorkflowsRoute(ctx) {
81
153
  toolRegistry: ctx.toolRegistry,
82
154
  permission: autoAllowChecker,
83
155
  config: ctx.config,
84
- cwd: ctx.cwd,
156
+ cwd: agentCwd,
85
157
  agentRegistry: ctx.agentRegistry,
86
158
  };
87
159
  try {
88
160
  const result = await executeWorkflow({
89
161
  registry,
90
162
  name,
163
+ entry,
91
164
  args,
92
165
  deps,
93
166
  parent,
@@ -36,6 +36,11 @@ declare function buildRegistryFromConfig(config: Config): Registry;
36
36
  * 使运行中的 ServerContext 立即生效,无需重启。
37
37
  */
38
38
  declare function syncRegistryFromConfig(registry: Registry, config: Config): void;
39
+ /** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则全局数据根下 pglite 子目录。
40
+ *
41
+ * 历史上数据库放在 <cwd>/.c0de/pglite(项目级),但项目 DB schema 通过 projectId
42
+ * 区分项目,设计本意是全局共享单库。旧路径已在 migrateLegacyPglite 中自动迁移。 */
43
+ declare function resolveDbDir(): string;
39
44
  /**
40
45
  * 围绕已有 DB handle 组装 ServerContext(不建/不关闭 DB)。
41
46
  *
@@ -47,10 +52,12 @@ declare function buildServerContext(db: DB, opts?: StartServerOptions): Promise<
47
52
  ctx: ServerContext;
48
53
  dispose: () => Promise<void>;
49
54
  }>;
55
+ /** Release the dev DB lock (best-effort, stale-detection covers missed calls). */
56
+ declare function releaseDevDbLock(dataDir: string): void;
50
57
  /** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
51
58
  declare function createDevDb(cwd: string): Promise<DB>;
52
59
  /** 初始化 DB + 配置 + 注册表,返回 ServerContext + 清理函数(dev 与独立后端共用)。 */
53
60
  declare function bootstrapServerContext(opts?: StartServerOptions): Promise<BootstrappedServer>;
54
61
  declare function startServer(opts?: StartServerOptions): Promise<RunningServer>;
55
62
  export type { BootstrappedServer, RunningServer, StartServerOptions };
56
- export { bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, startServer, syncRegistryFromConfig, };
63
+ export { bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveDbDir, startServer, syncRegistryFromConfig, };