@keyou007/agent-bot 0.1.12 → 0.1.13-alpha.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 (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +23 -10
  3. package/README.zh.md +23 -10
  4. package/config.example.yaml +0 -24
  5. package/dist/cli/AgentPrerequisites.js +262 -0
  6. package/dist/cli/AgentPrerequisites.js.map +1 -0
  7. package/dist/cli/DefaultAgentSelection.js +47 -0
  8. package/dist/cli/DefaultAgentSelection.js.map +1 -0
  9. package/dist/cli/Initializer.js +165 -2
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/help.js +4 -4
  12. package/dist/cli.js +194 -4
  13. package/dist/cli.js.map +1 -1
  14. package/dist/codex/AppServerConnection.js +3 -3
  15. package/dist/codex/AppServerConnection.js.map +1 -1
  16. package/dist/codex/CodexProcessManager.js +4 -4
  17. package/dist/codex/CodexProcessManager.js.map +1 -1
  18. package/dist/codex/CodexRuntime.js +28 -16
  19. package/dist/codex/CodexRuntime.js.map +1 -1
  20. package/dist/config/loadConfig.js +0 -24
  21. package/dist/config/loadConfig.js.map +1 -1
  22. package/dist/console/ConsoleConnector.js +2 -2
  23. package/dist/console/ConsoleConnector.js.map +1 -1
  24. package/dist/feishu/CardRenderer.js +8 -8
  25. package/dist/feishu/CardRenderer.js.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/proxy/ProxySessionController.js +51 -51
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/dist/runtime/AcpRuntimeAdapter.js +3 -2
  30. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  31. package/docs/technical-reference.md +32 -17
  32. package/docs/technical-reference.zh.md +32 -17
  33. package/npm-shrinkwrap.json +2 -2
  34. package/package.json +7 -3
  35. package/skills/agent-bot/SKILL.md +12 -6
  36. package/src/cli/AgentPrerequisites.ts +377 -0
  37. package/src/cli/DefaultAgentSelection.ts +56 -0
  38. package/src/cli/Initializer.ts +204 -3
  39. package/src/cli/help.ts +4 -4
  40. package/src/cli.ts +293 -5
  41. package/src/codex/AppServerConnection.ts +3 -3
  42. package/src/codex/CodexProcessManager.ts +4 -4
  43. package/src/codex/CodexRuntime.ts +38 -18
  44. package/src/config/loadConfig.ts +0 -24
  45. package/src/console/ConsoleConnector.ts +2 -2
  46. package/src/feishu/CardRenderer.ts +8 -8
  47. package/src/index.ts +1 -1
  48. package/src/proxy/ProxySessionController.ts +52 -52
  49. package/src/runtime/AcpRuntimeAdapter.ts +3 -1
@@ -0,0 +1,377 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export type SupportedAgentId = "codex" | "traex";
4
+ export type SupportedAgentState = "missing" | "outdated" | "ready";
5
+ export type AgentMaintenanceKind = "install" | "upgrade";
6
+
7
+ export interface AgentCommandResult {
8
+ status: number | null;
9
+ stdout: string;
10
+ stderr: string;
11
+ error?: string;
12
+ }
13
+
14
+ export interface AgentCommandSpec {
15
+ command: string;
16
+ args: string[];
17
+ display: string;
18
+ timeoutMs: number;
19
+ env?: NodeJS.ProcessEnv;
20
+ inheritStdio?: boolean;
21
+ }
22
+
23
+ export type AgentCommandRunner = (command: AgentCommandSpec) => Promise<AgentCommandResult>;
24
+
25
+ export interface SupportedAgentInspection {
26
+ id: SupportedAgentId;
27
+ name: string;
28
+ state: SupportedAgentState;
29
+ installedVersion?: string;
30
+ latestVersion?: string;
31
+ latestCheckFailed?: boolean;
32
+ action?: {
33
+ kind: AgentMaintenanceKind;
34
+ command: string;
35
+ };
36
+ }
37
+
38
+ export interface InspectSupportedAgentsOptions {
39
+ platform?: NodeJS.Platform;
40
+ env?: NodeJS.ProcessEnv;
41
+ run?: AgentCommandRunner;
42
+ }
43
+
44
+ const CODEX_PACKAGE = "@openai/codex";
45
+ const TRAEX_CHANNEL = "alpha";
46
+ const TRAEX_INSTALL_BASE_URL = "https://code.byted.org/api/tos-proxy/download";
47
+
48
+ export async function inspectSupportedAgents(
49
+ options: InspectSupportedAgentsOptions = {},
50
+ ): Promise<SupportedAgentInspection[]> {
51
+ return Promise.all([
52
+ inspectSupportedAgent("codex", options),
53
+ inspectSupportedAgent("traex", options),
54
+ ]);
55
+ }
56
+
57
+ export async function inspectSupportedAgent(
58
+ id: SupportedAgentId,
59
+ options: InspectSupportedAgentsOptions = {},
60
+ ): Promise<SupportedAgentInspection> {
61
+ const platform = options.platform ?? process.platform;
62
+ const env = options.env ?? process.env;
63
+ const run = options.run ?? runAgentCommand;
64
+ return id === "codex"
65
+ ? inspectCodex(platform, env, run)
66
+ : inspectTraex(platform, env, run);
67
+ }
68
+
69
+ export async function runSupportedAgentMaintenance(
70
+ id: SupportedAgentId,
71
+ kind: AgentMaintenanceKind,
72
+ options: InspectSupportedAgentsOptions = {},
73
+ ): Promise<AgentCommandResult> {
74
+ const platform = options.platform ?? process.platform;
75
+ const env = options.env ?? process.env;
76
+ const run = options.run ?? runAgentCommand;
77
+ const command = maintenanceCommand(id, kind, platform, env);
78
+ const result = await run({ ...command, inheritStdio: true });
79
+ if (id !== "codex" || kind !== "upgrade" || commandSucceeded(result)) return result;
80
+ const fallback = codexNpmInstallCommand(platform, env);
81
+ return run({ ...fallback, inheritStdio: true });
82
+ }
83
+
84
+ async function inspectCodex(
85
+ platform: NodeJS.Platform,
86
+ env: NodeJS.ProcessEnv,
87
+ run: AgentCommandRunner,
88
+ ): Promise<SupportedAgentInspection> {
89
+ const installed = await run(capturedCommand("codex", ["--version"], platform, env, 5_000));
90
+ const installedVersion = commandVersion(installed);
91
+ if (!installedVersion) return missingInspection("codex", "Codex", platform, env);
92
+
93
+ const latest = await run(capturedCommand(
94
+ "npm",
95
+ ["view", CODEX_PACKAGE, "version", "--json"],
96
+ platform,
97
+ env,
98
+ 15_000,
99
+ ));
100
+ const latestVersion = npmVersion(latest);
101
+ if (!latestVersion) {
102
+ return { id: "codex", name: "Codex", state: "ready", installedVersion, latestCheckFailed: true };
103
+ }
104
+ if (compareSemanticVersions(installedVersion, latestVersion) < 0) {
105
+ return {
106
+ id: "codex",
107
+ name: "Codex",
108
+ state: "outdated",
109
+ installedVersion,
110
+ latestVersion,
111
+ action: maintenanceAction("codex", "upgrade", platform, env),
112
+ };
113
+ }
114
+ return { id: "codex", name: "Codex", state: "ready", installedVersion, latestVersion };
115
+ }
116
+
117
+ async function inspectTraex(
118
+ platform: NodeJS.Platform,
119
+ env: NodeJS.ProcessEnv,
120
+ run: AgentCommandRunner,
121
+ ): Promise<SupportedAgentInspection> {
122
+ const installed = await run(capturedCommand("traex", ["--version"], platform, env, 5_000));
123
+ const installedVersion = commandVersion(installed);
124
+ if (!installedVersion) return missingInspection("traex", "TraeX", platform, env);
125
+
126
+ const latest = await run(capturedCommand(
127
+ "traex",
128
+ ["update", "--channel", TRAEX_CHANNEL, "check"],
129
+ platform,
130
+ env,
131
+ 15_000,
132
+ ));
133
+ const latestVersion = /^Latest TraeCode CLI version:\s*(\S+)/mu.exec(commandOutput(latest))?.[1];
134
+ if (!latestVersion) {
135
+ return { id: "traex", name: "TraeX", state: "ready", installedVersion, latestCheckFailed: true };
136
+ }
137
+ if (compareSemanticVersions(installedVersion, latestVersion) < 0) {
138
+ return {
139
+ id: "traex",
140
+ name: "TraeX",
141
+ state: "outdated",
142
+ installedVersion,
143
+ latestVersion,
144
+ action: maintenanceAction("traex", "upgrade", platform, env),
145
+ };
146
+ }
147
+ return { id: "traex", name: "TraeX", state: "ready", installedVersion, latestVersion };
148
+ }
149
+
150
+ function missingInspection(
151
+ id: SupportedAgentId,
152
+ name: string,
153
+ platform: NodeJS.Platform,
154
+ env: NodeJS.ProcessEnv,
155
+ ): SupportedAgentInspection {
156
+ return {
157
+ id,
158
+ name,
159
+ state: "missing",
160
+ action: maintenanceAction(id, "install", platform, env),
161
+ };
162
+ }
163
+
164
+ function maintenanceAction(
165
+ id: SupportedAgentId,
166
+ kind: AgentMaintenanceKind,
167
+ platform: NodeJS.Platform,
168
+ env: NodeJS.ProcessEnv,
169
+ ): SupportedAgentInspection["action"] {
170
+ const command = maintenanceCommand(id, kind, platform, env);
171
+ return { kind, command: command.display };
172
+ }
173
+
174
+ function maintenanceCommand(
175
+ id: SupportedAgentId,
176
+ kind: AgentMaintenanceKind,
177
+ platform: NodeJS.Platform,
178
+ env: NodeJS.ProcessEnv,
179
+ ): AgentCommandSpec {
180
+ if (id === "codex") {
181
+ return kind === "install"
182
+ ? codexNpmInstallCommand(platform, env)
183
+ : commandSpec("codex", ["update"], platform, env, 5 * 60_000);
184
+ }
185
+ if (kind === "upgrade") {
186
+ return commandSpec("traex", ["update", "--channel", TRAEX_CHANNEL], platform, env, 5 * 60_000);
187
+ }
188
+
189
+ const installEnv = {
190
+ ...env,
191
+ TRAEX_INSTALL_ASSUME_YES: "1",
192
+ TRAEX_INSTALL_CHANNEL: TRAEX_CHANNEL,
193
+ };
194
+ if (platform === "win32") {
195
+ const scriptUrl = `${TRAEX_INSTALL_BASE_URL}/traex_install_windows.ps1`;
196
+ return {
197
+ command: "powershell.exe",
198
+ args: [
199
+ "-NoProfile",
200
+ "-ExecutionPolicy",
201
+ "Bypass",
202
+ "-Command",
203
+ `Invoke-RestMethod -Uri '${scriptUrl}' | Invoke-Expression`,
204
+ ],
205
+ display: `$env:TRAEX_INSTALL_CHANNEL='${TRAEX_CHANNEL}'; irm '${scriptUrl}' | iex`,
206
+ timeoutMs: 5 * 60_000,
207
+ env: installEnv,
208
+ };
209
+ }
210
+ const scriptUrl = `${TRAEX_INSTALL_BASE_URL}/traex_install.sh`;
211
+ return {
212
+ command: "bash",
213
+ args: ["-c", `curl -fsSL '${scriptUrl}' | bash`],
214
+ display: `curl -fsSL '${scriptUrl}' | TRAEX_INSTALL_CHANNEL=${TRAEX_CHANNEL} bash`,
215
+ timeoutMs: 5 * 60_000,
216
+ env: installEnv,
217
+ };
218
+ }
219
+
220
+ function codexNpmInstallCommand(
221
+ platform: NodeJS.Platform,
222
+ env: NodeJS.ProcessEnv,
223
+ ): AgentCommandSpec {
224
+ return commandSpec(
225
+ "npm",
226
+ ["install", "--global", `${CODEX_PACKAGE}@latest`],
227
+ platform,
228
+ env,
229
+ 5 * 60_000,
230
+ );
231
+ }
232
+
233
+ function commandSucceeded(result: AgentCommandResult): boolean {
234
+ return result.status === 0 && !result.error;
235
+ }
236
+
237
+ function capturedCommand(
238
+ command: string,
239
+ args: string[],
240
+ platform: NodeJS.Platform,
241
+ env: NodeJS.ProcessEnv,
242
+ timeoutMs: number,
243
+ ): AgentCommandSpec {
244
+ return commandSpec(command, args, platform, env, timeoutMs);
245
+ }
246
+
247
+ function commandSpec(
248
+ command: string,
249
+ args: string[],
250
+ _platform: NodeJS.Platform,
251
+ env: NodeJS.ProcessEnv,
252
+ timeoutMs: number,
253
+ ): AgentCommandSpec {
254
+ return {
255
+ command,
256
+ args,
257
+ display: [command, ...args].join(" "),
258
+ timeoutMs,
259
+ env,
260
+ };
261
+ }
262
+
263
+ function commandVersion(result: AgentCommandResult): string | undefined {
264
+ if (result.status !== 0) return undefined;
265
+ return versionInText(commandOutput(result));
266
+ }
267
+
268
+ function npmVersion(result: AgentCommandResult): string | undefined {
269
+ if (result.status !== 0) return undefined;
270
+ const output = result.stdout.trim();
271
+ try {
272
+ const parsed = JSON.parse(output) as unknown;
273
+ return typeof parsed === "string" ? versionInText(parsed) : undefined;
274
+ } catch {
275
+ return versionInText(output);
276
+ }
277
+ }
278
+
279
+ function commandOutput(result: AgentCommandResult): string {
280
+ return [result.stdout, result.stderr].filter(Boolean).join("\n");
281
+ }
282
+
283
+ function versionInText(value: string): string | undefined {
284
+ return /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/u.exec(value)?.[1];
285
+ }
286
+
287
+ export function compareSemanticVersions(left: string, right: string): number {
288
+ const leftVersion = parseSemanticVersion(left);
289
+ const rightVersion = parseSemanticVersion(right);
290
+ if (!leftVersion || !rightVersion) return 0;
291
+ for (let index = 0; index < 3; index += 1) {
292
+ const difference = leftVersion.core[index]! - rightVersion.core[index]!;
293
+ if (difference !== 0) return difference;
294
+ }
295
+ if (!leftVersion.prerelease && !rightVersion.prerelease) return 0;
296
+ if (!leftVersion.prerelease) return 1;
297
+ if (!rightVersion.prerelease) return -1;
298
+ const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
299
+ for (let index = 0; index < length; index += 1) {
300
+ const leftPart = leftVersion.prerelease[index];
301
+ const rightPart = rightVersion.prerelease[index];
302
+ if (leftPart === undefined) return -1;
303
+ if (rightPart === undefined) return 1;
304
+ if (leftPart === rightPart) continue;
305
+ const leftNumber = /^\d+$/u.test(leftPart) ? Number(leftPart) : undefined;
306
+ const rightNumber = /^\d+$/u.test(rightPart) ? Number(rightPart) : undefined;
307
+ if (leftNumber !== undefined && rightNumber !== undefined) return leftNumber - rightNumber;
308
+ if (leftNumber !== undefined) return -1;
309
+ if (rightNumber !== undefined) return 1;
310
+ return leftPart.localeCompare(rightPart);
311
+ }
312
+ return 0;
313
+ }
314
+
315
+ function parseSemanticVersion(value: string): {
316
+ core: [number, number, number];
317
+ prerelease?: string[];
318
+ } | undefined {
319
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
320
+ if (!match) return undefined;
321
+ return {
322
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
323
+ ...(match[4] ? { prerelease: match[4].split(".") } : {}),
324
+ };
325
+ }
326
+
327
+ export function runAgentCommand(command: AgentCommandSpec): Promise<AgentCommandResult> {
328
+ return new Promise((resolve) => {
329
+ const invocation = resolveInvocation(command);
330
+ const child = spawn(invocation.command, invocation.args, {
331
+ env: command.env ?? process.env,
332
+ windowsHide: true,
333
+ stdio: command.inheritStdio ? "inherit" : ["ignore", "pipe", "pipe"],
334
+ });
335
+ const stdout: Buffer[] = [];
336
+ const stderr: Buffer[] = [];
337
+ if (!command.inheritStdio) {
338
+ child.stdout?.on("data", (chunk: Buffer) => stdout.push(chunk));
339
+ child.stderr?.on("data", (chunk: Buffer) => stderr.push(chunk));
340
+ }
341
+ let error: string | undefined;
342
+ let timedOut = false;
343
+ const timer = setTimeout(() => {
344
+ timedOut = true;
345
+ child.kill();
346
+ }, command.timeoutMs);
347
+ child.once("error", (spawnError) => {
348
+ error = spawnError.message;
349
+ });
350
+ child.once("close", (status) => {
351
+ clearTimeout(timer);
352
+ resolve({
353
+ status,
354
+ stdout: Buffer.concat(stdout).toString("utf8"),
355
+ stderr: Buffer.concat(stderr).toString("utf8"),
356
+ ...(error || timedOut ? { error: timedOut ? `Timed out after ${command.timeoutMs}ms.` : error } : {}),
357
+ });
358
+ });
359
+ });
360
+ }
361
+
362
+ function resolveInvocation(command: AgentCommandSpec): { command: string; args: string[] } {
363
+ if (process.platform !== "win32" || /\.exe$/iu.test(command.command)) {
364
+ return { command: command.command, args: command.args };
365
+ }
366
+ const commandLine = [command.command, ...command.args].map(quoteCmdArgument).join(" ");
367
+ return {
368
+ command: process.env.ComSpec ?? "cmd.exe",
369
+ args: ["/d", "/s", "/c", commandLine],
370
+ };
371
+ }
372
+
373
+ function quoteCmdArgument(value: string): string {
374
+ return /^[A-Za-z0-9_@./:\\=+-]+$/u.test(value)
375
+ ? value
376
+ : `"${value.replaceAll('"', '""')}"`;
377
+ }
@@ -0,0 +1,56 @@
1
+ import type { ConfiguredAgentOption } from "./Initializer.js";
2
+ import type { SupportedAgentInspection } from "./AgentPrerequisites.js";
3
+
4
+ export interface SelectableDefaultAgent extends ConfiguredAgentOption {
5
+ installedVersion?: string;
6
+ }
7
+
8
+ export function selectableDefaultAgents(
9
+ configured: ConfiguredAgentOption[],
10
+ inspections: SupportedAgentInspection[],
11
+ ): SelectableDefaultAgent[] {
12
+ const supported = new Map(inspections.map((inspection) => [inspection.id, inspection]));
13
+ return configured.flatMap((agent) => {
14
+ const inspection = supported.get(agent.name as SupportedAgentInspection["id"]);
15
+ if (inspection && !inspection.installedVersion) return [];
16
+ return [{
17
+ ...agent,
18
+ ...(inspection?.installedVersion ? { installedVersion: inspection.installedVersion } : {}),
19
+ }];
20
+ });
21
+ }
22
+
23
+ export function parseMaintenanceSelection(input: string, choiceCount: number): number[] | undefined {
24
+ const value = input.trim();
25
+ if (!value) return [];
26
+ if (/^(?:a|all)$/iu.test(value)) return Array.from({ length: choiceCount }, (_, index) => index);
27
+ const tokens = value.split(/[,,\s]+/u).filter(Boolean);
28
+ const selected = new Set<number>();
29
+ for (const token of tokens) {
30
+ const number = Number(token);
31
+ if (!Number.isInteger(number) || number < 1 || number > choiceCount) return undefined;
32
+ selected.add(number - 1);
33
+ }
34
+ return [...selected].sort((left, right) => left - right);
35
+ }
36
+
37
+ export function resolveDefaultAgentChoice(
38
+ input: string,
39
+ choices: SelectableDefaultAgent[],
40
+ currentAgent?: string,
41
+ ): number | undefined {
42
+ const value = input.trim();
43
+ if (!value) {
44
+ const currentIndex = choices.findIndex((choice) => choice.name === currentAgent);
45
+ return currentIndex >= 0 ? currentIndex : undefined;
46
+ }
47
+ const number = Number(value);
48
+ if (Number.isInteger(number) && number >= 1 && number <= choices.length) return number - 1;
49
+ const exactIndex = choices.findIndex((choice) => choice.name === value);
50
+ if (exactIndex >= 0) return exactIndex;
51
+ const folded = value.toLocaleLowerCase("en-US");
52
+ const matches = choices
53
+ .map((choice, index) => ({ choice, index }))
54
+ .filter(({ choice }) => choice.name.toLocaleLowerCase("en-US") === folded);
55
+ return matches.length === 1 ? matches[0]?.index : undefined;
56
+ }
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { cliText } from "./i18n.js";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parse as parseDotEnv } from "dotenv";
8
+ import { isMap, isScalar, parseDocument, type YAMLMap } from "yaml";
8
9
  import {
9
10
  agentBotHome,
10
11
  defaultConfigPath,
@@ -12,7 +13,7 @@ import {
12
13
  resolveUserPath,
13
14
  } from "../config/paths.js";
14
15
 
15
- export type InitializationStatus = "created" | "existing" | "reset";
16
+ export type InitializationStatus = "created" | "existing" | "updated" | "reset";
16
17
 
17
18
  export interface InitializedPath {
18
19
  path: string;
@@ -38,6 +39,16 @@ export interface InitializationOptions {
38
39
  reset?: boolean;
39
40
  }
40
41
 
42
+ export interface ConfiguredAgentOption {
43
+ name: string;
44
+ title: string;
45
+ }
46
+
47
+ export interface ConfiguredAgentSelection {
48
+ agents: ConfiguredAgentOption[];
49
+ defaultAgent?: string;
50
+ }
51
+
41
52
  export type FeishuCredentialStatus = "configured" | "missing" | "incomplete";
42
53
 
43
54
  export interface FeishuCredentialState {
@@ -82,8 +93,8 @@ export function initializeAgentBot(options: InitializationOptions = {}): Initial
82
93
  ? resetProfileContents(homePath, configPath, envPath, configDirectory)
83
94
  : undefined;
84
95
 
85
- const config = copyIfMissing(configTemplatePath, configPath);
86
- const envFile = copyIfMissing(envTemplatePath, envPath);
96
+ const config = initializeConfig(configTemplatePath, configPath);
97
+ const envFile = initializeEnv(envTemplatePath, envPath);
87
98
  restrictFilePermissions(envFile.path);
88
99
  const data = ensureDirectory(path.join(configDirectory, "data"));
89
100
  const logs = ensureDirectory(path.join(configDirectory, "logs"));
@@ -98,6 +109,68 @@ export function initializeAgentBot(options: InitializationOptions = {}): Initial
98
109
  };
99
110
  }
100
111
 
112
+ export function readConfiguredAgentSelection(configPath: string): ConfiguredAgentSelection {
113
+ const document = parseConfigurationDocument(fs.readFileSync(configPath, "utf8"), configPath);
114
+ if (!isMap(document.contents)) {
115
+ throw new Error(cliText(
116
+ `Configuration file must contain a YAML mapping: ${configPath}`,
117
+ `配置文件必须包含 YAML 映射:${configPath}`,
118
+ ));
119
+ }
120
+ const configuredAgents = document.contents.get("agents", true);
121
+ if (!isMap(configuredAgents)) {
122
+ throw new Error(cliText(
123
+ `Configuration file must contain an agents mapping: ${configPath}`,
124
+ `配置文件必须包含 agents 映射:${configPath}`,
125
+ ));
126
+ }
127
+ const agents = configuredAgents.items.flatMap((pair) => {
128
+ const name = yamlMappingKey(pair.key);
129
+ if (!name) return [];
130
+ const titleNode = isMap(pair.value) ? pair.value.get("title", true) : undefined;
131
+ const title = isScalar(titleNode) && typeof titleNode.value === "string"
132
+ ? titleNode.value
133
+ : name;
134
+ return [{ name, title }];
135
+ });
136
+ const defaultAgent = document.getIn(["defaults", "agent"]);
137
+ return {
138
+ agents,
139
+ ...(typeof defaultAgent === "string" && defaultAgent.trim() ? { defaultAgent } : {}),
140
+ };
141
+ }
142
+
143
+ export function writeDefaultAgent(configPath: string, agentName: string): boolean {
144
+ const original = fs.readFileSync(configPath, "utf8");
145
+ const document = parseConfigurationDocument(original, configPath);
146
+ if (!isMap(document.contents)) {
147
+ throw new Error(cliText(
148
+ `Configuration file must contain a YAML mapping: ${configPath}`,
149
+ `配置文件必须包含 YAML 映射:${configPath}`,
150
+ ));
151
+ }
152
+ const configuredAgents = document.contents.get("agents", true);
153
+ if (!isMap(configuredAgents) || !configuredAgents.has(agentName)) {
154
+ throw new Error(cliText(
155
+ `Cannot select an Agent that is not configured: ${agentName}`,
156
+ `不能选择尚未配置的 Agent:${agentName}`,
157
+ ));
158
+ }
159
+ const defaults = document.contents.get("defaults", true);
160
+ if (defaults !== undefined && !isMap(defaults)) {
161
+ throw new Error(cliText(
162
+ `Configuration defaults must contain a YAML mapping: ${configPath}`,
163
+ `配置文件的 defaults 必须是 YAML 映射:${configPath}`,
164
+ ));
165
+ }
166
+ const currentAgent = document.getIn(["defaults", "agent"]);
167
+ if (currentAgent === agentName) return false;
168
+ if (isMap(defaults)) defaults.set("agent", agentName);
169
+ else document.setIn(["defaults"], { agent: agentName });
170
+ writeFileAtomically(configPath, document.toString());
171
+ return true;
172
+ }
173
+
101
174
  export function readFeishuCredentials(envPath: string, env: NodeJS.ProcessEnv = process.env): FeishuCredentialState {
102
175
  const fileValues = fs.existsSync(envPath) ? parseDotEnv(fs.readFileSync(envPath, "utf8")) : {};
103
176
  const appId = firstNonBlank(env.FEISHU_APP_ID, fileValues.FEISHU_APP_ID);
@@ -244,6 +317,134 @@ function copyIfMissing(sourcePath: string, targetPath: string): InitializedPath
244
317
  }
245
318
  }
246
319
 
320
+ function initializeConfig(templatePath: string, configPath: string): InitializedPath {
321
+ if (!fs.existsSync(configPath)) {
322
+ const copied = copyIfMissing(templatePath, configPath);
323
+ if (copied.status === "created") return copied;
324
+ }
325
+
326
+ const original = fs.readFileSync(configPath, "utf8");
327
+ const template = fs.readFileSync(templatePath, "utf8");
328
+ const configDocument = parseConfigurationDocument(original, configPath);
329
+ const templateDocument = parseConfigurationDocument(template, templatePath);
330
+ if (!isMap(configDocument.contents) || !isMap(templateDocument.contents)) {
331
+ throw new Error(cliText(
332
+ `Configuration files must contain a YAML mapping: ${configPath}`,
333
+ `配置文件必须包含 YAML 映射:${configPath}`,
334
+ ));
335
+ }
336
+
337
+ const updated = mergeMissingConfiguration(
338
+ configDocument.contents,
339
+ templateDocument.contents,
340
+ [],
341
+ );
342
+ if (!updated) return { path: configPath, status: "existing" };
343
+ writeFileAtomically(configPath, configDocument.toString());
344
+ return { path: configPath, status: "updated" };
345
+ }
346
+
347
+ function initializeEnv(templatePath: string, envPath: string): InitializedPath {
348
+ if (!fs.existsSync(envPath)) {
349
+ const copied = copyIfMissing(templatePath, envPath);
350
+ if (copied.status === "created") return copied;
351
+ }
352
+
353
+ const original = fs.readFileSync(envPath, "utf8");
354
+ const template = fs.readFileSync(templatePath, "utf8");
355
+ const existingKeys = dotEnvKeys(original);
356
+ const missingLines = template
357
+ .split(/\r?\n/)
358
+ .flatMap((line) => {
359
+ const key = dotEnvAssignmentKey(line);
360
+ if (!key || existingKeys.has(key)) return [];
361
+ existingKeys.add(key);
362
+ return [line];
363
+ });
364
+ if (missingLines.length === 0) return { path: envPath, status: "existing" };
365
+
366
+ const newline = original.includes("\r\n") ? "\r\n" : "\n";
367
+ const separator = original.length === 0 || /\r?\n$/u.test(original) ? "" : newline;
368
+ writeFileAtomically(envPath, `${original}${separator}${missingLines.join(newline)}${newline}`);
369
+ return { path: envPath, status: "updated" };
370
+ }
371
+
372
+ function parseConfigurationDocument(contents: string, filePath: string) {
373
+ const document = parseDocument(contents);
374
+ if (document.errors.length === 0) return document;
375
+ throw new Error(cliText(
376
+ `Could not upgrade configuration file ${filePath}: ${document.errors[0]?.message ?? "invalid YAML"}`,
377
+ `无法升级配置文件 ${filePath}:${document.errors[0]?.message ?? "YAML 无效"}`,
378
+ ));
379
+ }
380
+
381
+ function mergeMissingConfiguration(
382
+ config: YAMLMap,
383
+ template: YAMLMap,
384
+ parentPath: string[],
385
+ ): boolean {
386
+ let updated = false;
387
+ for (const templatePair of template.items) {
388
+ const key = yamlMappingKey(templatePair.key);
389
+ if (!key) continue;
390
+ if (!config.has(key)) {
391
+ if (shouldPreserveMissingConfiguration(parentPath, key)) continue;
392
+ const clonedPair = templatePair.clone();
393
+ if (parentPath.length === 0 && key === "defaults" && isMap(clonedPair.value)) {
394
+ clonedPair.value.delete("agent");
395
+ if (clonedPair.value.items.length === 0) continue;
396
+ }
397
+ config.add(clonedPair);
398
+ updated = true;
399
+ continue;
400
+ }
401
+
402
+ const configValue = config.get(key, true);
403
+ if (isMap(configValue) && isMap(templatePair.value)) {
404
+ updated = mergeMissingConfiguration(configValue, templatePair.value, [...parentPath, key]) || updated;
405
+ }
406
+ }
407
+ return updated;
408
+ }
409
+
410
+ function shouldPreserveMissingConfiguration(parentPath: string[], key: string): boolean {
411
+ if (parentPath.length === 1 && parentPath[0] === "agents") return true;
412
+ return parentPath.length === 1 && parentPath[0] === "defaults" && key === "agent";
413
+ }
414
+
415
+ function yamlMappingKey(value: unknown): string | undefined {
416
+ if (typeof value === "string") return value;
417
+ return isScalar(value) && typeof value.value === "string" ? value.value : undefined;
418
+ }
419
+
420
+ function dotEnvKeys(contents: string): Set<string> {
421
+ return new Set(contents.split(/\r?\n/).flatMap((line) => {
422
+ const key = dotEnvAssignmentKey(line);
423
+ return key ? [key] : [];
424
+ }));
425
+ }
426
+
427
+ function dotEnvAssignmentKey(line: string): string | undefined {
428
+ return /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/u.exec(line)?.[1];
429
+ }
430
+
431
+ function writeFileAtomically(filePath: string, contents: string): void {
432
+ const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
433
+ let temporaryFile: number | undefined;
434
+ try {
435
+ temporaryFile = fs.openSync(temporaryPath, "wx", fs.statSync(filePath).mode);
436
+ fs.writeFileSync(temporaryFile, contents, "utf8");
437
+ fs.fsyncSync(temporaryFile);
438
+ fs.closeSync(temporaryFile);
439
+ temporaryFile = undefined;
440
+ fs.renameSync(temporaryPath, filePath);
441
+ syncDirectory(path.dirname(filePath));
442
+ } finally {
443
+ if (temporaryFile !== undefined) fs.closeSync(temporaryFile);
444
+ fs.rmSync(temporaryPath, { force: true });
445
+ }
446
+ }
447
+
247
448
  function resetProfileContents(
248
449
  homePath: string,
249
450
  configPath: string,