@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.20

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 (63) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +13 -0
  3. package/README.zh.md +9 -0
  4. package/dist/cli/AgentPrerequisites.js +11 -47
  5. package/dist/cli/AgentPrerequisites.js.map +1 -1
  6. package/dist/cli/DefaultAgentSelection.js +1 -1
  7. package/dist/cli/DefaultAgentSelection.js.map +1 -1
  8. package/dist/cli.js +15 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/codex/CodexLocalActivityDetector.js +32 -10
  11. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  12. package/dist/codex/CodexProcessManager.js +53 -7
  13. package/dist/codex/CodexProcessManager.js.map +1 -1
  14. package/dist/codex/CodexRuntime.js +225 -164
  15. package/dist/codex/CodexRuntime.js.map +1 -1
  16. package/dist/codex/CodexVersion.js +18 -0
  17. package/dist/codex/CodexVersion.js.map +1 -0
  18. package/dist/codex/ProviderSwitch.js +32 -0
  19. package/dist/codex/ProviderSwitch.js.map +1 -0
  20. package/dist/feishu/CardRenderer.js +36 -11
  21. package/dist/feishu/CardRenderer.js.map +1 -1
  22. package/dist/feishu/FeishuConnector.js +17 -5
  23. package/dist/feishu/FeishuConnector.js.map +1 -1
  24. package/dist/feishu/InboundText.js +11 -0
  25. package/dist/feishu/InboundText.js.map +1 -1
  26. package/dist/feishu/MergedForwardMessage.js +17 -5
  27. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/local-files/LocalFileViewerServer.js +14 -4
  31. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  32. package/dist/local-files/MarkdownFileLinks.js +65 -0
  33. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  34. package/dist/proxy/ProxySessionController.js +399 -257
  35. package/dist/proxy/ProxySessionController.js.map +1 -1
  36. package/dist/state/StateStore.js +58 -65
  37. package/dist/state/StateStore.js.map +1 -1
  38. package/dist/utils/semanticVersion.js +39 -0
  39. package/dist/utils/semanticVersion.js.map +1 -0
  40. package/docs/technical-reference.md +5 -3
  41. package/docs/technical-reference.zh.md +5 -3
  42. package/npm-shrinkwrap.json +2 -2
  43. package/package.json +1 -1
  44. package/skills/agent-bot/SKILL.md +7 -1
  45. package/src/cli/AgentPrerequisites.ts +14 -40
  46. package/src/cli/DefaultAgentSelection.ts +1 -1
  47. package/src/cli.ts +13 -0
  48. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  49. package/src/codex/CodexProcessManager.ts +44 -7
  50. package/src/codex/CodexRuntime.ts +244 -195
  51. package/src/codex/CodexVersion.ts +18 -0
  52. package/src/codex/ProviderSwitch.ts +36 -0
  53. package/src/feishu/CardRenderer.ts +37 -27
  54. package/src/feishu/FeishuConnector.ts +15 -5
  55. package/src/feishu/InboundText.ts +11 -0
  56. package/src/feishu/MergedForwardMessage.ts +15 -5
  57. package/src/index.ts +1 -1
  58. package/src/local-files/LocalFileViewerServer.ts +14 -4
  59. package/src/local-files/MarkdownFileLinks.ts +64 -0
  60. package/src/proxy/ProxySessionController.ts +369 -238
  61. package/src/runtime/types.ts +15 -2
  62. package/src/state/StateStore.ts +77 -90
  63. package/src/utils/semanticVersion.ts +28 -0
@@ -1,4 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { codexVersionIssue, MINIMUM_CODEX_VERSION } from "../codex/CodexVersion.js";
3
+ import { compareSemanticVersions } from "../utils/semanticVersion.js";
4
+ export { compareSemanticVersions } from "../utils/semanticVersion.js";
2
5
 
3
6
  export type SupportedAgentId = "codex" | "traex";
4
7
  export type SupportedAgentState = "missing" | "outdated" | "ready";
@@ -29,6 +32,8 @@ export interface SupportedAgentInspection {
29
32
  installedVersion?: string;
30
33
  latestVersion?: string;
31
34
  latestCheckFailed?: boolean;
35
+ minimumVersion?: string;
36
+ compatibilityIssue?: string;
32
37
  action?: {
33
38
  kind: AgentMaintenanceKind;
34
39
  command: string;
@@ -100,6 +105,15 @@ async function inspectCodex(
100
105
  const installedVersion = commandVersion(installed);
101
106
  if (!installedVersion) return missingInspection("codex", "Codex", platform, env);
102
107
 
108
+ const compatibilityIssue = codexVersionIssue(installedVersion);
109
+ if (compatibilityIssue) {
110
+ return {
111
+ id: "codex", name: "Codex", state: "outdated", installedVersion,
112
+ minimumVersion: MINIMUM_CODEX_VERSION, compatibilityIssue,
113
+ action: maintenanceAction("codex", "upgrade", platform, env),
114
+ };
115
+ }
116
+
103
117
  const latest = await run(capturedCommand(
104
118
  "npm",
105
119
  ["view", CODEX_PACKAGE, "version", "--json"],
@@ -294,46 +308,6 @@ function versionInText(value: string): string | undefined {
294
308
  return /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b/u.exec(value)?.[1];
295
309
  }
296
310
 
297
- export function compareSemanticVersions(left: string, right: string): number {
298
- const leftVersion = parseSemanticVersion(left);
299
- const rightVersion = parseSemanticVersion(right);
300
- if (!leftVersion || !rightVersion) return 0;
301
- for (let index = 0; index < 3; index += 1) {
302
- const difference = leftVersion.core[index]! - rightVersion.core[index]!;
303
- if (difference !== 0) return difference;
304
- }
305
- if (!leftVersion.prerelease && !rightVersion.prerelease) return 0;
306
- if (!leftVersion.prerelease) return 1;
307
- if (!rightVersion.prerelease) return -1;
308
- const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
309
- for (let index = 0; index < length; index += 1) {
310
- const leftPart = leftVersion.prerelease[index];
311
- const rightPart = rightVersion.prerelease[index];
312
- if (leftPart === undefined) return -1;
313
- if (rightPart === undefined) return 1;
314
- if (leftPart === rightPart) continue;
315
- const leftNumber = /^\d+$/u.test(leftPart) ? Number(leftPart) : undefined;
316
- const rightNumber = /^\d+$/u.test(rightPart) ? Number(rightPart) : undefined;
317
- if (leftNumber !== undefined && rightNumber !== undefined) return leftNumber - rightNumber;
318
- if (leftNumber !== undefined) return -1;
319
- if (rightNumber !== undefined) return 1;
320
- return leftPart.localeCompare(rightPart);
321
- }
322
- return 0;
323
- }
324
-
325
- function parseSemanticVersion(value: string): {
326
- core: [number, number, number];
327
- prerelease?: string[];
328
- } | undefined {
329
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
330
- if (!match) return undefined;
331
- return {
332
- core: [Number(match[1]), Number(match[2]), Number(match[3])],
333
- ...(match[4] ? { prerelease: match[4].split(".") } : {}),
334
- };
335
- }
336
-
337
311
  export function runAgentCommand(command: AgentCommandSpec): Promise<AgentCommandResult> {
338
312
  return new Promise((resolve) => {
339
313
  const invocation = resolveInvocation(command);
@@ -12,7 +12,7 @@ export function selectableDefaultAgents(
12
12
  const supported = new Map(inspections.map((inspection) => [inspection.id, inspection]));
13
13
  return configured.flatMap((agent) => {
14
14
  const inspection = supported.get(agent.name as SupportedAgentInspection["id"]);
15
- if (!inspection?.installedVersion) return [];
15
+ if (!inspection?.installedVersion || inspection.compatibilityIssue) return [];
16
16
  return [{
17
17
  ...agent,
18
18
  installedVersion: inspection.installedVersion,
package/src/cli.ts CHANGED
@@ -338,6 +338,9 @@ async function initCommand(
338
338
  );
339
339
  const configuredAgents = readConfiguredAgentSelection(paths.config.path).agents.map((agent) => agent.name);
340
340
  const configuredAgentNames = new Set(configuredAgents);
341
+ for (const agent of inspectedAgents) {
342
+ if (configuredAgentNames.has(agent.id) && agent.compatibilityIssue) throw new Error(agent.compatibilityIssue);
343
+ }
341
344
  const agents = inspectedAgents.map((agent) => ({
342
345
  ...agent,
343
346
  configured: configuredAgentNames.has(agent.id),
@@ -554,6 +557,8 @@ async function configureAgentsAndDefault(
554
557
 
555
558
  const choices = selectableDefaultAgents(configured.agents, inspections);
556
559
  if (choices.length === 0) {
560
+ const issue = inspections.find((inspection) => inspection.compatibilityIssue)?.compatibilityIssue;
561
+ if (issue) throw new Error(issue);
557
562
  throw new Error(cliText(
558
563
  "No installed supported Agent was detected. Install Codex or TraeX, then run agentbot init again.",
559
564
  "没有检测到已安装且受支持的 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
@@ -679,6 +684,10 @@ function printInteractiveAgentInspection(
679
684
  inspection: SupportedAgentInspection,
680
685
  ui: InitUi,
681
686
  ): void {
687
+ if (inspection.compatibilityIssue) {
688
+ ui.warn(inspection.compatibilityIssue);
689
+ return;
690
+ }
682
691
  if (inspection.state === "missing") {
683
692
  ui.info(cliText(
684
693
  `${inspection.name}: not installed`,
@@ -710,6 +719,10 @@ function printAgentInspection(
710
719
  inspection: SupportedAgentInspection,
711
720
  output: NodeJS.WriteStream,
712
721
  ): void {
722
+ if (inspection.compatibilityIssue) {
723
+ output.write(` ${inspection.compatibilityIssue}\n`);
724
+ return;
725
+ }
713
726
  if (inspection.state === "missing") {
714
727
  output.write(cliText(
715
728
  ` ${inspection.name}: not installed\n`,
@@ -9,6 +9,16 @@ const TASK_COMPLETE = Buffer.from('"type":"task_complete"');
9
9
  const TURN_ABORTED = Buffer.from('"type":"turn_aborted"');
10
10
  const PATTERN_OVERLAP = Math.max(TASK_STARTED.length, TASK_COMPLETE.length, TURN_ABORTED.length) - 1;
11
11
 
12
+ interface RolloutState {
13
+ size: number;
14
+ mtimeMs: number;
15
+ ctimeMs: number;
16
+ ino: number;
17
+ dev: number;
18
+ tail: Buffer;
19
+ task: { active: boolean; turnId?: string };
20
+ }
21
+
12
22
  interface ThreadPathRow {
13
23
  id: string;
14
24
  rollout_path: string;
@@ -35,6 +45,7 @@ export interface CodexLocalThreadSettings {
35
45
  * even while Codex Desktop is actively appending events to its rollout.
36
46
  */
37
47
  export class CodexLocalActivityDetector {
48
+ private readonly rollouts = new Map<string, RolloutState>();
38
49
  constructor(private readonly codexHome: string) {}
39
50
 
40
51
  async activeThreadIds(threadIds: string[]): Promise<Set<string>> {
@@ -64,7 +75,7 @@ export class CodexLocalActivityDetector {
64
75
 
65
76
  const active = new Map<string, string | undefined>();
66
77
  for (const row of rows) {
67
- const task = await latestTask(row.rollout_path);
78
+ const task = await latestTask(row.rollout_path, this.rollouts);
68
79
  if (task.active) active.set(row.id, task.turnId);
69
80
  }
70
81
  return active;
@@ -104,16 +115,31 @@ export class CodexLocalActivityDetector {
104
115
  }
105
116
  }
106
117
 
107
- async function latestTask(rolloutPath: string): Promise<{ active: boolean; turnId?: string }> {
118
+ async function latestTask(rolloutPath: string, cache: Map<string, RolloutState>): Promise<{ active: boolean; turnId?: string }> {
108
119
  let file;
109
120
  try {
110
121
  file = await open(rolloutPath, "r");
111
- const size = (await file.stat()).size;
122
+ const { size, mtimeMs, ctimeMs, ino, dev } = await file.stat();
123
+ const previous = cache.get(rolloutPath);
124
+ const sameFile = previous?.ino === ino && previous.dev === dev;
125
+ if (sameFile && previous.size === size && previous.mtimeMs === mtimeMs && previous.ctimeMs === ctimeMs) {
126
+ return previous.task;
127
+ }
128
+ let stopAt = 0;
129
+ let task: RolloutState["task"] = { active: false };
130
+ if (sameFile && size > previous.size) {
131
+ const boundary = Buffer.alloc(previous.tail.length);
132
+ await file.read(boundary, 0, boundary.length, previous.size - boundary.length);
133
+ if (boundary.equals(previous.tail)) {
134
+ stopAt = Math.max(0, previous.size - PATTERN_OVERLAP);
135
+ task = previous.task;
136
+ }
137
+ }
112
138
  let position = size;
113
139
  let laterPrefix = Buffer.alloc(0);
114
140
 
115
- while (position > 0) {
116
- const length = Math.min(READ_CHUNK_SIZE, position);
141
+ while (position > stopAt) {
142
+ const length = Math.min(READ_CHUNK_SIZE, position - stopAt);
117
143
  position -= length;
118
144
  const chunk = Buffer.allocUnsafe(length);
119
145
  await file.read(chunk, 0, length, position);
@@ -123,14 +149,20 @@ async function latestTask(rolloutPath: string): Promise<{ active: boolean; turnI
123
149
  const abortedAt = searchable.lastIndexOf(TURN_ABORTED);
124
150
  const terminalAt = Math.max(completedAt, abortedAt);
125
151
  if (startedAt >= 0 || terminalAt >= 0) {
126
- if (startedAt <= terminalAt) return { active: false };
127
- const absoluteOffset = position + startedAt;
128
- return { active: true, turnId: await readTurnId(file, absoluteOffset, size) };
152
+ task = startedAt <= terminalAt ? { active: false }
153
+ : { active: true, turnId: await readTurnId(file, position + startedAt, size) };
154
+ break;
129
155
  }
130
156
  laterPrefix = chunk.subarray(0, Math.min(PATTERN_OVERLAP, chunk.length));
131
157
  }
132
- return { active: false };
158
+ const tail = Buffer.alloc(Math.min(1_024, size));
159
+ await file.read(tail, 0, tail.length, size - tail.length);
160
+ cache.delete(rolloutPath);
161
+ cache.set(rolloutPath, { size, mtimeMs, ctimeMs, ino, dev, tail, task });
162
+ if (cache.size > 512) cache.delete(cache.keys().next().value!);
163
+ return task;
133
164
  } catch {
165
+ cache.delete(rolloutPath);
134
166
  return { active: false };
135
167
  } finally {
136
168
  await file?.close();
@@ -11,12 +11,15 @@ import {
11
11
  } from "../runtime/agentEnvironment.js";
12
12
  import type { AgentProcessInfo } from "../runtime/types.js";
13
13
  import { spawnStdioCommand } from "../utils/spawnCommand.js";
14
+ import { assertSupportedCodexVersion } from "./CodexVersion.js";
14
15
 
15
16
  export class CodexProcessManager implements AppServerClientProvider {
16
17
  private static readonly RELEASE_TIMEOUT_MS = 5_000;
17
18
  private client?: AppServerConnection;
18
19
  private child?: ChildProcessWithoutNullStreams;
19
20
  private version?: string;
21
+ private agentFamily?: "codex" | "traex";
22
+ private initializing?: Promise<AppServerClient>;
20
23
  private readonly disconnectListeners = new Set<(error: Error) => void>();
21
24
 
22
25
  constructor(
@@ -28,7 +31,18 @@ export class CodexProcessManager implements AppServerClientProvider {
28
31
  ) {}
29
32
 
30
33
  async getClient(): Promise<AppServerClient> {
34
+ if (this.initializing) return this.initializing;
31
35
  if (this.client) return this.client;
36
+ const initialization = this.startClient();
37
+ this.initializing = initialization;
38
+ try {
39
+ return await initialization;
40
+ } finally {
41
+ if (this.initializing === initialization) this.initializing = undefined;
42
+ }
43
+ }
44
+
45
+ private async startClient(): Promise<AppServerClient> {
32
46
  this.version = undefined;
33
47
  const environmentContext = this.environmentContext();
34
48
  const child = spawnStdioCommand(
@@ -52,13 +66,25 @@ export class CodexProcessManager implements AppServerClientProvider {
52
66
  const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
53
67
  for (const listener of this.disconnectListeners) listener(error);
54
68
  });
55
- const initializeResult = await client.request("initialize", {
56
- clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
57
- capabilities: { experimentalApi: true },
58
- });
59
- this.version = initializedAgentVersion(initializeResult);
60
- client.notify("initialized", {});
61
- return client;
69
+ try {
70
+ const initializeResult = await client.request("initialize", {
71
+ clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
72
+ capabilities: { experimentalApi: true },
73
+ });
74
+ this.version = initializedAgentVersion(initializeResult);
75
+ const userAgent = initializeResult && typeof initializeResult === "object"
76
+ ? (initializeResult as Record<string, unknown>).userAgent : undefined;
77
+ if (!this.getAgentFamily() && typeof userAgent === "string") {
78
+ if (/^codex(?:-cli)?[ /]/iu.test(userAgent)) this.agentFamily = "codex";
79
+ else if (/^trae(?:cli|x)?[ /]/iu.test(userAgent)) this.agentFamily = "traex";
80
+ }
81
+ if (this.getAgentFamily() === "codex") assertSupportedCodexVersion(this.version);
82
+ client.notify("initialized", {});
83
+ return client;
84
+ } catch (error) {
85
+ if (this.client === client) this.close();
86
+ throw error;
87
+ }
62
88
  }
63
89
 
64
90
  getProcessInfo(): AgentProcessInfo {
@@ -70,6 +96,17 @@ export class CodexProcessManager implements AppServerClientProvider {
70
96
  };
71
97
  }
72
98
 
99
+ getAgentFamily(): "codex" | "traex" | undefined {
100
+ if (this.agentFamily) return this.agentFamily;
101
+ const candidates = [this.command, this.args[0], this.environmentContext().agentName];
102
+ for (const value of candidates) {
103
+ const executable = value?.replaceAll("\\", "/").split("/").at(-1) ?? "";
104
+ if (/^codex(?:\.(?:exe|cmd|bat|m?js))?$/iu.test(executable)) return "codex";
105
+ if (/^(?:traex|traecli)(?:\.(?:exe|cmd|bat|m?js))?$/iu.test(executable)) return "traex";
106
+ }
107
+ return undefined;
108
+ }
109
+
73
110
  getCodexHome(): string {
74
111
  return this.env.CODEX_HOME ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
75
112
  }