@keyou007/agent-bot 0.1.13-alpha.1 → 0.1.13-alpha.3

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 +106 -103
  3. package/README.zh.md +76 -73
  4. package/dist/acp/AcpProcessManager.js +8 -3
  5. package/dist/acp/AcpProcessManager.js.map +1 -1
  6. package/dist/acp/AcpSessionManager.js +15 -0
  7. package/dist/acp/AcpSessionManager.js.map +1 -1
  8. package/dist/cli/InitializationWelcome.js +116 -0
  9. package/dist/cli/InitializationWelcome.js.map +1 -0
  10. package/dist/cli/help.js +4 -2
  11. package/dist/cli/help.js.map +1 -1
  12. package/dist/cli/serverStatus.js +27 -0
  13. package/dist/cli/serverStatus.js.map +1 -1
  14. package/dist/cli/taskListOutput.js +72 -0
  15. package/dist/cli/taskListOutput.js.map +1 -0
  16. package/dist/cli.js +103 -56
  17. package/dist/cli.js.map +1 -1
  18. package/dist/codex/CodexProcessManager.js +34 -4
  19. package/dist/codex/CodexProcessManager.js.map +1 -1
  20. package/dist/codex/CodexRuntime.js +3 -0
  21. package/dist/codex/CodexRuntime.js.map +1 -1
  22. package/dist/feishu/CardRenderer.js +164 -2
  23. package/dist/feishu/CardRenderer.js.map +1 -1
  24. package/dist/feishu/FeishuConnector.js +16 -5
  25. package/dist/feishu/FeishuConnector.js.map +1 -1
  26. package/dist/index.js +23 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/proxy/ProxySessionController.js +157 -59
  29. package/dist/proxy/ProxySessionController.js.map +1 -1
  30. package/dist/runtime/AcpRuntimeAdapter.js +3 -0
  31. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  32. package/dist/runtime/agentEnvironment.js +24 -4
  33. package/dist/runtime/agentEnvironment.js.map +1 -1
  34. package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
  35. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
  36. package/dist/state/StateStore.js +12 -9
  37. package/dist/state/StateStore.js.map +1 -1
  38. package/dist/supervision/SafeRestartNotifier.js +1 -9
  39. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  40. package/docs/technical-reference.md +12 -4
  41. package/docs/technical-reference.zh.md +12 -4
  42. package/npm-shrinkwrap.json +2 -2
  43. package/package.json +1 -1
  44. package/skills/agent-bot/SKILL.md +53 -22
  45. package/src/acp/AcpProcessManager.ts +16 -3
  46. package/src/acp/AcpSessionManager.ts +14 -0
  47. package/src/cli/InitializationWelcome.ts +160 -0
  48. package/src/cli/help.ts +4 -2
  49. package/src/cli/serverStatus.ts +28 -0
  50. package/src/cli/taskListOutput.ts +91 -0
  51. package/src/cli.ts +140 -55
  52. package/src/codex/CodexProcessManager.ts +38 -3
  53. package/src/codex/CodexRuntime.ts +6 -0
  54. package/src/feishu/CardRenderer.ts +204 -1
  55. package/src/feishu/FeishuConnector.ts +13 -4
  56. package/src/index.ts +35 -3
  57. package/src/proxy/ProxySessionController.ts +186 -59
  58. package/src/runtime/AcpRuntimeAdapter.ts +5 -0
  59. package/src/runtime/agentEnvironment.ts +41 -3
  60. package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
  61. package/src/runtime/types.ts +5 -0
  62. package/src/state/StateStore.ts +12 -9
  63. package/src/supervision/SafeRestartNotifier.ts +1 -11
@@ -31,6 +31,19 @@ running_in_agent_bot = os.environ.get("AGENT_BOT") == "1"
31
31
 
32
32
  Treat this environment marker as authoritative. Do not infer Agent Bot execution merely because the CLI is installed, `~/.agent-bot` exists, or an Agent Bot process is running elsewhere. Configured agent environment variables cannot override the marker. Child processes normally inherit it, so the marker identifies the Agent Bot-started process tree unless a child explicitly replaces its environment.
33
33
 
34
+ Agent Bot preserves ordinary parent-process variables but removes inherited and Agent-configured `FEISHU_*` and `AGENT_BOT_*` variables before starting an Agent. It then exposes only this controlled, non-secret context when values are available:
35
+
36
+ ```text
37
+ AGENT_BOT_HOME
38
+ AGENT_BOT_CONFIG
39
+ AGENT_BOT_AGENT_NAME
40
+ AGENT_BOT_LARK_APP_ID
41
+ AGENT_BOT_LARK_BOT_OPEN_ID
42
+ AGENT_BOT_LARK_USER_OPEN_ID
43
+ ```
44
+
45
+ `AGENT_BOT_HOME` is the active Profile root. Never expect `FEISHU_APP_SECRET`, Supervisor state, restart reasons, or restart notification routes in the Agent environment.
46
+
34
47
  ## Behave safely inside Agent Bot
35
48
 
36
49
  When `AGENT_BOT=1`:
@@ -88,7 +101,7 @@ agentbot server status
88
101
  agentbot task list
89
102
  ```
90
103
 
91
- Agent Bot CLI interface text follows the system locale: locales beginning with `zh` use Chinese, while English and unsupported locales use English. JSON output is not localized. `agentbot server status` also reports the running profile's Lark App ID; with `--json`, read it from `feishuAppId`.
104
+ Agent Bot CLI interface text follows the system locale: locales beginning with `zh` use Chinese, while English and unsupported locales use English. JSON output is not localized. `agentbot server status` reports the running profile's Lark App ID and each configured Agent process's PID and initialized version. With `--json`, read them from `feishuAppId` and `agents`; an Agent that has not started has `null` process fields.
92
105
 
93
106
  Add `--json` to status and list commands when machine-readable output helps. If `agentbot` is unavailable, report that the CLI is not installed or linked instead of guessing process state.
94
107
 
@@ -132,6 +145,7 @@ List tasks before resolving a numeric reference because task numbers follow the
132
145
 
133
146
  ```powershell
134
147
  agentbot task list
148
+ agentbot task current [--json]
135
149
  agentbot task chat <number-or-task-id> [--json]
136
150
  agentbot task status <number-or-task-id>
137
151
  agentbot task stop <number-or-task-id>
@@ -143,52 +157,69 @@ agentbot task forkgroup <number-or-task-id> [title] [--json]
143
157
 
144
158
  Task IDs may be supplied in full or as an unambiguous prefix. Use `task stop` to ask the running worker to send the App Server Interrupt signal; leave child-process lifecycle management to the selected Agent.
145
159
 
160
+ Read all task identity fields from `task list`: every entry shows its Agent and `AgentBot task ID`; App Server tasks also show `App Server thread ID`, while ACP tasks show `ACP session ID`. The status, title, conversation context, and update time remain visible. The CLI reads `CODEX_THREAD_ID` for Codex and `TRAECLI_THREAD_ID` for TraeX; when the available native Thread IDs identify one task, it marks that entry as `Current`. Use `task current` to print that task's details directly. Use any displayed full ID or an unambiguous prefix as a task reference. With `--json`, read the local and native values from `localSessionId`, `remoteSessionId`, and `acpSessionId`.
161
+
146
162
  Use `task chat <number-or-task-id>` to read the Feishu chat ID associated with a task from local routing state. Plain output is only the chat ID for scripting. Add `--json` to include the stable task ID, complete context key, and the thread ID for a topic-bound task. This read-only command does not require the Agent Bot server to be running.
147
163
 
148
164
  Use `task prompt` to send input to a specific task without changing any chat's current task. The bot first posts the Prompt text to the task's existing Feishu chat, thread, or private-chat route, then submits it to the task. If posting fails, the task is not started or steered. The thinking card and final response continue to the same route; the command line only reports whether the Prompt was accepted.
149
165
 
150
- ### Create a task group from the CLI
166
+ ### Create or fork a group from the CLI
167
+
168
+ Choose by whether the new task needs the source conversation:
151
169
 
152
- Use `task newgroup` to create a Lark group and bind a new task from outside Feishu. List tasks first and pass the intended source task explicitly; `<number-or-task-id>` defines the source context, so never substitute the shell's current working directory for the task's project directory.
170
+ - Use **New Group** for a fresh task in a separate private Lark group. Inherit the project and execution settings, but do not carry over prior turns. Use it for a separate assignment in the same codebase or for a different project or Agent.
171
+ - Use **Fork Group** to continue or parallelize work with the source task's conversation through a completed turn. Preserve that context in a separate private Lark group.
172
+
173
+ Both operations create and bind the destination task immediately. Neither interrupts the source task nor changes the source chat's current task.
174
+
175
+ When creating or forking from the task that is currently running this Skill, run `agentbot task current --json` first and pass its `localSessionId` as `<number-or-task-id>`. The command resolves Codex through `CODEX_THREAD_ID` and TraeX through `TRAECLI_THREAD_ID`, while preferring the only running match when another Agent's variable was inherited from a parent process. If the command cannot identify one task, ask for an explicit AgentBot task ID instead of guessing from list order, title, or project directory.
176
+
177
+ List tasks first, identify the intended source explicitly, then run one of these commands:
153
178
 
154
179
  ```powershell
155
- # Inherit the source task's Agent, project, and execution settings.
180
+ agentbot task list
181
+
182
+ # Fresh task: inherit the source Agent, project, and execution settings.
156
183
  agentbot task newgroup <number-or-task-id> "Review"
157
184
 
158
- # Override the project directory. Prefer an absolute path or a ~-based path.
185
+ # Fresh task: override the inherited project directory.
159
186
  agentbot task newgroup <number-or-task-id> "Review" --dir ~/dev/project
160
187
 
161
- # Create a fresh Projectless App Server task instead of inheriting a project.
188
+ # Fresh task: use a Projectless App Server workspace.
162
189
  agentbot task newgroup <number-or-task-id> "Question" --nodir
163
190
 
164
- # Use another configured Agent with the inherited project shape.
191
+ # Fresh task: use another configured Agent and that Agent's defaults.
165
192
  agentbot task newgroup <number-or-task-id> "Review" --agent traex
193
+
194
+ # Context-preserving branch from the source task.
195
+ agentbot task forkgroup <number-or-task-id> "Parallel review"
166
196
  ```
167
197
 
168
- Apply these rules:
198
+ Apply these New Group rules:
169
199
 
170
- - With neither `--dir` nor `--nodir`, inherit the source task's project directory. If the source is Projectless, create a fresh Projectless workspace rather than reusing its generated directory.
171
- - `--dir <cwd>` overrides the inherited project and resolves `~`, `~/...`, and `~\...` from the user's home directory.
172
- - `--nodir` forces a Projectless task and is valid only for an App Server Agent. Reject combining it with `--dir`.
173
- - With no `--agent`, inherit the source task's Agent, Provider, model, reasoning effort, and permission mode. When `--agent <standard-name>` selects a different Agent, retain the chosen or inherited project shape but use that Agent runtime's own execution defaults.
174
- - The Server must be running. The Profile must contain `FEISHU_USER_OPEN_ID`; the Server invites that saved authorizing user because the CLI has no Lark operator identity.
175
- - Creating the group must not interrupt the source task or switch the source chat's current task. Add `--json` when the caller needs `group.chatId`, `group.contextKey`, or the created task IDs.
200
+ - Treat `<number-or-task-id>` as the source context; never substitute the shell's current working directory for its project directory.
201
+ - With neither `--dir` nor `--nodir`, inherit the source project. If it is Projectless, create a fresh Projectless workspace rather than reusing its generated directory.
202
+ - Resolve `~`, `~/...`, and `~\...` in `--dir <cwd>` from the user's home directory. Reject combining `--dir` with `--nodir`.
203
+ - Allow `--nodir` only for an App Server Agent.
204
+ - With no `--agent`, inherit the source Agent, Provider, model, reasoning effort, and permission mode. With `--agent <standard-name>`, preserve the selected project shape but use the selected Agent's runtime defaults. `--agent` is a CLI-only New Group option.
176
205
 
177
- Use `task forkgroup` to Fork the selected task's latest available completed turn into a new group. It does not interrupt a newer active source turn and does not switch the source chat. Both group commands accept an optional title and `--json`; use the structured result to read the new `group.chatId`, `group.contextKey`, and task IDs.
206
+ Apply these Fork Group rules:
178
207
 
179
- Use `/queue <prompt>` in Feishu to persist a separate follow-up Prompt instead of steering the active turn. Queued Prompts run in FIFO order after the current turn completes and survive Agent Bot restarts. `/nosteer <prompt>` is an equivalent compatibility spelling.
208
+ - Fork the latest available completed turn and preserve its conversation context. Never include or interrupt a newer active turn. Reject the request when no completed turn is available.
209
+ - Keep the source Agent, project, Provider, model, reasoning effort, and permission mode. Fork Group does not accept Agent or project overrides; use New Group when those must change.
210
+ - Use the optional title as the destination task title and group suffix.
180
211
 
181
- For a long-running objective in the active Feishu task, use `/goal <objective>`. Use `/goal` to inspect it, `/goal pause` or `/goal resume` to control automatic continuation, `/goal edit <objective>` to revise it, and `/goal clear` to remove it. Stopping a Goal turn through Agent Bot also pauses the Goal before sending Interrupt so it does not immediately continue.
212
+ Require a running Server for either CLI command. Require `FEISHU_USER_OPEN_ID` so the Server can invite the saved authorizing user; the CLI has no Lark operator identity of its own. Add `--json` when the caller needs `group.chatId`, `group.contextKey`, or the created task IDs.
182
213
 
183
- Use `/agent` in Feishu to open the unified execution-settings card on its Agent tab and `/agent <name>` to directly select the default agent. The Agent tab is present only when multiple agents are configured and remains usable before the chat has a current task. When only one Agent is configured, `/agent` reports that current Agent directly instead of sending a card. Agent selection affects future tasks and does not replace the Agent of an existing task; use `/new` after selecting when a new task is needed.
214
+ Name a New Group without an explicit title as `[agent] [project directory] 新任务 (mm-dd)`, or `[agent] 新任务 (mm-dd)` when Projectless. Name an untitled Fork Group with the source task's persistent `source title(分支 N)` sequence, without a date suffix. Cap the displayed Lark group name at 60 characters by truncating only the group title portion; preserve the complete Agent Bot task title. Do not automatically send a Sessions or Status card to the destination group. Send a welcome message that reports Provider, model, reasoning effort, and permission mode.
184
215
 
185
- Use `/newgroup [title] [--dir <cwd> | --nodir]` in Feishu to create a private group named `[agent] [project directory] <title>`, invite the command sender, and immediately create and bind a new task in that group. `/new` and `/newgroup` share the same project options: an explicit `--dir` overrides the inherited project directory, while `--nodir` forces a Projectless App Server task; reject using both together. Resolve `~`, `~/...`, and `~\...` from the current user's home directory for both commands. Projectless groups omit the project segment and use `[agent] <title>`. The new task inherits the source chat's current task project directory when neither option is present, plus its model, reasoning effort, and permission mode without interrupting or switching the source task. When the source chat has no current task, use the selected agent and its runtime defaults. An explicit title becomes both the group suffix and the task title. When the title is omitted, use task title `新任务` and group name `[agent] [project directory] 新任务 (mm-dd)`, or `[agent] 新任务 (mm-dd)` for Projectless; an explicit title never gets a date suffix. Every new group gets a deterministic scheme-C Identicon avatar: Latin project names display their first word, Chinese names display up to their first four characters, and the normalized full project path hashes into the palette and symmetric node pattern. Projectless groups use the title as the stable seed. Replace the user's home-directory prefix with `~`. The bracketed project-directory value is limited to 15 characters. Longer paths keep the final two directories when they fit, otherwise keep the final directory, truncating the tail only when the final directory itself is overlong. Use `\` on Windows and `/` on macOS/Linux. Do not automatically send a Sessions or Status card to the new group. A later `/new --dir <cwd>` or `/new --nodir` in the group may explicitly replace the inherited project shape.
216
+ Give every created group a deterministic scheme-C Identicon avatar. Display the first word for Latin project names or up to four characters for Chinese names, and hash the normalized full project path into the palette and symmetric node pattern. Use the title as the stable seed for Projectless groups. Replace the user's home prefix with `~` in the group-name project segment. Limit that segment to 15 characters, preferring the final two directories, then the final directory, and only then tail truncation. Use `\` on Windows and `/` on macOS/Linux.
186
217
 
187
- Provider is part of the same inherited execution-settings group as model, reasoning effort, and permission mode. Preserve all four across `/new`, `/newgroup`, `/fork`, `/forkgroup`, and the matching `/sessions` actions. New tasks with no inherited Provider must leave it unset so the selected App Server Agent uses its own default configuration. New-group welcome messages report all four settings.
218
+ Use `/queue <prompt>` in Feishu to persist a separate follow-up Prompt instead of steering the active turn. Queued Prompts run in FIFO order after the current turn completes and survive Agent Bot restarts. `/nosteer <prompt>` is an equivalent compatibility spelling.
188
219
 
189
- Group names created by `/newgroup` and `/forkgroup` are capped at 60 displayed characters. When the generated name would exceed that limit, truncate only the title portion in the Feishu group name; preserve the underlying Agent Bot task title.
220
+ For a long-running objective in the active Feishu task, use `/goal <objective>`. Use `/goal` to inspect it, `/goal pause` or `/goal resume` to control automatic continuation, `/goal edit <objective>` to revise it, and `/goal clear` to remove it. Stopping a Goal turn through Agent Bot also pauses the Goal before sending Interrupt so it does not immediately continue.
190
221
 
191
- Use `/forkgroup [title]` in Feishu to fork the current position into a newly created private group. In a Feishu thread, use the thread's original anchor turn when the thread has no bound task or its bound task has not completed a turn of its own. Once the bound thread task has completed a turn, use its latest locally persisted completed turn; exclude any newer active turn without interrupting it. Resolve this source before generic thread initialization so an unbound thread does not create an intermediate task. Outside a thread, fork the current App Server task's latest available completed turn. Agent Bot invites the command sender and binds the fork as the new group's current task, but does not automatically send a Sessions or Status card there. The new-group welcome message reports the forked task's model, reasoning effort, and permission type. An explicit title is used directly. Without an explicit title, both the forked task and group name use the same persistent `source title(分支 N)` sequence as `/fork`. `/forkgroup` does not add a date suffix.
222
+ Use `/agent` in Feishu to open the unified execution-settings card on its Agent tab and `/agent <name>` to directly select the default agent. The Agent tab is present only when multiple agents are configured and remains usable before the chat has a current task. When only one Agent is configured, `/agent` reports that current Agent directly instead of sending a card. Agent selection affects future tasks and does not replace the Agent of an existing task; use `/new` after selecting when a new task is needed.
192
223
 
193
224
  Use `/new [title] --nodir` to force a new App Server Projectless task even when the current task belongs to a project. `--nodir` and `--dir <cwd>` are mutually exclusive; omitting both preserves the normal project-shape inheritance behavior.
194
225
 
@@ -3,7 +3,10 @@ import readline from "node:readline";
3
3
  import type { Logger } from "pino";
4
4
  import type { AgentConfig } from "../config/schema.js";
5
5
  import { AcpJsonRpcConnection } from "./AcpJsonRpcConnection.js";
6
- import { agentBotEnvironment } from "../runtime/agentEnvironment.js";
6
+ import {
7
+ agentBotEnvironment,
8
+ type AgentEnvironmentContext,
9
+ } from "../runtime/agentEnvironment.js";
7
10
  import { spawnStdioCommand } from "../utils/spawnCommand.js";
8
11
 
9
12
  export interface ManagedAcpProcess {
@@ -16,14 +19,24 @@ export interface ManagedAcpProcess {
16
19
  export class AcpProcessManager {
17
20
  private readonly processes = new Map<string, ManagedAcpProcess>();
18
21
 
19
- constructor(private readonly logger: Logger) {}
22
+ constructor(
23
+ private readonly logger: Logger,
24
+ private readonly environmentContext: () => AgentEnvironmentContext = () => ({}),
25
+ ) {}
20
26
 
21
27
  start(processKey: string, agentName: string, agent: AgentConfig): ManagedAcpProcess {
22
28
  if (this.processes.has(processKey)) {
23
29
  throw new Error(`ACP process already exists: ${processKey}`);
24
30
  }
25
31
 
26
- const child = spawnStdioCommand(agent.command, agent.args, agentBotEnvironment(process.env, agent.env));
32
+ const child = spawnStdioCommand(
33
+ agent.command,
34
+ agent.args,
35
+ agentBotEnvironment(process.env, agent.env, {
36
+ ...this.environmentContext(),
37
+ agentName,
38
+ }),
39
+ );
27
40
 
28
41
  const childLogger = this.logger.child({ processKey, agentName });
29
42
  const connection = new AcpJsonRpcConnection(child, childLogger);
@@ -40,6 +40,7 @@ export class AcpSessionManager {
40
40
  private readonly sessions = new Map<string, RuntimeSession>();
41
41
  private readonly sessionsByAcpId = new Map<string, RuntimeSession>();
42
42
  private processStart?: Promise<ManagedAcpProcess>;
43
+ private processVersion?: string;
43
44
 
44
45
  constructor(
45
46
  private readonly agentName: string,
@@ -52,6 +53,15 @@ export class AcpSessionManager {
52
53
  return this.sessions.get(localSessionId);
53
54
  }
54
55
 
56
+ getProcessInfo(): { pid?: number; version?: string } {
57
+ const pid = this.processManager.get(this.agentName)?.child.pid;
58
+ if (!pid) return {};
59
+ return {
60
+ pid,
61
+ ...(this.processVersion ? { version: this.processVersion } : {}),
62
+ };
63
+ }
64
+
55
65
  async create(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
56
66
  if (input.agentName !== this.agentName) throw new Error(`Unexpected agent: ${input.agentName}`);
57
67
 
@@ -133,6 +143,7 @@ export class AcpSessionManager {
133
143
  this.sessions.clear();
134
144
  this.sessionsByAcpId.clear();
135
145
  this.processStart = undefined;
146
+ this.processVersion = undefined;
136
147
  this.processManager.stopAll();
137
148
  }
138
149
 
@@ -181,6 +192,7 @@ export class AcpSessionManager {
181
192
  }
182
193
 
183
194
  private async startProcess(): Promise<ManagedAcpProcess> {
195
+ this.processVersion = undefined;
184
196
  const managed = this.processManager.start(this.agentName, this.agentName, this.agent);
185
197
  try {
186
198
  const connection = managed.connection;
@@ -210,6 +222,8 @@ export class AcpSessionManager {
210
222
  version: "0.1.0",
211
223
  },
212
224
  });
225
+ const version = initializeResult.agentInfo?.version?.trim();
226
+ if (version) this.processVersion = version;
213
227
  if (initializeResult.authMethods?.length) {
214
228
  const method = initializeResult.authMethods[0];
215
229
  this.logger.info({ methodId: method.id }, "Authenticating ACP agent with first advertised method.");
@@ -0,0 +1,160 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import pino from "pino";
5
+ import { loadConfig } from "../config/loadConfig.js";
6
+ import type { AppConfig } from "../config/schema.js";
7
+ import {
8
+ CardRenderer,
9
+ type InitializationWelcomeFeature,
10
+ type InitializationWelcomeKind,
11
+ } from "../feishu/CardRenderer.js";
12
+ import { FeishuMessageClient } from "../feishu/FeishuMessageClient.js";
13
+ import type { CliLanguage } from "./i18n.js";
14
+
15
+ export interface InitializationReceipt {
16
+ version: string;
17
+ initializedAt: string;
18
+ }
19
+
20
+ export type InitializationWelcomeResult =
21
+ | { status: "sent"; kind: InitializationWelcomeKind }
22
+ | { status: "skipped"; kind: InitializationWelcomeKind; reason: "feishu-skipped" | "missing-user-open-id" }
23
+ | { status: "failed"; kind: InitializationWelcomeKind; message: string };
24
+
25
+ export interface SendInitializationWelcomeInput {
26
+ configPath?: string;
27
+ language: CliLanguage;
28
+ version: string;
29
+ previousVersion?: string;
30
+ kind: InitializationWelcomeKind;
31
+ activationPending?: boolean;
32
+ }
33
+
34
+ export interface InitializationWelcomeDependencies {
35
+ loadConfig(configPath?: string): AppConfig;
36
+ sendCard(config: AppConfig, contextKey: string, card: Record<string, unknown>): Promise<void>;
37
+ logoPath: string;
38
+ }
39
+
40
+ const INITIALIZATION_RECEIPT_FILE = "initialization.json";
41
+ const DEFAULT_LOGO_PATH = fileURLToPath(new URL("../../assets/agent-bot-logo.png", import.meta.url));
42
+
43
+ export function resolveInitializationWelcomeKind(input: {
44
+ firstInitialization: boolean;
45
+ previousVersion?: string;
46
+ currentVersion: string;
47
+ }): InitializationWelcomeKind {
48
+ if (input.firstInitialization) return "first";
49
+ return input.previousVersion === input.currentVersion ? "refresh" : "upgrade";
50
+ }
51
+
52
+ export function readInitializationReceipt(homePath: string): InitializationReceipt | undefined {
53
+ try {
54
+ const value = JSON.parse(fs.readFileSync(initializationReceiptPath(homePath), "utf8")) as Partial<InitializationReceipt>;
55
+ if (typeof value.version !== "string" || !value.version.trim()) return undefined;
56
+ if (typeof value.initializedAt !== "string" || !value.initializedAt.trim()) return undefined;
57
+ return { version: value.version.trim(), initializedAt: value.initializedAt.trim() };
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ export function writeInitializationReceipt(homePath: string, version: string, initializedAt = new Date()): void {
64
+ const filePath = initializationReceiptPath(homePath);
65
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
66
+ fs.writeFileSync(filePath, `${JSON.stringify({
67
+ version,
68
+ initializedAt: initializedAt.toISOString(),
69
+ }, null, 2)}\n`, { mode: 0o600 });
70
+ }
71
+
72
+ export async function sendInitializationWelcome(
73
+ input: SendInitializationWelcomeInput,
74
+ dependencies: InitializationWelcomeDependencies = defaultDependencies,
75
+ ): Promise<Exclude<InitializationWelcomeResult, { status: "failed" }>> {
76
+ const config = dependencies.loadConfig(input.configPath);
77
+ const userOpenId = config.feishu.userOpenId?.trim();
78
+ if (!userOpenId) {
79
+ return { status: "skipped", kind: input.kind, reason: "missing-user-open-id" };
80
+ }
81
+ const defaultAgentName = config.defaults.agent!;
82
+ const defaultAgent = config.agents[defaultAgentName]!;
83
+ const renderer = new CardRenderer();
84
+ const card = renderer.renderInitializationWelcome({
85
+ kind: input.kind,
86
+ language: input.language,
87
+ version: input.version,
88
+ previousVersion: input.previousVersion,
89
+ activationPending: input.activationPending,
90
+ defaultAgentName,
91
+ defaultAgentTitle: defaultAgent.title,
92
+ availableAgents: Object.values(config.agents).map((agent) => agent.title),
93
+ logoPath: dependencies.logoPath,
94
+ features: welcomeFeatures(input.kind, input.language),
95
+ });
96
+ await dependencies.sendCard(config, `open_id:${userOpenId}`, card);
97
+ return { status: "sent", kind: input.kind };
98
+ }
99
+
100
+ function initializationReceiptPath(homePath: string): string {
101
+ return path.join(homePath, "data", INITIALIZATION_RECEIPT_FILE);
102
+ }
103
+
104
+ function welcomeFeatures(
105
+ kind: InitializationWelcomeKind,
106
+ language: CliLanguage,
107
+ ): InitializationWelcomeFeature[] {
108
+ if (kind === "upgrade") {
109
+ return language === "zh"
110
+ ? [
111
+ { icon: "⚙️", title: "Agent 状态更透明", description: "Server 状态会显示每个 Agent 的 PID 与版本。" },
112
+ { icon: "🧭", title: "快速定位当前任务", description: "CLI 可识别正在调用它的 Codex 或 TraeX 任务。" },
113
+ { icon: "🗂️", title: "卡片操作更顺手", description: "Help 命令可点击,Sessions 翻页与操作更清晰。" },
114
+ { icon: "🛡️", title: "Crash 恢复更完整", description: "服务重启后会修复历史 Turn 的父子连接。" },
115
+ ]
116
+ : [
117
+ { icon: "⚙️", title: "Clearer Agent status", description: "Server status now shows each Agent's PID and version." },
118
+ { icon: "🧭", title: "Find the current task", description: "The CLI can identify its invoking Codex or TraeX task." },
119
+ { icon: "🗂️", title: "Better card actions", description: "Clickable Help commands and clearer Sessions pagination." },
120
+ { icon: "🛡️", title: "Stronger crash recovery", description: "Turn graph links are repaired after service recovery." },
121
+ ];
122
+ }
123
+ if (kind === "refresh") {
124
+ return language === "zh"
125
+ ? [
126
+ { icon: "💬", title: "随时开始对话", description: "私聊或群聊发送消息,即可继续当前任务。" },
127
+ { icon: "🧠", title: "统一运行设置", description: "在同一张卡片中切换 Agent、模型、思考和权限。" },
128
+ { icon: "🌿", title: "任务分支", description: "使用 New Group 或 Fork Group 并行处理工作。" },
129
+ { icon: "🔄", title: "安全恢复", description: "任务、进度与服务状态会持久保存并恢复。" },
130
+ ]
131
+ : [
132
+ { icon: "💬", title: "Start anywhere", description: "Continue work from a private chat or group conversation." },
133
+ { icon: "🧠", title: "Unified settings", description: "Switch Agent, model, thinking, and permissions in one card." },
134
+ { icon: "🌿", title: "Branch tasks", description: "Use New Group or Fork Group for parallel work." },
135
+ { icon: "🔄", title: "Safe recovery", description: "Tasks, progress, and service state persist across restarts." },
136
+ ];
137
+ }
138
+ return language === "zh"
139
+ ? [
140
+ { icon: "💬", title: "飞书直接对话", description: "私聊、群聊和话题分别维护自己的任务上下文。" },
141
+ { icon: "🤖", title: "连接本地 Agent", description: "目前支持 Codex、TraeX 和兼容的编程 Agent。" },
142
+ { icon: "🌿", title: "新建与分支", description: "把新任务或对话分支放进独立群聊并行推进。" },
143
+ { icon: "📍", title: "进度始终可见", description: "通过思考卡片查看工具调用、文件变更和最终结果。" },
144
+ ]
145
+ : [
146
+ { icon: "💬", title: "Work from Lark", description: "Private chats, groups, and topics keep separate task context." },
147
+ { icon: "🤖", title: "Connect local Agents", description: "Use Codex, TraeX, and compatible coding Agents." },
148
+ { icon: "🌿", title: "Create and branch", description: "Move fresh tasks or conversation branches into new groups." },
149
+ { icon: "📍", title: "See every step", description: "Follow tools, file changes, and final results in progress cards." },
150
+ ];
151
+ }
152
+
153
+ const defaultDependencies: InitializationWelcomeDependencies = {
154
+ loadConfig,
155
+ logoPath: DEFAULT_LOGO_PATH,
156
+ sendCard: async (config, contextKey, card) => {
157
+ const logger = pino({ level: "silent" });
158
+ await new FeishuMessageClient(config, logger).sendInteractiveCard(contextKey, card);
159
+ },
160
+ };
package/src/cli/help.ts CHANGED
@@ -13,10 +13,11 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
13
13
  init --reset 重置并重新配置显式指定的 Profile
14
14
  console 打开本地控制台
15
15
  server start 在后台启动 Agent Bot
16
- server status 显示服务、飞书 App ID 和安全重启状态
16
+ server status 显示服务、飞书 App ID、Agent 进程和安全重启状态
17
17
  server restart [--task <任务>] 安排安全重启并将状态发回指定任务会话
18
18
  server stop 停止 Agent Bot
19
19
  task list 列出任务
20
+ task current [--json] 显示当前调用任务的详情
20
21
  task status <任务> 显示任务状态
21
22
  task chat <任务> 显示任务绑定的飞书会话
22
23
  task prompt <任务> <提示词> 向任务发送提示词
@@ -66,10 +67,11 @@ Common commands:
66
67
  init --reset Reset and reconfigure an explicit profile
67
68
  console Open the local console
68
69
  server start Start Agent Bot in the background
69
- server status Show the server, Lark App ID, and safe-restart status
70
+ server status Show the server, Lark App ID, Agent processes, and safe-restart status
70
71
  server restart [--task <task>] Schedule a safe restart and return status to the task's conversation
71
72
  server stop Stop Agent Bot
72
73
  task list List tasks
74
+ task current [--json] Show details for the task invoking the CLI
73
75
  task status <task> Show task status
74
76
  task chat <task> Show the task's Lark chat binding
75
77
  task prompt <task> <prompt> Send a Prompt to a task
@@ -29,16 +29,44 @@ export function formatServerStatus(data: unknown, language: CliLanguage = cliLan
29
29
  language,
30
30
  )
31
31
  : cliText("not scheduled", "未计划", language);
32
+ const agents = formatAgentProcesses(value.agents, language);
32
33
  return [
33
34
  `${cliText("Agent Bot server: ", "Agent Bot 服务:", language)}${state}`,
34
35
  `${cliText("Lark App ID: ", "飞书 App ID:", language)}${value.feishuAppId ?? cliText("not configured", "未配置", language)}`,
35
36
  `${cliText("PID: ", "PID:", language)}${value.pid ?? "-"}`,
36
37
  `${cliText("Started at: ", "启动时间:", language)}${value.startedAt ?? "-"}`,
37
38
  `${cliText("Supervisor: ", "Supervisor:", language)}${value.supervised ? cliText("enabled", "已启用", language) : cliText("disabled", "未启用", language)}`,
39
+ ...agents,
38
40
  `${cliText("Running tasks: ", "运行中任务:", language)}${activity.runningSessions ?? 0}`,
39
41
  `${cliText("Pending final deliveries: ", "待发送最终结果:", language)}${activity.pendingFinalDeliveries ?? 0}`,
40
42
  `${cliText("Safe restart: ", "安全重启:", language)}${safeRestart}`,
41
43
  "",
42
44
  ].join("\n");
43
45
  }
46
+
47
+ function formatAgentProcesses(value: unknown, language: CliLanguage): string[] {
48
+ const agents = Array.isArray(value)
49
+ ? value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")
50
+ : [];
51
+ if (agents.length === 0) {
52
+ return [`${cliText("Agents: ", "Agent 进程:", language)}-`];
53
+ }
54
+ return [
55
+ cliText("Agents:", "Agent 进程:", language),
56
+ ...agents.map((agent) => {
57
+ const name = typeof agent.name === "string" && agent.name.trim() ? agent.name.trim() : "-";
58
+ const title = typeof agent.title === "string" && agent.title.trim() ? agent.title.trim() : undefined;
59
+ const label = title && title !== name ? `${name} (${title})` : name;
60
+ const pid = typeof agent.pid === "number" && Number.isInteger(agent.pid) && agent.pid > 0
61
+ ? String(agent.pid)
62
+ : "-";
63
+ const version = typeof agent.version === "string" && agent.version.trim()
64
+ ? agent.version.trim()
65
+ : "-";
66
+ return language === "zh"
67
+ ? ` ${label}:PID ${pid},版本 ${version}`
68
+ : ` ${label}: PID ${pid}, version ${version}`;
69
+ }),
70
+ ];
71
+ }
44
72
  import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
@@ -0,0 +1,91 @@
1
+ import type { SessionRecord } from "../state/StateStore.js";
2
+ import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
3
+
4
+ export type CurrentTaskResolution =
5
+ | { status: "found"; session: SessionRecord }
6
+ | { status: "missing-thread-id" }
7
+ | { status: "not-found"; threadIds: string[] }
8
+ | { status: "ambiguous"; sessions: SessionRecord[]; threadIds: string[] };
9
+
10
+ export function formatTaskList(
11
+ sessions: SessionRecord[],
12
+ language: CliLanguage = cliLanguage,
13
+ currentThreadIds: readonly string[] = [],
14
+ ): string {
15
+ if (sessions.length === 0) return cliText("No tasks.\n", "没有任务。\n", language);
16
+
17
+ const current = resolveCurrentTask(sessions, currentThreadIds);
18
+ const currentLocalSessionId = current.status === "found" ? current.session.localSessionId : undefined;
19
+ return sessions.map((session, index) => {
20
+ const title = session.title ?? cliText("Untitled task", "未命名任务", language);
21
+ const current = session.localSessionId === currentLocalSessionId
22
+ ? ` [${cliText("Current", "当前任务", language)}]`
23
+ : "";
24
+ const nativeId = nativeTaskIdLabel(session, language);
25
+ return [
26
+ `${index + 1}.${current} [${taskStateLabel(session.status, language)}/${taskStateLabel(session.lastTurnStatus, language)}] ${title}`,
27
+ ` ${cliText("Agent: ", "Agent:", language)}${session.agentName} · ${cliText("AgentBot task ID: ", "AgentBot 任务 ID:", language)}${session.localSessionId}`,
28
+ ` ${nativeId ? `${nativeId} · ` : ""}${session.contextKey} · ${session.updatedAt}`,
29
+ ].join("\n");
30
+ }).join("\n") + "\n";
31
+ }
32
+
33
+ export function currentAppServerThreadIds(env: NodeJS.ProcessEnv): string[] {
34
+ return [...new Set([
35
+ env.CODEX_THREAD_ID?.trim(),
36
+ env.TRAECLI_THREAD_ID?.trim(),
37
+ ].filter((value): value is string => Boolean(value)))];
38
+ }
39
+
40
+ export function resolveCurrentTask(
41
+ sessions: SessionRecord[],
42
+ currentThreadIds: readonly string[],
43
+ ): CurrentTaskResolution {
44
+ const threadIds = [...new Set(currentThreadIds.map((value) => value.trim()).filter(Boolean))];
45
+ if (threadIds.length === 0) return { status: "missing-thread-id" };
46
+
47
+ const candidates = new Set(threadIds);
48
+ const matches = sessions.filter((session) => session.remoteSessionId && candidates.has(session.remoteSessionId));
49
+ if (matches.length === 0) return { status: "not-found", threadIds };
50
+ if (matches.length === 1) return { status: "found", session: matches[0]! };
51
+
52
+ const running = matches.filter((session) =>
53
+ session.status === "running" || session.lastTurnStatus === "running");
54
+ if (running.length === 1) return { status: "found", session: running[0]! };
55
+ return { status: "ambiguous", sessions: matches, threadIds };
56
+ }
57
+
58
+ export function taskStateLabel(
59
+ status: string | undefined,
60
+ language: CliLanguage = cliLanguage,
61
+ ): string {
62
+ if (!status) return "-";
63
+ const chinese: Record<string, string> = {
64
+ starting: "启动中",
65
+ ready: "就绪",
66
+ running: "运行中",
67
+ closed: "已关闭",
68
+ failed: "失败",
69
+ completed: "已完成",
70
+ cancelled: "已取消",
71
+ interrupted: "已中断",
72
+ inProgress: "进行中",
73
+ active: "活动中",
74
+ idle: "空闲",
75
+ not_loaded: "未加载",
76
+ error: "错误",
77
+ };
78
+ return language === "zh" ? chinese[status] ?? status : status;
79
+ }
80
+
81
+ function nativeTaskIdLabel(session: SessionRecord, language: CliLanguage): string | undefined {
82
+ if (session.runtimeKind === "acp" || session.acpSessionId) {
83
+ const sessionId = session.acpSessionId ?? session.remoteSessionId;
84
+ return sessionId
85
+ ? `${cliText("ACP session ID: ", "ACP Session ID:", language)}${sessionId}`
86
+ : undefined;
87
+ }
88
+ return session.remoteSessionId
89
+ ? `${cliText("App Server thread ID: ", "App Server 原生任务 ID(Thread ID):", language)}${session.remoteSessionId}`
90
+ : undefined;
91
+ }