@fieldwangai/agentflow 0.1.29 → 0.1.30

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 (60) hide show
  1. package/agents/agentflow-node-executor-code.md +3 -2
  2. package/agents/agentflow-node-executor-planning.md +3 -2
  3. package/agents/agentflow-node-executor-requirement.md +3 -2
  4. package/agents/agentflow-node-executor-test.md +3 -2
  5. package/agents/agentflow-node-executor-ui.md +3 -2
  6. package/agents/agentflow-node-executor.md +3 -2
  7. package/agents/en/agentflow-node-executor.md +3 -2
  8. package/agents/zh/agentflow-node-executor.md +3 -2
  9. package/bin/lib/agent-runners.mjs +38 -13
  10. package/bin/lib/api-runner.mjs +6 -3
  11. package/bin/lib/auth.mjs +240 -0
  12. package/bin/lib/catalog-agents.mjs +2 -2
  13. package/bin/lib/catalog-flows.mjs +192 -16
  14. package/bin/lib/composer-agent.mjs +21 -1
  15. package/bin/lib/composer-skill-router.mjs +10 -78
  16. package/bin/lib/flow-import.mjs +2 -2
  17. package/bin/lib/flow-write.mjs +20 -20
  18. package/bin/lib/help.mjs +2 -2
  19. package/bin/lib/locales/en.json +25 -1
  20. package/bin/lib/locales/zh.json +25 -1
  21. package/bin/lib/main.mjs +6 -1
  22. package/bin/lib/node-exec-context.mjs +5 -5
  23. package/bin/lib/node-execute.mjs +14 -9
  24. package/bin/lib/paths.mjs +64 -13
  25. package/bin/lib/recent-runs.mjs +2 -2
  26. package/bin/lib/run-node-statuses-from-disk.mjs +3 -3
  27. package/bin/lib/runtime-context.mjs +225 -0
  28. package/bin/lib/scheduler.mjs +41 -38
  29. package/bin/lib/skill-registry.mjs +145 -0
  30. package/bin/lib/ui-server.mjs +902 -57
  31. package/bin/lib/workspace-tree.mjs +4 -3
  32. package/bin/lib/workspace.mjs +9 -11
  33. package/bin/pipeline/build-node-prompt.mjs +29 -4
  34. package/bin/pipeline/get-exec-id.mjs +2 -2
  35. package/bin/pipeline/get-resolved-values.mjs +1 -0
  36. package/bin/pipeline/pre-process-node.mjs +306 -6
  37. package/bin/pipeline/validate-flow.mjs +2 -0
  38. package/builtin/nodes/agent_subAgent.md +7 -1
  39. package/builtin/nodes/control_cd_workspace.md +43 -0
  40. package/builtin/nodes/control_load_skills.md +48 -0
  41. package/builtin/nodes/display_ascii.md +17 -0
  42. package/builtin/nodes/display_markdown.md +17 -0
  43. package/builtin/nodes/display_mermaid.md +17 -0
  44. package/builtin/nodes/tool_git_checkout.md +54 -0
  45. package/builtin/nodes/tool_nodejs.md +8 -1
  46. package/builtin/nodes/tool_print.md +4 -1
  47. package/builtin/web-ui/dist/assets/index-NdVOJLL9.js +196 -0
  48. package/builtin/web-ui/dist/assets/index-naVI6LZj.css +1 -0
  49. package/builtin/web-ui/dist/index.html +2 -2
  50. package/package.json +1 -1
  51. package/skills/agentflow-flow-recipes/SKILL.md +24 -0
  52. package/skills/agentflow-flow-recipes/references/recipes.md +63 -0
  53. package/skills/agentflow-node-reference/SKILL.md +25 -0
  54. package/skills/agentflow-node-reference/references/builtin-nodes.md +210 -0
  55. package/skills/agentflow-placeholder-reference/SKILL.md +24 -0
  56. package/skills/agentflow-placeholder-reference/references/placeholders.md +20 -0
  57. package/skills/agentflow-runtime-reference/SKILL.md +25 -0
  58. package/skills/agentflow-runtime-reference/references/runtime.md +64 -0
  59. package/builtin/web-ui/dist/assets/index-0vJxkTJz.css +0 -1
  60. package/builtin/web-ui/dist/assets/index-h69bpxLI.js +0 -190
@@ -0,0 +1,225 @@
1
+ import path from "path";
2
+ import { getFlowDir } from "./workspace.mjs";
3
+ import { PACKAGE_ROOT, PIPELINES_DIR } from "./paths.mjs";
4
+ import { listSkills, listSkillsFromSources, workspaceSkillSources } from "./skill-registry.mjs";
5
+
6
+ function parseJsonObject(raw) {
7
+ if (raw == null) return null;
8
+ if (typeof raw === "object" && !Array.isArray(raw)) return raw;
9
+ const text = String(raw || "").trim();
10
+ if (!text) return null;
11
+ try {
12
+ const parsed = JSON.parse(text);
13
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function getPipelineFlowDir(workspaceRoot, flowName, flowJson = null) {
20
+ if (flowJson?.flowDir && typeof flowJson.flowDir === "string" && flowJson.flowDir.trim()) {
21
+ return path.isAbsolute(flowJson.flowDir) ? path.resolve(flowJson.flowDir) : path.resolve(workspaceRoot, flowJson.flowDir);
22
+ }
23
+ return getFlowDir(workspaceRoot, flowName) || path.join(path.resolve(workspaceRoot), PIPELINES_DIR, flowName);
24
+ }
25
+
26
+ export function buildDefaultWorkspaceContext(workspaceRoot, flowName, flowJson = null) {
27
+ const pipelineWorkspace = path.resolve(workspaceRoot);
28
+ const flowDir = getPipelineFlowDir(workspaceRoot, flowName, flowJson);
29
+ return {
30
+ version: 1,
31
+ label: "pipeline",
32
+ cwd: pipelineWorkspace,
33
+ workspaceRoot: pipelineWorkspace,
34
+ pipelineWorkspace,
35
+ flowDir,
36
+ previous: null,
37
+ };
38
+ }
39
+
40
+ export function normalizeWorkspaceContext(raw, workspaceRoot, flowName, flowJson = null) {
41
+ const base = buildDefaultWorkspaceContext(workspaceRoot, flowName, flowJson);
42
+ const parsed = parseJsonObject(raw);
43
+ if (!parsed) return base;
44
+ const cwdRaw = parsed.cwd || parsed.workspaceRoot || parsed.path || "";
45
+ const cwd = cwdRaw ? path.resolve(String(cwdRaw)) : base.cwd;
46
+ return {
47
+ ...base,
48
+ ...parsed,
49
+ version: 1,
50
+ cwd,
51
+ workspaceRoot: cwd,
52
+ pipelineWorkspace: path.resolve(parsed.pipelineWorkspace || base.pipelineWorkspace),
53
+ flowDir: path.resolve(parsed.flowDir || base.flowDir),
54
+ previous: parsed.previous && typeof parsed.previous === "object" ? parsed.previous : null,
55
+ };
56
+ }
57
+
58
+ export function normalizeSkillsContext(raw) {
59
+ const parsed = parseJsonObject(raw);
60
+ if (!parsed) return null;
61
+ return {
62
+ version: 1,
63
+ ...parsed,
64
+ skills: Array.isArray(parsed.skills) ? parsed.skills : [],
65
+ skillKeys: Array.isArray(parsed.skillKeys) ? parsed.skillKeys : [],
66
+ sources: Array.isArray(parsed.sources) ? parsed.sources : [],
67
+ warnings: Array.isArray(parsed.warnings) ? parsed.warnings : [],
68
+ };
69
+ }
70
+
71
+ export function expandRuntimePlaceholders(text, workspaceContext, extra = {}) {
72
+ if (text == null) return "";
73
+ const raw = String(text).trim();
74
+ if (!raw) return "";
75
+ const values = {
76
+ workspaceRoot: workspaceContext.workspaceRoot || workspaceContext.cwd || "",
77
+ cwd: workspaceContext.cwd || workspaceContext.workspaceRoot || "",
78
+ pipelineWorkspace: workspaceContext.pipelineWorkspace || "",
79
+ flowDir: workspaceContext.flowDir || "",
80
+ ...extra,
81
+ };
82
+ return raw.replace(/\$\{([^}]+)\}/g, (_, key) => {
83
+ const k = String(key || "").trim();
84
+ return values[k] != null ? String(values[k]) : "";
85
+ });
86
+ }
87
+
88
+ export function resolveWorkspaceTarget(rawTarget, workspaceContext, extra = {}) {
89
+ const expanded = expandRuntimePlaceholders(rawTarget, workspaceContext, extra).trim();
90
+ if (!expanded || expanded === "pipeline" || expanded === "pipeline-workspace") {
91
+ return workspaceContext.pipelineWorkspace;
92
+ }
93
+ if (expanded === "current" || expanded === ".") return workspaceContext.cwd;
94
+ if (expanded === "flowDir") return workspaceContext.flowDir;
95
+ return path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(workspaceContext.cwd, expanded);
96
+ }
97
+
98
+ export function scanSkillsFromPaths(paths, opts = {}) {
99
+ const sources = (paths || []).map((entry, index) => ({
100
+ source: entry.source || `runtime-${index}`,
101
+ sourceLabel: entry.sourceLabel || entry.label || entry.dir,
102
+ dir: path.resolve(entry.dir),
103
+ installedBy: entry.installedBy || "runtime",
104
+ }));
105
+ return listSkillsFromSources(sources, { ...opts, warnMissing: true });
106
+ }
107
+
108
+ export function parseSkillKeyList(raw) {
109
+ if (Array.isArray(raw)) return raw.map((x) => String(x || "").trim()).filter(Boolean);
110
+ return String(raw || "")
111
+ .split(/[\s,]+/)
112
+ .map((x) => x.trim())
113
+ .filter(Boolean);
114
+ }
115
+
116
+ function skillBodyFromRegistryItem(skill) {
117
+ return {
118
+ name: skill.name,
119
+ key: skill.key,
120
+ description: skill.description,
121
+ source: skill.source,
122
+ sourceLabel: skill.sourceLabel,
123
+ path: skill.path,
124
+ body: skill.body,
125
+ };
126
+ }
127
+
128
+ export function buildSkillsContextFromRegistry({ workspaceContext, skillKeys = [], mergeMode = "replace" }) {
129
+ const wc = workspaceContext;
130
+ const wanted = parseSkillKeyList(skillKeys);
131
+ const registryWorkspaceRoot = wc.pipelineWorkspace || wc.workspaceRoot || wc.cwd || process.cwd();
132
+ const registry = listSkills(PACKAGE_ROOT, registryWorkspaceRoot);
133
+ const skillBodies = [];
134
+ const warnings = [];
135
+ const seenKeys = new Set();
136
+
137
+ for (const raw of wanted) {
138
+ const key = String(raw || "").trim();
139
+ if (!key) continue;
140
+ const match = registry.find((skill) => skill.key === key || skill.name === key || skill.id === key);
141
+ if (!match) {
142
+ warnings.push(`skill not found: ${key}`);
143
+ continue;
144
+ }
145
+ const dedupeKey = match.key || match.path || match.name;
146
+ if (seenKeys.has(dedupeKey)) continue;
147
+ seenKeys.add(dedupeKey);
148
+ skillBodies.push(skillBodyFromRegistryItem(match));
149
+ }
150
+
151
+ return {
152
+ version: 1,
153
+ workspaceRoot: wc.workspaceRoot,
154
+ cwd: wc.cwd,
155
+ pipelineWorkspace: wc.pipelineWorkspace,
156
+ flowDir: wc.flowDir,
157
+ source: "public-registry",
158
+ mergeMode,
159
+ requestedSkillKeys: wanted,
160
+ skills: skillBodies.map(({ body, ...meta }) => meta),
161
+ skillKeys: skillBodies.map((s) => s.key),
162
+ sources: [...new Set(skillBodies.map((s) => s.sourceLabel || s.source || "").filter(Boolean))],
163
+ loadedCount: skillBodies.length,
164
+ warnings,
165
+ skillBodies,
166
+ };
167
+ }
168
+
169
+ export function buildSkillsContext({ workspaceContext, source = "current-workspace", paths = [], include = [], exclude = [], mergeMode = "replace" }) {
170
+ const wc = workspaceContext;
171
+ const sourcePaths = [];
172
+ const addWorkspace = (root, label) => {
173
+ sourcePaths.push(...workspaceSkillSources(root, label, label));
174
+ };
175
+ if (source === "pipeline-workspace") {
176
+ addWorkspace(wc.pipelineWorkspace, "pipeline");
177
+ } else if (source === "explicit-paths") {
178
+ for (const p of paths) {
179
+ const resolved = path.isAbsolute(p) ? path.resolve(p) : path.resolve(wc.cwd, p);
180
+ sourcePaths.push({ dir: resolved, label: "explicit" });
181
+ }
182
+ } else if (source === "all") {
183
+ addWorkspace(wc.cwd, "current");
184
+ if (path.resolve(wc.cwd) !== path.resolve(wc.pipelineWorkspace)) addWorkspace(wc.pipelineWorkspace, "pipeline");
185
+ } else {
186
+ addWorkspace(wc.cwd, "current");
187
+ }
188
+ const scanned = scanSkillsFromPaths(sourcePaths, { include, exclude });
189
+ return {
190
+ version: 1,
191
+ workspaceRoot: wc.workspaceRoot,
192
+ cwd: wc.cwd,
193
+ source,
194
+ mergeMode,
195
+ skills: scanned.skills.map(({ body, ...meta }) => meta),
196
+ skillKeys: scanned.skills.map((s) => s.key),
197
+ sources: sourcePaths.map((p) => p.dir),
198
+ loadedCount: scanned.skills.length,
199
+ warnings: scanned.warnings,
200
+ skillBodies: scanned.skills.map((s) => ({
201
+ name: s.name,
202
+ key: s.key,
203
+ description: s.description,
204
+ sourceLabel: s.sourceLabel,
205
+ path: s.path,
206
+ body: s.body,
207
+ })),
208
+ };
209
+ }
210
+
211
+ export function renderSkillsContextForPrompt(skillsContext) {
212
+ const ctx = normalizeSkillsContext(skillsContext);
213
+ if (!ctx || !Array.isArray(ctx.skillBodies) || ctx.skillBodies.length === 0) return "";
214
+ const blocks = ctx.skillBodies.slice(0, 20).map((skill) => {
215
+ const body = String(skill.body || "").trim();
216
+ return [
217
+ `### ${skill.name}`,
218
+ skill.description ? `说明:${skill.description}` : "",
219
+ `来源:${skill.path || skill.sourceLabel || ""}`,
220
+ "",
221
+ body.slice(0, 16000),
222
+ ].filter(Boolean).join("\n");
223
+ });
224
+ return ["## 已加载 Skills", "", ...blocks].join("\n\n");
225
+ }
@@ -9,7 +9,7 @@ import {
9
9
  readScheduleState,
10
10
  writeScheduleState,
11
11
  } from "./schedule-config.mjs";
12
- import { getRunDir, PACKAGE_ROOT } from "./paths.mjs";
12
+ import { getAgentflowUserContexts, getRunDir, PACKAGE_ROOT } from "./paths.mjs";
13
13
  import { isApplyProcessAlive } from "./run-apply-active-lock.mjs";
14
14
  import { log } from "./log.mjs";
15
15
  import { writeResult } from "../pipeline/write-result.mjs";
@@ -92,13 +92,13 @@ function buildCliInputArgs(flowDir, presetName) {
92
92
  return args;
93
93
  }
94
94
 
95
- function hasHigherPriorityDuplicate(workspaceRoot, flow) {
95
+ function hasHigherPriorityDuplicate(workspaceRoot, flow, opts = {}) {
96
96
  if ((flow.source || "user") !== "workspace") return false;
97
- return listFlowsJson(workspaceRoot).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
97
+ return listFlowsJson(workspaceRoot, opts).some((f) => f.id === flow.id && !f.archived && (f.source || "user") === "user");
98
98
  }
99
99
 
100
- function getLatestRunUuidForFlow(workspaceRoot, flowId) {
101
- const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000"));
100
+ function getLatestRunUuidForFlow(workspaceRoot, flowId, opts = {}) {
101
+ const runRoot = path.dirname(getRunDir(workspaceRoot, flowId, "00000000000000", opts));
102
102
  if (!fs.existsSync(runRoot)) return null;
103
103
  try {
104
104
  const dirs = fs.readdirSync(runRoot, { withFileTypes: true })
@@ -209,13 +209,13 @@ function readNodeResultStatus(runDir, instanceId) {
209
209
  }
210
210
  }
211
211
 
212
- function isFlowCurrentlyRunning(workspaceRoot, flowId, state) {
212
+ function isFlowCurrentlyRunning(workspaceRoot, flowId, state, opts = {}) {
213
213
  const candidates = [];
214
214
  if (state && typeof state.lastRunUuid === "string") candidates.push(state.lastRunUuid);
215
- const latest = getLatestRunUuidForFlow(workspaceRoot, flowId);
215
+ const latest = getLatestRunUuidForFlow(workspaceRoot, flowId, opts);
216
216
  if (latest) candidates.push(latest);
217
217
  for (const uuid of candidates) {
218
- const runDir = getRunDir(workspaceRoot, flowId, uuid);
218
+ const runDir = getRunDir(workspaceRoot, flowId, uuid, opts);
219
219
  if (isApplyProcessAlive(runDir)) return true;
220
220
  }
221
221
  return false;
@@ -231,7 +231,7 @@ function baseState(flow, schedule, previousState) {
231
231
  };
232
232
  }
233
233
 
234
- function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
234
+ function ensureNextRunAt(workspaceRoot, flow, schedule, state, opts = {}) {
235
235
  const identity = scheduleIdentity(schedule);
236
236
  if (state.scheduleIdentity === identity && state.nextRunAt) return state;
237
237
  const nextRunAt = schedule.enabled && schedule.cron ? computeNextRunAtFromSchedule(schedule) : null;
@@ -240,11 +240,11 @@ function ensureNextRunAt(workspaceRoot, flow, schedule, state) {
240
240
  nextRunAt,
241
241
  lastError: "",
242
242
  };
243
- writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next);
243
+ writeScheduleState(workspaceRoot, flow.id, flow.source || "user", next, opts);
244
244
  return next;
245
245
  }
246
246
 
247
- function startScheduledRun(workspaceRoot, flow, schedule, state) {
247
+ function startScheduledRun(workspaceRoot, flow, schedule, state, opts = {}) {
248
248
  const flowDir = flow.path || "";
249
249
  const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
250
250
  const args = [agentflowBin, "apply", flow.id, "--machine-readable", "--workspace-root", path.resolve(workspaceRoot), "--force"];
@@ -252,7 +252,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
252
252
  const child = spawn(process.execPath, args, {
253
253
  cwd: path.resolve(workspaceRoot),
254
254
  stdio: ["ignore", "pipe", "pipe"],
255
- env: { ...process.env, FORCE_COLOR: "0" },
255
+ env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
256
256
  detached: true,
257
257
  });
258
258
 
@@ -276,7 +276,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
276
276
  lastRunUuid,
277
277
  lastPid: child.pid || null,
278
278
  lastError: "",
279
- });
279
+ }, opts);
280
280
  }
281
281
  } catch {
282
282
  /* ignore non-json lines */
@@ -290,7 +290,7 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
290
290
  });
291
291
 
292
292
  child.on("exit", (code, signal) => {
293
- const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user").state || state;
293
+ const prev = readScheduleState(workspaceRoot, flow.id, flow.source || "user", opts).state || state;
294
294
  writeScheduleState(workspaceRoot, flow.id, flow.source || "user", {
295
295
  ...baseState(flow, schedule, prev),
296
296
  nextRunAt: prev.nextRunAt || computeNextRunAtFromSchedule(schedule),
@@ -300,14 +300,14 @@ function startScheduledRun(workspaceRoot, flow, schedule, state) {
300
300
  lastExitSignal: signal || "",
301
301
  lastFinishedAt: new Date().toISOString(),
302
302
  lastError: code === 0 ? "" : `scheduled run exited with code ${code}${signal ? ` signal ${signal}` : ""}`,
303
- });
303
+ }, opts);
304
304
  });
305
305
 
306
306
  child.unref();
307
307
  return child;
308
308
  }
309
309
 
310
- function startWaitingRunResume(workspaceRoot, flow, waitState) {
310
+ function startWaitingRunResume(workspaceRoot, flow, waitState, opts = {}) {
311
311
  const agentflowBin = path.join(PACKAGE_ROOT, "bin", "agentflow.mjs");
312
312
  const uuid = String(waitState.uuid || "");
313
313
  const instanceId = String(waitState.instanceId || "");
@@ -325,7 +325,7 @@ function startWaitingRunResume(workspaceRoot, flow, waitState) {
325
325
  const child = spawn(process.execPath, args, {
326
326
  cwd: path.resolve(workspaceRoot),
327
327
  stdio: ["ignore", "pipe", "pipe"],
328
- env: { ...process.env, FORCE_COLOR: "0" },
328
+ env: { ...process.env, FORCE_COLOR: "0", AGENTFLOW_USER_ID: opts.userId || "" },
329
329
  detached: true,
330
330
  });
331
331
  child.stdout.on("data", () => {});
@@ -411,17 +411,17 @@ export function cancelScheduledRun(workspaceRoot, flowId, uuid) {
411
411
  return { ok: true, flowId, uuid, cancelledAt, updatedWaits: updated, propagatedWaits: propagated, resumePid };
412
412
  }
413
413
 
414
- export function listScheduleStatuses(workspaceRoot) {
414
+ export function listScheduleStatuses(workspaceRoot, opts = {}) {
415
415
  const rows = [];
416
- for (const flow of listFlowsJson(workspaceRoot)) {
416
+ for (const flow of listFlowsJson(workspaceRoot, opts)) {
417
417
  if (flow.archived || flow.source === "builtin") continue;
418
- const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user");
418
+ const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flow.source || "user", opts);
419
419
  if (!scheduleRes.success) {
420
420
  rows.push({ flowId: flow.id, flowSource: flow.source || "user", enabled: false, error: scheduleRes.error });
421
421
  continue;
422
422
  }
423
423
  const schedule = scheduleRes.schedule;
424
- const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user");
424
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flow.source || "user", opts);
425
425
  const state = stateRes.success ? stateRes.state : {};
426
426
  rows.push({
427
427
  flowId: flow.id,
@@ -433,10 +433,10 @@ export function listScheduleStatuses(workspaceRoot) {
433
433
  nextRunAt: state.nextRunAt || schedule.nextRunAt || null,
434
434
  lastTriggeredAt: state.lastTriggeredAt || null,
435
435
  lastRunUuid: state.lastRunUuid || null,
436
- lastError: hasHigherPriorityDuplicate(workspaceRoot, flow)
436
+ lastError: hasHigherPriorityDuplicate(workspaceRoot, flow, opts)
437
437
  ? "workspace flow is shadowed by a user flow with the same id"
438
438
  : state.lastError || "",
439
- running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state),
439
+ running: isFlowCurrentlyRunning(workspaceRoot, flow.id, state, opts),
440
440
  waiting: countActiveWaitsForFlow(flow),
441
441
  });
442
442
  }
@@ -454,7 +454,9 @@ export async function startScheduler(workspaceRoot, opts = {}) {
454
454
  log.info(`AgentFlow scheduler started. workspace=${path.resolve(workspaceRoot)} poll=${pollMs}ms`);
455
455
  while (true) {
456
456
  const now = Date.now();
457
- for (const flow of listFlowsJson(workspaceRoot)) {
457
+ const contexts = opts.userId ? [{ userId: opts.userId }] : getAgentflowUserContexts();
458
+ for (const scheduleCtx of contexts) {
459
+ for (const flow of listFlowsJson(workspaceRoot, scheduleCtx)) {
458
460
  if (flow.archived || flow.source === "builtin") continue;
459
461
  const flowSource = flow.source || "user";
460
462
  let resumedWaitingRun = false;
@@ -462,7 +464,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
462
464
  if (resumedWaitingRun) break;
463
465
  for (const waitState of readWaitStates(run.runDir)) {
464
466
  if (!waitState || !waitState.wakeAt || !waitState.instanceId) continue;
465
- if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) {
467
+ if (waitState.status === "resuming" && !isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) {
466
468
  const nodeStatus = readNodeResultStatus(run.runDir, String(waitState.instanceId));
467
469
  writeWaitState(waitState, {
468
470
  status: nodeStatus === "pending" ? "waiting" : "resumed",
@@ -472,7 +474,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
472
474
  }
473
475
  if (waitState.status !== "waiting") continue;
474
476
  if (Date.parse(waitState.wakeAt) > now) continue;
475
- if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid })) continue;
477
+ if (isFlowCurrentlyRunning(workspaceRoot, flow.id, { lastRunUuid: run.uuid }, scheduleCtx)) continue;
476
478
  const nextState = {
477
479
  ...waitState,
478
480
  status: "resuming",
@@ -480,7 +482,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
480
482
  resumeStartedAt: new Date().toISOString(),
481
483
  };
482
484
  try {
483
- const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir });
485
+ const child = startWaitingRunResume(workspaceRoot, flow, { ...waitState, uuid: run.uuid, runDir: run.runDir }, scheduleCtx);
484
486
  nextState.resumePid = child.pid || null;
485
487
  writeWaitState(waitState, nextState);
486
488
  resumedWaitingRun = true;
@@ -497,41 +499,41 @@ export async function startScheduler(workspaceRoot, opts = {}) {
497
499
  }
498
500
  }
499
501
 
500
- const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource);
502
+ const scheduleRes = readFlowSchedule(workspaceRoot, flow.id, flowSource, scheduleCtx);
501
503
  if (!scheduleRes.success) {
502
504
  log.debug(`[scheduler] ${flow.id}: ${scheduleRes.error}`);
503
505
  continue;
504
506
  }
505
507
  const schedule = scheduleRes.schedule;
506
508
  if (!schedule.enabled || !schedule.cron) continue;
507
- if (hasHigherPriorityDuplicate(workspaceRoot, flow)) {
508
- const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
509
+ if (hasHigherPriorityDuplicate(workspaceRoot, flow, scheduleCtx)) {
510
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource, scheduleCtx);
509
511
  writeScheduleState(workspaceRoot, flow.id, flowSource, {
510
512
  ...baseState(flow, schedule, stateRes.success ? stateRes.state : {}),
511
513
  nextRunAt: null,
512
514
  lastError: "workspace flow is shadowed by a user flow with the same id; scheduled run skipped",
513
515
  lastErrorAt: new Date().toISOString(),
514
- });
516
+ }, scheduleCtx);
515
517
  continue;
516
518
  }
517
- const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource);
518
- let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {});
519
+ const stateRes = readScheduleState(workspaceRoot, flow.id, flowSource, scheduleCtx);
520
+ let state = ensureNextRunAt(workspaceRoot, flow, schedule, stateRes.success ? stateRes.state : {}, scheduleCtx);
519
521
  if (!state.nextRunAt || Date.parse(state.nextRunAt) > now) continue;
520
522
 
521
- if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state)) {
523
+ if (isFlowCurrentlyRunning(workspaceRoot, flow.id, state, scheduleCtx)) {
522
524
  const nextRunAt = computeNextRunAtFromSchedule(schedule);
523
525
  writeScheduleState(workspaceRoot, flow.id, flowSource, {
524
526
  ...baseState(flow, schedule, state),
525
527
  nextRunAt,
526
528
  lastSkippedAt: new Date().toISOString(),
527
529
  lastSkipReason: "running",
528
- });
530
+ }, scheduleCtx);
529
531
  log.info(`[scheduler] skip ${flow.id}: already running; next=${nextRunAt}`);
530
532
  continue;
531
533
  }
532
534
 
533
535
  try {
534
- const child = startScheduledRun(workspaceRoot, flow, schedule, state);
536
+ const child = startScheduledRun(workspaceRoot, flow, schedule, state, scheduleCtx);
535
537
  const nextRunAt = computeNextRunAtFromSchedule(schedule);
536
538
  writeScheduleState(workspaceRoot, flow.id, flowSource, {
537
539
  ...baseState(flow, schedule, state),
@@ -539,7 +541,7 @@ export async function startScheduler(workspaceRoot, opts = {}) {
539
541
  lastTriggeredAt: new Date().toISOString(),
540
542
  lastPid: child.pid || null,
541
543
  lastError: "",
542
- });
544
+ }, scheduleCtx);
543
545
  log.info(`[scheduler] triggered ${flow.id}; pid=${child.pid || "?"}; next=${nextRunAt}`);
544
546
  } catch (e) {
545
547
  const nextRunAt = computeNextRunAtFromSchedule(schedule);
@@ -548,10 +550,11 @@ export async function startScheduler(workspaceRoot, opts = {}) {
548
550
  nextRunAt,
549
551
  lastError: e && e.message ? e.message : String(e),
550
552
  lastErrorAt: new Date().toISOString(),
551
- });
553
+ }, scheduleCtx);
552
554
  log.info(`[scheduler] failed ${flow.id}: ${e && e.message ? e.message : String(e)}`);
553
555
  }
554
556
  }
557
+ }
555
558
  if (once) return;
556
559
  await sleep(pollMs);
557
560
  }
@@ -0,0 +1,145 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import yaml from "js-yaml";
5
+
6
+ const fileCache = new Map();
7
+ const CACHE_TTL_MS = 60_000;
8
+
9
+ function readFileCached(absPath) {
10
+ const now = Date.now();
11
+ const cached = fileCache.get(absPath);
12
+ if (cached && now - cached.ts < CACHE_TTL_MS) return cached.content;
13
+ try {
14
+ const content = fs.readFileSync(absPath, "utf-8");
15
+ fileCache.set(absPath, { content, ts: now });
16
+ return content;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function stripSkillFrontmatter(content) {
23
+ const raw = String(content || "");
24
+ const match = raw.match(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/);
25
+ return match ? raw.slice(match[0].length).trim() : raw.trim();
26
+ }
27
+
28
+ function parseSkillFrontmatter(content) {
29
+ const match = String(content || "").match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
30
+ if (!match) return {};
31
+ try {
32
+ return yaml.load(match[1]) || {};
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ export function parseSkillFile(absPath, source = {}) {
39
+ const content = readFileCached(absPath);
40
+ if (!content) return null;
41
+ const meta = parseSkillFrontmatter(content);
42
+ const name = String(meta.name || path.basename(path.dirname(absPath))).trim();
43
+ if (!name) return null;
44
+ const body = stripSkillFrontmatter(content);
45
+ const sourceId = String(source.source || source.id || "unknown").trim() || "unknown";
46
+ const sourceLabel = String(source.sourceLabel || source.label || sourceId).trim() || sourceId;
47
+ return {
48
+ key: `${sourceId}:${name}`,
49
+ id: name,
50
+ name,
51
+ description: String(meta.description || "").trim(),
52
+ source: sourceId,
53
+ sourceLabel,
54
+ path: absPath,
55
+ body,
56
+ content,
57
+ installedBy: source.installedBy || "",
58
+ };
59
+ }
60
+
61
+ export function listSkillFiles(dir) {
62
+ try {
63
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
64
+ return fs.readdirSync(dir, { withFileTypes: true })
65
+ .filter((entry) => entry.isDirectory())
66
+ .map((entry) => path.join(dir, entry.name, "SKILL.md"))
67
+ .filter((skillPath) => fs.existsSync(skillPath));
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ export function workspaceSkillSources(root, prefix, labelPrefix) {
74
+ if (!root) return [];
75
+ const abs = path.resolve(root);
76
+ return [
77
+ { source: `${prefix}-agents`, sourceLabel: `${labelPrefix}/.agents`, dir: path.join(abs, ".agents", "skills"), installedBy: "filesystem" },
78
+ { source: `${prefix}-cursor`, sourceLabel: `${labelPrefix}/.cursor`, dir: path.join(abs, ".cursor", "skills"), installedBy: "filesystem" },
79
+ { source: `${prefix}-codex`, sourceLabel: `${labelPrefix}/.codex`, dir: path.join(abs, ".codex", "skills"), installedBy: "filesystem" },
80
+ ];
81
+ }
82
+
83
+ export function defaultSkillSources(packageRoot, workspaceRoot) {
84
+ const sources = [
85
+ {
86
+ source: "builtin",
87
+ sourceLabel: "AgentFlow",
88
+ dir: path.join(path.resolve(packageRoot), "skills"),
89
+ installedBy: "agentflow",
90
+ },
91
+ ];
92
+ if (workspaceRoot) sources.push(...workspaceSkillSources(workspaceRoot, "workspace", "工作区"));
93
+ const home = os.homedir();
94
+ sources.push(
95
+ { source: "global-agents", sourceLabel: "全局 .agents", dir: path.join(home, ".agents", "skills"), installedBy: "global" },
96
+ { source: "global-cursor", sourceLabel: "全局 .cursor", dir: path.join(home, ".cursor", "skills"), installedBy: "global" },
97
+ { source: "global-codex", sourceLabel: "全局 .codex", dir: path.join(home, ".codex", "skills"), installedBy: "skillhub" },
98
+ );
99
+ return sources;
100
+ }
101
+
102
+ export function listSkillsFromSources(sources, opts = {}) {
103
+ const include = new Set((opts.include || []).map((x) => String(x).trim()).filter(Boolean));
104
+ const exclude = new Set((opts.exclude || []).map((x) => String(x).trim()).filter(Boolean));
105
+ const skills = [];
106
+ const warnings = [];
107
+ const seenKeys = new Set();
108
+ const seenPaths = new Set();
109
+ for (const source of sources || []) {
110
+ const dir = path.resolve(source.dir || "");
111
+ if (!fs.existsSync(dir)) {
112
+ if (opts.warnMissing) warnings.push(`skills path not found: ${dir}`);
113
+ continue;
114
+ }
115
+ for (const skillPath of listSkillFiles(dir)) {
116
+ const skill = parseSkillFile(skillPath, source);
117
+ if (!skill) continue;
118
+ if (include.size > 0 && !include.has(skill.name) && !include.has(skill.key)) continue;
119
+ if (exclude.has(skill.name) || exclude.has(skill.key)) continue;
120
+ if (seenPaths.has(skill.path)) continue;
121
+ if (seenKeys.has(skill.key)) continue;
122
+ seenPaths.add(skill.path);
123
+ seenKeys.add(skill.key);
124
+ skills.push(skill);
125
+ }
126
+ }
127
+ skills.sort((a, b) => {
128
+ const bySource = a.sourceLabel.localeCompare(b.sourceLabel);
129
+ if (bySource !== 0) return bySource;
130
+ return a.name.localeCompare(b.name);
131
+ });
132
+ return { skills, warnings };
133
+ }
134
+
135
+ export function listSkills(packageRoot, workspaceRoot, opts = {}) {
136
+ return listSkillsFromSources(defaultSkillSources(packageRoot, workspaceRoot), opts).skills;
137
+ }
138
+
139
+ export function readSkillDetail(packageRoot, workspaceRoot, keyOrName) {
140
+ const wanted = String(keyOrName || "").trim();
141
+ if (!wanted) return null;
142
+ const item = listSkills(packageRoot, workspaceRoot).find((skill) => skill.key === wanted || skill.name === wanted);
143
+ if (!item) return null;
144
+ return item;
145
+ }