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/README.md +154 -16
- package/dist/agentConfig.js +6 -11
- package/dist/agents/claude.js +2 -2
- package/dist/agents/codex.js +6 -3
- package/dist/agents/gemini.js +2 -2
- package/dist/agents/index.js +0 -2
- package/dist/agents/types.js +14 -0
- package/dist/cleanup.js +54 -0
- package/dist/cli.js +120 -48
- package/dist/config.js +2 -0
- package/dist/detect.js +82 -0
- package/dist/engine.js +23 -13
- package/dist/logs.js +173 -0
- package/dist/paths.js +4 -2
- package/dist/proc.js +1 -4
- package/dist/runner.js +30 -26
- package/dist/schemas.js +2 -2
- package/dist/setup.js +70 -0
- package/package.json +1 -1
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
|
|
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
|
-
/**
|
|
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
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
/**
|
|
52
|
+
/** 可用的 agent;沒有內建,全部都要在這裡定義(通常用 agent add) */
|
|
53
53
|
agents: z.record(z.string(), AgentDef).default({}),
|
|
54
|
-
/**
|
|
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
|