agentflowctl 0.3.0 → 0.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/runner.js CHANGED
@@ -1,20 +1,12 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { z } from "zod";
4
- import { ADAPTERS, builtinAgents } from "./agents/index.js";
5
- import { AgentDef } from "./schemas.js";
4
+ import { ADAPTERS } from "./agents/index.js";
6
5
  import { projectRoot, runDir } from "./paths.js";
6
+ import { config } from "./config.js";
7
+ import { CMD_AGENT, footerLine, headerLine, logSeq, STDERR_MARK } from "./logs.js";
7
8
  import { exec, execShell } from "./proc.js";
8
9
  import { tail } from "./util.js";
9
- /** 會讓各家 CLI 改走 API 計費的環境變數;只用訂閱登入,執行 agent 時一律移除 */
10
- export const API_KEY_VARS = [
11
- "ANTHROPIC_API_KEY",
12
- "ANTHROPIC_AUTH_TOKEN",
13
- "CODEX_API_KEY",
14
- "OPENAI_API_KEY",
15
- "GEMINI_API_KEY",
16
- "GOOGLE_API_KEY",
17
- ];
18
10
  /**
19
11
  * 判斷 agent 失敗是否因為方案額度或速率限制。
20
12
  * 各家的錯誤訊息會隨版本變動,這裡用寬鬆的樣式比對,且只在執行失敗時才檢查,避免誤判正常輸出。
@@ -62,17 +54,13 @@ export function formatToolLine(agent, tool) {
62
54
  const detail = tool.detail?.trim();
63
55
  return detail ? `${head}: ${detail.split("\n").join("\n ")}` : head;
64
56
  }
65
- /** 內建的 claude、codex、gemini 定義,可在 flow.config.json 覆寫或新增其他 agent */
57
+ /** 取出 flow.config.json 裡定義的 agent;沒有內建 agent */
66
58
  export function resolveAgent(cfg, name) {
67
- const custom = cfg.agents[name];
68
- if (custom)
69
- return custom;
70
- if (builtinAgents(cfg.removedAgents).includes(name)) {
71
- return AgentDef.parse({ adapter: name });
72
- }
73
- if (cfg.removedAgents.includes(name))
74
- throw new Error(`${name} 已從設定移除(removedAgents),要使用請先 agent add ${name} --adapter ${name}`);
75
- throw new Error(`未定義的 agent:${name}(請在 flow.config.json 的 agents 裡設定)`);
59
+ const def = cfg.agents[name];
60
+ if (!def)
61
+ throw new Error(`未定義的 agent:${name}(請先用 agent add ${name} --adapter <adapter> 新增)`);
62
+ const defaultModel = def.adapter === "command" ? undefined : cfg.defaultModels[def.adapter];
63
+ return { ...def, model: def.model ?? defaultModel };
76
64
  }
77
65
  /** 確認某個 agent 的 CLI 是否可以執行 */
78
66
  export async function probeAgent(def) {
@@ -96,7 +84,7 @@ export async function runAgent(name, def, t, prompt) {
96
84
  projectRoot: projectRoot(),
97
85
  command: def.command,
98
86
  });
99
- appendLog(t.logFile, `# agent=${name} adapter=${def.adapter} cmd=${inv.cmd}`);
87
+ appendLog(t.logFile, headerLine({ stage: t.stage, step: t.step, agent: name, adapter: def.adapter, startedAt: new Date().toISOString() }));
100
88
  let done;
101
89
  let lastText = "";
102
90
  let inputTokens = 0;
@@ -104,7 +92,6 @@ export async function runAgent(name, def, t, prompt) {
104
92
  const r = await exec(inv.cmd, inv.args, {
105
93
  cwd: t.cwd,
106
94
  env: inv.env,
107
- unsetEnv: API_KEY_VARS,
108
95
  input: inv.input,
109
96
  onStdoutLine: (line) => {
110
97
  if (!line.trim())
@@ -113,10 +100,12 @@ export async function runAgent(name, def, t, prompt) {
113
100
  for (const ev of adapter.parse(line)) {
114
101
  if (ev.kind === "text") {
115
102
  lastText = ev.text;
116
- console.log(` 💬 [${name}] ${ev.text.trim().split("\n")[0]?.slice(0, 110)}`);
103
+ if (config.verbose)
104
+ console.log(` 💬 [${name}] ${ev.text.trim().split("\n")[0]?.slice(0, 110)}`);
117
105
  }
118
106
  else if (ev.kind === "tool") {
119
- console.log(formatToolLine(name, ev));
107
+ if (config.verbose)
108
+ console.log(formatToolLine(name, ev));
120
109
  }
121
110
  else if (ev.kind === "usage") {
122
111
  inputTokens += ev.inputTokens ?? 0;
@@ -129,8 +118,11 @@ export async function runAgent(name, def, t, prompt) {
129
118
  },
130
119
  });
131
120
  if (r.stderr.trim())
132
- appendLog(t.logFile, `[stderr]\n${r.stderr}`);
121
+ appendLog(t.logFile, `${STDERR_MARK}\n${r.stderr}`);
133
122
  const ok = r.code === 0 && (done?.ok ?? true);
123
+ appendLog(t.logFile, footerLine({ code: r.code, ok, endedAt: new Date().toISOString() }));
124
+ if (!ok)
125
+ console.log(` ✗ ${name} 執行失敗(結束碼 ${r.code}),可用 agentflowctl logs ${t.runId} ${logSeq(t.logFile)} 查看`);
134
126
  const summary = done?.summary || lastText || tail(r.stdout, 2000) || tail(r.stderr, 2000);
135
127
  const quotaExhausted = !ok && isQuotaError(`${summary}\n${done?.summary ?? ""}\n${r.stderr}\n${tail(r.stdout, 4000)}`);
136
128
  const meta = parseResultMeta(summary) ?? parseResultMeta(lastText);
@@ -141,11 +133,15 @@ export async function runAgent(name, def, t, prompt) {
141
133
  * 注意:這些指令會執行 agent 寫出來的程式碼,而且不在任何沙箱內。
142
134
  */
143
135
  export async function runCommand(t, cmd) {
136
+ appendLog(t.logFile, headerLine({ stage: t.stage, step: t.step, agent: CMD_AGENT, startedAt: new Date().toISOString() }));
144
137
  appendLog(t.logFile, `$ ${cmd}`);
145
- console.log(` $ ${cmd.trim().split("\n").join("\n ")}`);
138
+ if (config.verbose)
139
+ console.log(` $ ${cmd.trim().split("\n").join("\n ")}`);
146
140
  const r = await execShell(cmd, { cwd: t.cwd });
147
141
  const output = `${r.stdout}\n${r.stderr}`.trim();
148
- appendLog(t.logFile, output);
149
- return { ok: r.code === 0, output };
142
+ if (output)
143
+ appendLog(t.logFile, output);
144
+ appendLog(t.logFile, footerLine({ code: r.code, ok: r.code === 0, endedAt: new Date().toISOString() }));
145
+ return { ok: r.code === 0, output, seq: logSeq(t.logFile) };
150
146
  }
151
147
  //# sourceMappingURL=runner.js.map
package/dist/schemas.js CHANGED
@@ -39,7 +39,7 @@ export const ReviewResult = z.object({
39
39
  note: z.string().default(""),
40
40
  })),
41
41
  });
42
- /** 一個 agent 的定義;名稱(agents 的 key)用在輪替順序裡 */
42
+ /** 一個 agent 的定義;名稱(agents 的 key)用在 cycle 裡 */
43
43
  export const AgentDef = z.object({
44
44
  adapter: z.enum(["claude", "codex", "gemini", "command"]),
45
45
  model: z.string().optional(),
@@ -49,11 +49,15 @@ export const AgentDef = z.object({
49
49
  });
50
50
  /** 目標專案可選的 flow.config.json,預設值對應 Vite + TypeScript + Vitest 專案 */
51
51
  export const RepoConfig = z.object({
52
- /** 自訂或覆寫 agent;claude、codex、gemini 三個名稱內建 */
52
+ /** 可用的 agent;沒有內建,全部都要在這裡定義(通常用 agent add) */
53
53
  agents: z.record(z.string(), AgentDef).default({}),
54
- /** 移除的內建 agent:不再自動偵測,也不能放進輪替;用 agent add 加回 */
55
- removedAgents: z.array(z.string()).default([]),
56
- /** 輪替順序;未設定時自動偵測已安裝的 CLI */
54
+ /** 各 CLI adapter 的預設模型;agent 自己指定 model 時優先使用個別設定 */
55
+ defaultModels: z.strictObject({
56
+ claude: z.string().trim().min(1).optional(),
57
+ codex: z.string().trim().min(1).optional(),
58
+ gemini: z.string().trim().min(1).optional(),
59
+ }).default({}),
60
+ /** 參與的 agent(順序不影響分工);未設定時取 agents 裡已安裝的 CLI */
57
61
  cycle: z.array(z.string()).min(1).optional(),
58
62
  /** review 後的修正由誰做:ring=輪到下一位;author=最後寫程式的 agent */
59
63
  fixStrategy: z.enum(["ring", "author"]).default("ring"),
@@ -96,7 +100,7 @@ export const FlowRun = z.object({
96
100
  /** 暫停前所在的階段與原因(額度用完時) */
97
101
  pausedStage: Stage.optional(),
98
102
  pauseReason: z.string().optional(),
99
- /** 這個 run 的 agent 輪替順序(建立時決定,resume 時沿用) */
103
+ /** 這個 run 參與的 agent(建立時決定,resume 時沿用) */
100
104
  cycle: z.array(z.string()).min(1),
101
105
  /** 最後一個寫程式的 agent,reviewer 不可以是它 */
102
106
  lastWriter: z.string().optional(),
package/dist/setup.js ADDED
@@ -0,0 +1,70 @@
1
+ import { addAgent, setAgent, setCycle } from "./agentConfig.js";
2
+ /**
3
+ * `agentflowctl agent setup` 的互動精靈。
4
+ * 問答與偵測結果由外部注入,這裡只負責流程;所有修改都在記憶體裡完成,確認後才交給呼叫端寫入。
5
+ */
6
+ export const SETUP_ADAPTERS = ["claude", "codex", "gemini"];
7
+ /** 回傳要寫入的設定;使用者沒選任何 agent 或最後沒確認時回傳 null */
8
+ export async function runSetup(initial, deps) {
9
+ const { detected, log } = deps;
10
+ const ask = async (q, def = "") => (await deps.ask(def ? `${q}(${def}):` : `${q}:`)).trim() || def;
11
+ const confirm = async (q, def) => {
12
+ const a = (await deps.ask(`${q}(${def ? "Y/n" : "y/N"}):`)).trim().toLowerCase();
13
+ return a ? a.startsWith("y") : def;
14
+ };
15
+ let cfg = initial;
16
+ const changes = [];
17
+ const chosen = [];
18
+ const existing = () => Object.keys((cfg.agents ?? {}));
19
+ for (const adapter of SETUP_ADAPTERS) {
20
+ log(`${detected[adapter] ? "✅" : "❌"} ${adapter}${detected[adapter] ? "" : "(沒有偵測到 CLI)"}`);
21
+ if (!(await confirm(` 加入 ${adapter}?`, detected[adapter])))
22
+ continue;
23
+ let name;
24
+ for (;;) {
25
+ name = await ask(" 名稱", adapter);
26
+ if (!/^[\w-]+$/.test(name))
27
+ log(" 名稱只能用英數字、底線與連字號");
28
+ else if (chosen.includes(name))
29
+ log(` ${name} 這次已經用過了`);
30
+ else
31
+ break;
32
+ }
33
+ const isNew = !existing().includes(name);
34
+ if (!isNew && !(await confirm(` ${name} 已存在,要覆寫嗎?`, false))) {
35
+ chosen.push(name);
36
+ continue;
37
+ }
38
+ const model = (await ask(" model(Enter 不指定)")) || undefined;
39
+ const edit = isNew ? addAgent(cfg, name, { adapter, model }) : setAgent(cfg, name, { adapter, model });
40
+ cfg = edit.cfg;
41
+ changes.push(...edit.changes);
42
+ chosen.push(name);
43
+ }
44
+ if (!chosen.length) {
45
+ log("沒有選擇任何 agent,設定沒有變更");
46
+ return null;
47
+ }
48
+ for (;;) {
49
+ const list = (await ask("參與的 agent,用逗號分隔", chosen.join(","))).split(",").map((s) => s.trim()).filter(Boolean);
50
+ try {
51
+ cfg = setCycle(cfg, list).cfg;
52
+ break;
53
+ }
54
+ catch (e) {
55
+ log(` ${e.message}`);
56
+ }
57
+ }
58
+ const agents = cfg.agents;
59
+ log("\n即將寫入:");
60
+ for (const name of chosen)
61
+ log(` ${name.padEnd(14)} adapter=${agents[name].adapter}${agents[name].model ? ` model=${agents[name].model}` : ""}`);
62
+ log(` 參與的 agent:${cfg.cycle.join("、")}`);
63
+ log(" 需要自訂指令的 CLI 請改用 agent add <name> --adapter command -- <指令>");
64
+ if (!(await confirm("寫入 flow.config.json?", true))) {
65
+ log("已取消,設定沒有變更");
66
+ return null;
67
+ }
68
+ return { cfg, changes };
69
+ }
70
+ //# sourceMappingURL=setup.js.map
@@ -7,6 +7,7 @@
7
7
  "planArbiter": true,
8
8
  "tieBreak": "proceed",
9
9
  "maxAgentRuns": 60,
10
+ "defaultModels": { "claude": "Claude 模型名稱", "codex": "Codex 模型名稱" },
10
11
  "agents": {
11
12
  "claude": { "adapter": "claude" },
12
13
  "codex": { "adapter": "codex" }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",