agentflowctl 0.2.0 → 0.4.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/proc.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  export function exec(cmd, args, opts = {}) {
3
3
  return new Promise((resolve, reject) => {
4
- const env = { ...process.env, ...opts.env };
5
- for (const key of opts.unsetEnv ?? [])
6
- delete env[key];
7
4
  const child = spawn(cmd, args, {
8
5
  cwd: opts.cwd,
9
- env,
6
+ env: { ...process.env, ...opts.env },
10
7
  shell: opts.shell ?? false,
11
8
  });
12
9
  let stdout = "";
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, DEFAULT_CYCLE } 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
  * 各家的錯誤訊息會隨版本變動,這裡用寬鬆的樣式比對,且只在執行失敗時才檢查,避免誤判正常輸出。
@@ -56,15 +48,18 @@ function appendLog(file, text) {
56
48
  mkdirSync(dirname(file), { recursive: true });
57
49
  appendFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
58
50
  }
59
- /** 內建的 claude、codex、gemini 定義,可在 flow.config.json 覆寫或新增其他 agent */
51
+ /** 一行工具事件的畫面輸出:完整顯示指令或參數,多行時後續行縮排對齊 */
52
+ export function formatToolLine(agent, tool) {
53
+ const head = ` 🔧 [${agent}] ${tool.name}`;
54
+ const detail = tool.detail?.trim();
55
+ return detail ? `${head}: ${detail.split("\n").join("\n ")}` : head;
56
+ }
57
+ /** 取出 flow.config.json 裡定義的 agent;沒有內建 agent */
60
58
  export function resolveAgent(cfg, name) {
61
- const custom = cfg.agents[name];
62
- if (custom)
63
- return custom;
64
- if (DEFAULT_CYCLE.includes(name)) {
65
- return AgentDef.parse({ adapter: name });
66
- }
67
- 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
+ return def;
68
63
  }
69
64
  /** 確認某個 agent 的 CLI 是否可以執行 */
70
65
  export async function probeAgent(def) {
@@ -88,7 +83,7 @@ export async function runAgent(name, def, t, prompt) {
88
83
  projectRoot: projectRoot(),
89
84
  command: def.command,
90
85
  });
91
- appendLog(t.logFile, `# agent=${name} adapter=${def.adapter} cmd=${inv.cmd}`);
86
+ appendLog(t.logFile, headerLine({ stage: t.stage, step: t.step, agent: name, adapter: def.adapter, startedAt: new Date().toISOString() }));
92
87
  let done;
93
88
  let lastText = "";
94
89
  let inputTokens = 0;
@@ -96,7 +91,6 @@ export async function runAgent(name, def, t, prompt) {
96
91
  const r = await exec(inv.cmd, inv.args, {
97
92
  cwd: t.cwd,
98
93
  env: inv.env,
99
- unsetEnv: API_KEY_VARS,
100
94
  input: inv.input,
101
95
  onStdoutLine: (line) => {
102
96
  if (!line.trim())
@@ -105,10 +99,12 @@ export async function runAgent(name, def, t, prompt) {
105
99
  for (const ev of adapter.parse(line)) {
106
100
  if (ev.kind === "text") {
107
101
  lastText = ev.text;
108
- console.log(` 💬 [${name}] ${ev.text.trim().split("\n")[0]?.slice(0, 110)}`);
102
+ if (config.verbose)
103
+ console.log(` 💬 [${name}] ${ev.text.trim().split("\n")[0]?.slice(0, 110)}`);
109
104
  }
110
105
  else if (ev.kind === "tool") {
111
- console.log(` 🔧 [${name}] ${ev.name}`);
106
+ if (config.verbose)
107
+ console.log(formatToolLine(name, ev));
112
108
  }
113
109
  else if (ev.kind === "usage") {
114
110
  inputTokens += ev.inputTokens ?? 0;
@@ -121,8 +117,11 @@ export async function runAgent(name, def, t, prompt) {
121
117
  },
122
118
  });
123
119
  if (r.stderr.trim())
124
- appendLog(t.logFile, `[stderr]\n${r.stderr}`);
120
+ appendLog(t.logFile, `${STDERR_MARK}\n${r.stderr}`);
125
121
  const ok = r.code === 0 && (done?.ok ?? true);
122
+ appendLog(t.logFile, footerLine({ code: r.code, ok, endedAt: new Date().toISOString() }));
123
+ if (!ok)
124
+ console.log(` ✗ ${name} 執行失敗(結束碼 ${r.code}),可用 agentflowctl logs ${t.runId} ${logSeq(t.logFile)} 查看`);
126
125
  const summary = done?.summary || lastText || tail(r.stdout, 2000) || tail(r.stderr, 2000);
127
126
  const quotaExhausted = !ok && isQuotaError(`${summary}\n${done?.summary ?? ""}\n${r.stderr}\n${tail(r.stdout, 4000)}`);
128
127
  const meta = parseResultMeta(summary) ?? parseResultMeta(lastText);
@@ -133,10 +132,15 @@ export async function runAgent(name, def, t, prompt) {
133
132
  * 注意:這些指令會執行 agent 寫出來的程式碼,而且不在任何沙箱內。
134
133
  */
135
134
  export async function runCommand(t, cmd) {
135
+ appendLog(t.logFile, headerLine({ stage: t.stage, step: t.step, agent: CMD_AGENT, startedAt: new Date().toISOString() }));
136
136
  appendLog(t.logFile, `$ ${cmd}`);
137
+ if (config.verbose)
138
+ console.log(` $ ${cmd.trim().split("\n").join("\n ")}`);
137
139
  const r = await execShell(cmd, { cwd: t.cwd });
138
140
  const output = `${r.stdout}\n${r.stderr}`.trim();
139
- appendLog(t.logFile, output);
140
- return { ok: r.code === 0, output };
141
+ if (output)
142
+ appendLog(t.logFile, output);
143
+ appendLog(t.logFile, footerLine({ code: r.code, ok: r.code === 0, endedAt: new Date().toISOString() }));
144
+ return { ok: r.code === 0, output, seq: logSeq(t.logFile) };
141
145
  }
142
146
  //# sourceMappingURL=runner.js.map
package/dist/schemas.js CHANGED
@@ -49,9 +49,9 @@ 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
- /** 輪替順序;未設定時自動偵測已安裝的 CLI */
54
+ /** 輪替順序;未設定時依 agents 的順序,取已安裝的 CLI */
55
55
  cycle: z.array(z.string()).min(1).optional(),
56
56
  /** review 後的修正由誰做:ring=輪到下一位;author=最後寫程式的 agent */
57
57
  fixStrategy: z.enum(["ring", "author"]).default("ring"),
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("輪替順序,用逗號分隔", 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(` 輪替順序:${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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentflowctl",
3
3
  "license": "MIT",
4
- "version": "0.2.0",
4
+ "version": "0.4.0",
5
5
  "description": "跨廠商 AI 開發 harness:Claude Code、Codex、Gemini 輪流實作、審查、修正",
6
6
  "keywords": [
7
7
  "ai",