@ganglion/xacpx 0.14.0 → 0.15.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 CHANGED
@@ -23,18 +23,12 @@ English · **[中文](./docs/zh/README_zh.md)**
23
23
 
24
24
  If you need to code or work remotely on a temporary basis, `xacpx` gives you a fast, convenient **remote entry point** so you can get things done from WeChat or Feishu anytime, anywhere.
25
25
 
26
- ## Who it's for
27
-
28
- `xacpx` suits users who want lightweight, on-demand multi-agent work. You can watch tasks, send commands, and view results from WeChat, Feishu, or Yuanbao, and manage multiple sessions within the same chat.
29
-
30
- > For everyday use, remember `/ss` first: it creates or reuses an xacpx logical session. If you want to attach to an existing native session of a local agent such as Codex, use `/ssn`; see [docs/native-sessions.md](./docs/native-sessions.md) for advanced details.
26
+ > For everyday use, remember `/ss` first: it creates or reuses an xacpx logical session. If you want to attach to an existing native session of a local agent such as Codex, use `/ssn`; see [native sessions](./docs/native-sessions.md).
31
27
 
32
28
  ## 5-minute quick start
33
29
 
34
30
  ### Prerequisites
35
31
 
36
- Before you start, you need at least:
37
-
38
32
  - Node.js 22+ or Bun
39
33
  - A working agent CLI you intend to use, such as Codex / Claude Code / Gemini / OpenCode
40
34
  - A phone with WeChat, Feishu, or Yuanbao installed
@@ -49,32 +43,25 @@ npm install -g @ganglion/xacpx --registry=https://registry.npmjs.org
49
43
  bun add -g @ganglion/xacpx
50
44
  ```
51
45
 
52
- ### Log in to WeChat
46
+ ### Log in and start
53
47
 
54
48
  ```bash
55
- xacpx login
49
+ xacpx login # shows a QR code; scan it with WeChat
50
+ xacpx start # start the background service
56
51
  ```
57
52
 
58
- The terminal will show a QR code; scan it with WeChat to log in.
59
-
60
- If you want to use Feishu or Yuanbao instead of WeChat, see "Switch / add other channels" below first.
61
-
62
- ### Start the service
63
-
64
- ```bash
65
- xacpx start
66
- ```
53
+ To use Feishu or Yuanbao instead of WeChat, see "Other channels" below first.
67
54
 
68
55
  ### Create your first session in WeChat
69
56
 
70
- Send these two messages in WeChat:
57
+ Send these messages in WeChat:
71
58
 
72
59
  ```text
73
60
  /ss codex -d /absolute/path/to/your/repo
74
61
  /help
75
62
  ```
76
63
 
77
- Then just send plain text, for example:
64
+ Then just send plain text:
78
65
 
79
66
  ```text
80
67
  hello
@@ -82,14 +69,9 @@ hello
82
69
 
83
70
  If everything works, plain text goes into the current session and the agent's reply comes back to WeChat.
84
71
 
85
- ### Switch / add other channels
86
-
87
- WeChat is the built-in default channel. Feishu and Yuanbao are distributed as official plugin packages, and third-party channels follow the same plugin flow. If you can't remember the package names, check the official plugin list first:
72
+ ### Other channels
88
73
 
89
- ```bash
90
- xacpx plugin known
91
- # Install: xacpx plugin add <package>
92
- ```
74
+ WeChat is the built-in default channel. Feishu and Yuanbao are distributed as official plugin packages, and third-party channels follow the same plugin flow. If you can't remember the package names, run `xacpx plugin known` first.
93
75
 
94
76
  ```bash
95
77
  # Feishu
@@ -103,7 +85,7 @@ xacpx channel add yuanbao # enter appKey/appSecret when prompted
103
85
  xacpx restart
104
86
  ```
105
87
 
106
- For full credential configuration, parameters, and management commands such as `enable/disable/rm`, see [docs/channel-management.md](./docs/channel-management.md). If you want to write your own channel plugin, see [docs/plugin-development.md](./docs/plugin-development.md).
88
+ Full credentials, parameters, and management commands (`enable/disable/rm`): [channel-management.md](./docs/channel-management.md). To write your own channel plugin: [plugin-development.md](./docs/plugin-development.md).
107
89
 
108
90
  ## Your everyday workflow
109
91
 
@@ -111,34 +93,16 @@ The most common sequence is just four steps:
111
93
 
112
94
  1. **Start the background service**: `xacpx start`
113
95
  2. **Create or switch sessions**: `/ss ...`, `/use ...`
114
- 3. **Send plain text directly**: let the current session keep working
115
- 4. **Check status or cancel the current task when needed**: `/status`, `/cancel`
116
-
117
- ### 1) Create a session
118
-
119
- The most common command:
120
-
121
- ```text
122
- /ss codex -d /absolute/path/to/your/repo
123
- ```
124
-
125
- It uses `codex`, binds this working directory, and automatically switches to the new session.
126
-
127
- ### 2) Send plain messages
128
-
129
- Any text not starting with `/` is sent to the current session.
130
-
131
- ```text
132
- Fix this recent API timeout issue
133
- ```
96
+ 3. **Send plain text directly**: any text not starting with `/` goes to the current session
97
+ 4. **Check status or cancel when needed**: `/status`, `/cancel`
134
98
 
135
- ### 3) View replies
99
+ ### Reply modes
136
100
 
137
- `xacpx` supports three common reply modes:
101
+ `xacpx` supports three reply modes (switch per session with `/replymode`):
138
102
 
139
103
  - `stream`: stream back intermediate text
140
104
  - `final`: return only the final result
141
- - `verbose`: the default; in addition to streaming text, also shows tool-call summaries
105
+ - `verbose`: the default; streaming text plus tool-call summaries
142
106
 
143
107
  For example, in `verbose` mode you'll see:
144
108
 
@@ -149,474 +113,121 @@ For example, in `verbose` mode you'll see:
149
113
  ✏️ Edit parse-command.ts
150
114
  ```
151
115
 
152
- ### 4) Switch sessions
116
+ ## Command cheat sheet
153
117
 
154
- ```text
155
- /ss
156
- /use backend:codex
157
- ```
158
-
159
- This lets you switch between sessions for different projects and different agents within the same WeChat chat.
160
-
161
- ## Common CLI commands
118
+ The essentials to get going. Full references: **CLI → [cli-reference.md](./docs/cli-reference.md)**, **chat commands → [commands.md](./docs/commands.md)**.
162
119
 
163
- These commands run in a terminal on your computer.
120
+ **Terminal (on the host):**
164
121
 
165
122
  | Command | Description |
166
123
  |------|------|
167
- | `xacpx login` | Log in to WeChat |
168
- | `xacpx logout` | Clear the WeChat login credentials saved on this machine |
169
- | `xacpx run` | Run in the foreground, useful for debugging |
170
- | `xacpx start` | Start the service in the background |
171
- | `xacpx status` | Show background status, PID, config path, and log path |
172
- | `xacpx stop` | Stop the background instance |
173
- | `xacpx restart` | Restart the background instance so channel config changes take effect |
174
- | `xacpx update [--all\|<name>]` | Check and update xacpx and installed plugins; when plugins are installed, it interactively lets you choose what to update |
175
- | `xacpx channel list\|show\|add\|rm\|enable\|disable [--account <id>]` | Manage message channels; `--account <id>` targets one bot when several share a channel (multi-bot) |
176
- | `xacpx plugin list\|add\|update\|remove\|enable\|disable\|doctor\|known` | Manage plugins: list/install/update/remove, toggle, run `doctor`, or list official packages with `known` |
177
- | `xacpx plugin add @ganglion/xacpx-channel-feishu && xacpx channel add feishu` | Install and add the Feishu channel; prompts for Feishu app credentials |
178
- | `xacpx plugin add @ganglion/xacpx-channel-yuanbao && xacpx channel add yuanbao` | Install and add the Yuanbao channel; prompts for Yuanbao appKey/appSecret |
124
+ | `xacpx login` / `logout` | Log in / out of WeChat |
125
+ | `xacpx start` / `stop` / `restart` / `status` | Manage the background service |
126
+ | `xacpx update` | Update xacpx and installed plugins |
179
127
  | `xacpx doctor` | Run environment diagnostics |
180
- | `xacpx version` | Show the current version |
181
- | `xacpx agent list` | List agents registered on this machine |
182
- | `xacpx agent add <name>` | Add an agent from a built-in template; an existing agent of the same name with a different config is not overwritten |
183
- | `xacpx agent rm <name>` | Remove an agent |
184
- | `xacpx workspace list` | List workspaces registered on this machine |
185
- | `xacpx workspace add [name] [--raw]` | Register the current directory as a workspace; without `name`, uses the current directory name, and names with special characters are normalized automatically |
186
- | `xacpx workspace rm <name>` | Remove a workspace |
187
- | `xacpx later list` / `xacpx lt list` | List this machine's pending scheduled tasks in the terminal |
188
- | `xacpx later cancel <id>` / `xacpx lt cancel <id>` | Cancel a pending scheduled task in the terminal |
189
-
190
- The first time you run `xacpx start` or `xacpx run`, if there are no sessions, workspaces, or plugins, the CLI asks whether to register the current directory as a workspace and lets you choose a built-in agent template; after the service starts, it creates the initial acpx session through the normal session-creation flow.
191
-
192
- `workspace` can also be abbreviated as `ws`:
193
-
194
- ```bash
195
- xacpx ws add
196
- xacpx ws list
197
- xacpx ws rm backend
198
- ```
199
-
200
- ### How to use the `workspace` CLI
201
-
202
- `xacpx workspace` maintains the `workspaces` config in `~/.xacpx/config.json` on your local machine. It's good for registering frequently used project directories in the terminal first, then referencing them directly in WeChat with `--ws <name>`.
203
-
204
- | Command | Description |
205
- |------|------|
206
- | `xacpx workspace list` | List registered workspaces and their paths |
207
- | `xacpx workspace add` | Register the current directory as a workspace, defaulting the name to the current directory name (normalized automatically) |
208
- | `xacpx workspace add <name>` | Register the current directory under a specific name (normalized if it contains special characters) |
209
- | `xacpx workspace add [name] --raw` | Keep the original name (including spaces, etc.); later commands must quote it |
210
- | `xacpx workspace rm <name>` | Remove a specific workspace |
211
-
212
- Common usage:
213
-
214
- ```bash
215
- cd /absolute/path/to/backend
216
- xacpx workspace add backend
217
-
218
- cd /absolute/path/to/frontend
219
- xacpx ws add frontend
220
-
221
- xacpx ws list
222
- xacpx ws rm frontend
223
- ```
128
+ | `xacpx channel add <name>` | Add a message channel (Feishu / Yuanbao / …) |
129
+ | `xacpx ws add` / `xacpx agent add <name>` | Register a workspace / agent |
224
130
 
225
- Once registered, you can use it directly in WeChat:
226
-
227
- ```text
228
- /ss codex --ws backend
229
- /ss new claude --ws frontend
230
- ```
231
-
232
- Note: `workspace add` always registers the **directory the terminal is currently in**. Without a name, it uses the current directory name as the workspace name. Names containing spaces, Chinese characters, etc. are normalized automatically to `[a-zA-Z0-9._-]+` (for example, the directory `My Project` is saved as `My-Project`), with `-2`, `-3` appended on collisions. To keep the original name, add `--raw`; afterwards `xacpx workspace rm`, `/ws rm`, and `--ws <name>` all need quoting, for example `xacpx workspace rm "My Project"`.
233
-
234
- ### How to use the `agent` CLI
235
-
236
- `xacpx agent` maintains the `agents` config in `~/.xacpx/config.json` on your local machine; `agents` is an equivalent alias.
131
+ **Chat (in WeChat / Feishu / Yuanbao):**
237
132
 
238
133
  | Command | Description |
239
134
  |------|------|
240
- | `xacpx agent list` | List registered agents |
241
- | `xacpx agent templates` | List the built-in templates you can add |
242
- | `xacpx agent add <name>` | Add an agent from a built-in template, e.g. `kimi`, `opencode` |
243
- | `xacpx agent rm <name>` | Remove a specific agent |
244
-
245
- Common usage:
246
-
247
- ```bash
248
- xacpx agent templates
249
- xacpx agent add kimi
250
- xacpx agents list
251
- xacpx agent rm kimi
252
- ```
253
-
254
- ### How to use `doctor`
255
-
256
- ```bash
257
- xacpx doctor
258
- xacpx doctor --verbose
259
- xacpx doctor --smoke
260
- xacpx doctor --smoke --agent codex --workspace backend
261
- xacpx doctor --fix
262
- ```
263
-
264
- Notes:
265
-
266
- - `--verbose` expands the details of each check
267
- - `--smoke` additionally runs a minimal real transport-level prompt check
268
- - `--agent` / `--workspace` only affect `--smoke`
269
- - Without `--smoke`, the related checks show as `SKIP`
270
- - `--fix` applies safe local repairs (runtime dir permissions, stale locks, invalid state records) and re-checks; state-mutating repairs are withheld while the daemon runs — see [docs/doctor-command.md](docs/doctor-command.md)
271
-
272
- ### How to use `update`
273
-
274
- `xacpx update` checks for and installs new versions of xacpx itself and your installed channel plugins.
275
-
276
- ```bash
277
- xacpx update # interactive: pick what to update
278
- xacpx update --all # update everything (core + all plugins) non-interactively
279
- xacpx update <name> # update a single target (the core, or a specific plugin package)
280
- ```
281
-
282
- Notes:
283
-
284
- - When plugins are installed, the bare `xacpx update` is interactive and lets you choose which targets to update.
285
- - In a non-interactive environment, updating the core or plugins needs explicit confirmation: use `xacpx update --all`, or name the target with `xacpx update <name>`.
286
- - `update` covers the core package and channel plugins; to manage a single plugin's version directly, see `xacpx plugin update <name>` ([docs/plugin-development.md](./docs/plugin-development.md)).
287
- - After updating, run `xacpx restart` so a running daemon loads the new version.
288
- - Cross-package rename migration: this project was renamed `weacpx` → `xacpx`. If you still have the legacy `weacpx` package installed, running `weacpx update` will offer to migrate you across to `xacpx` automatically (you confirm the switch). Already on `xacpx`? Just use `xacpx update` as a normal self-update.
289
-
290
- ## Common chat commands
291
-
292
- These commands are sent in a WeChat or Feishu chat. For the full command reference, see [docs/commands.md](./docs/commands.md).
293
-
294
- ### Agent management
295
-
296
- The default config usually already includes `codex` and `claude`. If you want to use another acpx-supported agent, you can add it from a built-in template with `/agent add <name>`.
297
-
298
- | Command | Description |
299
- |------|------|
300
- | `/agents` | List agents |
301
- | `/agent add gemini` | Add the `Gemini` agent |
302
- | `/agent add opencode` | Add the `OpenCode` agent |
303
- | `/agent rm <name>` | Remove an agent |
304
-
305
- The current built-in templates align with acpx's built-in agents:
306
-
307
- ```text
308
- codex, claude, pi, openclaw, gemini, cursor, copilot, droid,
309
- factory-droid, factorydroid, iflow, kilocode, kimi, kiro,
310
- opencode, qoder, qwen, trae
311
- ```
312
-
313
- These templates only write `driver`; the actual launch command is resolved by acpx. For example, `/agent add kimi` saves `{ "driver": "kimi" }`. For full command docs see [docs/commands.md](./docs/commands.md), and for config fields see [docs/config-reference.md](./docs/config-reference.md).
314
-
315
- ### Workspace management
316
-
317
- | Command | Description |
318
- |------|------|
319
- | `/workspaces` / `/workspace` / `/ws` | List workspaces |
320
- | `/ws new <name> -d <path> [--raw]` | Add a workspace; `path` is an absolute path on your computer, and Windows does not distinguish forward/back slashes; names with special characters such as spaces/Chinese are normalized automatically, and --raw keeps the original name |
321
- | `/workspace rm <name>` | Remove a workspace |
322
-
323
- ### Sessions
324
-
325
- | Command | Description |
326
- |------|------|
327
- | `/sessions` / `/session` / `/ss` | List sessions |
328
- | `/ss <agent> (-d <path> \| --ws <name>)` | Create or reuse your current most-used session |
329
- | `/ss new <agent> (-d <path> \| --ws <name>)` | Force-create a new session |
330
- | `/ssn <agent> (-d <path> \| --ws <name>)` | Attach to an existing native session of a local agent; see [native sessions](./docs/native-sessions.md) |
135
+ | `/ss <agent> -d <path>` | Create or reuse a session in a project directory |
136
+ | `/ss new <agent> --ws <name>` | Force-create a new session |
137
+ | `/ssn <agent> -d <path>` | Attach to a local agent's [native session](./docs/native-sessions.md) |
331
138
  | `/use <alias>` | Switch the current session |
332
- | `/status` | Show the current session status |
333
- | `/mode` / `/mode <id>` | View or set the underlying `acpx` mode |
334
- | `/model` / `/model <id>` | View or switch the session's LLM model (also `/session new --model`, `/agent add --model`) |
335
- | `/replymode` | Show the current reply mode |
336
- | `/replymode stream` | Streaming replies |
337
- | `/replymode verbose` | Streaming + tool-call summaries |
338
- | `/replymode final` | Return only the final result |
339
- | `/replymode reset` | Fall back to the global default reply mode |
340
- | `/session reset` | Reset the current session context |
341
- | `/clear` | Shortcut alias for `/session reset` |
342
- | `/cancel` / `/stop` | Stop the current task |
343
-
344
- We suggest remembering these three first:
139
+ | `/status` · `/cancel` | Show status · stop the current task |
140
+ | `/model` · `/mode` | Switch the LLM model · set the acpx mode |
141
+ | `/replymode stream\|verbose\|final` | Change how replies stream |
142
+ | `/lt <time> <message>` | Schedule a one-time future message ([/later](./docs/later-command.md)) |
143
+ | `/dg <agent> <task>` | Delegate a subtask to another agent |
144
+ | `/pm set read` · `/config set <path> <value>` | Permissions · whitelisted config |
345
145
 
346
- ```text
347
- /ss codex -d /absolute/path/to/repo
348
- /use <alias>
349
- /cancel
350
- ```
146
+ ## Multi-agent orchestration & MCP
351
147
 
352
- To attach to an existing native session of a local agent such as Codex, use `/ssn codex -d /absolute/path/to/repo`; for full semantics see [docs/native-sessions.md](./docs/native-sessions.md).
148
+ The current session acts as the coordinator; delegated subtasks (`/dg`, `/tasks`,
149
+ `/task approve`) run as independent worker sessions and need human confirmation by
150
+ default. External MCP hosts such as Codex or Claude Code can drive xacpx's
151
+ orchestration directly by configuring `xacpx mcp-stdio` as a stdio MCP server
152
+ (`delegate_request` / `delegate_batch` support MCP Tasks).
353
153
 
354
- ### Scheduled tasks (/later)
355
-
356
- Have the agent automatically receive a message at some point in the future. **By default it runs in a temporary session created just for that task** (inheriting the agent and workspace of the current session at creation time, with a fresh conversation history, destroyed once finished); adding `--bind` sends it to the current session bound at creation time. When the time comes, the message is delivered as a normal prompt and the result is pushed back to the original chat.
357
-
358
- | Command | Description |
359
- |------|------|
360
- | `/lt <time> <message>` | Create a scheduled task (runs in a temporary session by default; `/later` is a synonym) |
361
- | `/lt --bind <time> <message>` | Send to the current session instead |
362
- | `/lt list` | List globally pending tasks |
363
- | `/lt cancel <id>` | Cancel a pending task |
364
-
365
- The most common examples:
366
-
367
- ```text
368
- /lt in 2h check whether CI passes # temporary session (default)
369
- /lt --bind tomorrow 09:00 review the PR # bound to the current session
370
- /lt list
371
- ```
372
-
373
- Notes:
374
-
375
- - Runs in a temporary session by default; `--bind` binds to the current session. The default mode can be changed via the config `later.defaultMode` (`temp` / `bind`, default `temp`)
376
- - Only one-time tasks are supported; the time must be more than 10 seconds and within 7 days from now
377
- - The time format is a fixed whitelist (relative time / today·tomorrow·day-after-tomorrow / weekday + time); natural language is not supported
378
- - In normal conversation, the agent can also create, list, and cancel scheduled tasks via the current session's internal tools (`scheduled_create` / `scheduled_list` / `scheduled_cancel`); routing and permissions are resolved by the daemon from the current chat session, and the external `mcp-stdio` does not expose these tools
379
- - You can also manage pending tasks from the terminal with `xacpx later list` / `xacpx later cancel <id>`; the CLI only lists and cancels, it does not create scheduled tasks
380
- - For full time formats, temporary/bound modes, task status, and limits, see [docs/later-command.md](./docs/later-command.md)
381
-
382
- ### Config and permissions
383
-
384
- | Command | Description |
385
- |------|------|
386
- | `/config` | Show the config paths that can be changed via chat commands |
387
- | `/config set <path> <value>` | Change a whitelisted config item |
388
- | `/pm` / `/permission` | Show the current permission mode |
389
- | `/pm set allow` | Switch to `approve-all` |
390
- | `/pm set read` | Switch to `approve-reads` |
391
- | `/pm set deny` | Switch to `deny-all` |
392
- | `/pm auto` | Show the current non-interactive permission policy |
393
- | `/pm auto deny` | Switch to `deny` |
394
- | `/pm auto fail` | Switch to `fail` |
395
-
396
- The most common examples:
397
-
398
- ```text
399
- /config set wechat.replyMode final
400
- /pm set read
401
- /pm auto deny
402
- ```
403
-
404
- > `/config set language en` (or `zh`) switches the xacpx interface language; it otherwise follows your system locale. See [docs/config-reference.md](./docs/config-reference.md).
405
-
406
- ### Multi-agent orchestration
407
-
408
- The README keeps only the most common user-facing commands.
409
-
410
- | Command | Description |
411
- |------|------|
412
- | `/dg <agent> <task>` | Quickly delegate a subtask |
413
- | `/tasks` | List tasks under the current main line |
414
- | `/task <id>` | Show details of a single task |
415
- | `/task approve <id>` | Approve a `needs_confirmation` task |
416
- | `/task cancel <id>` | Cancel a task; cancelling a not-yet-approved task is equivalent to rejecting it |
417
-
418
- The most common examples:
419
-
420
- ```text
421
- /dg claude review the 3 high-risk points of the current plan
422
- /tasks
423
- /task approve task_123
424
- ```
425
-
426
- Notes:
427
-
428
- - The current session is the coordinator session
429
- - What gets delegated out are independent subtask sessions
430
- - Delegation requests initiated by the agent require human confirmation by default
431
- - If you're using an external MCP host (Codex / Claude Code), use `delegate_batch` to dispatch multiple parallel subtasks at once: pass a `tasks` array, a group is created automatically under the hood, and all results are injected back at once with no need to maintain a groupId manually
432
-
433
- If you want to first understand when to delegate and when to dispatch multiple subtasks in parallel, see:
434
-
435
- - [docs/xacpx-group-usage-guide.md](./docs/xacpx-group-usage-guide.md)
436
-
437
-
438
- ### MCP integration: external coordinator
439
-
440
- If you want external MCP hosts such as Codex or Claude Code to use xacpx's multi-agent orchestration directly, you can configure `xacpx mcp-stdio` as a stdio MCP server.
441
-
442
- `delegate_request` supports MCP Tasks: a host that supports this capability can make the delegation request return a native task handle immediately, then get status, results, or cancel the task via `tasks/get` / `tasks/result` / `tasks/cancel`; the worker's `[PROGRESS] ...` output shows up in the `statusMessage` of `tasks/get` / `tasks/list`; in the `input_required` state, `tasks/result` returns a next-step hint and ends this result stream rather than blocking for a long time; after the client calls tools such as `task_get` / `task_approve` / `coordinator_answer_question` per the hint, it continues polling `tasks/get` / `tasks/result`. A host that does not support MCP Tasks can still use the compatibility tools `task_get` / `task_list` / `task_watch` / `task_cancel`.
443
-
444
- The natural-language creation tool for scheduled tasks is an internal capability of the xacpx current session and does not appear in the external `xacpx mcp-stdio` tool list.
445
-
446
- Start the daemon first:
447
-
448
- ```bash
449
- xacpx start
450
- ```
451
-
452
- We recommend keeping the MCP config simple and not binding a workspace in the launch arguments:
453
-
454
- ```json
455
- {
456
- "mcpServers": {
457
- "xacpx": {
458
- "command": "xacpx",
459
- "args": ["mcp-stdio"]
460
- }
461
- }
462
- }
463
- ```
464
-
465
- When an external host calls `delegate_request`, pass `workingDirectory`, and xacpx will make the delegated worker work in that directory:
466
-
467
- ```json
468
- {
469
- "targetAgent": "claude",
470
- "task": "review the risks of this change",
471
- "workingDirectory": "/absolute/path/to/your/repo"
472
- }
473
- ```
474
-
475
- On Windows, if the MCP host won't resolve a `command` with arguments for you, put `node.exe` in `command` and the xacpx script and arguments in `args`:
476
-
477
- ```json
478
- {
479
- "type": "stdio",
480
- "command": "C:\\Program Files\\nodejs\\node.exe",
481
- "args": [
482
- "C:\\path\\to\\xacpx\\dist\\cli.js",
483
- "mcp-stdio"
484
- ]
485
- }
486
- ```
487
-
488
- For more identity rules, `workingDirectory` semantics, the tool list, flow diagrams, and troubleshooting, see [docs/external-mcp.md](./docs/external-mcp.md).
154
+ - When to delegate vs. open a parallel group: [xacpx-group-usage-guide.md](./docs/xacpx-group-usage-guide.md)
155
+ - External MCP setup, identity rules, tool list, troubleshooting: [external-mcp.md](./docs/external-mcp.md)
489
156
 
490
157
  ## Common scenarios
491
158
 
492
- ### Keep watching a local project from your phone
493
-
494
159
  ```text
160
+ # Keep watching a local project from your phone
495
161
  /ss codex -d /absolute/path/to/backend
496
162
  take a look at today's API timeout issue
497
- ```
498
-
499
- ### Switch between two projects in the same chat
500
163
 
501
- ```text
164
+ # Switch between two projects in the same chat
502
165
  /ss codex -d /absolute/path/to/backend
503
166
  /ss new codex -d /absolute/path/to/frontend
504
167
  /ss
505
168
  /use backend:codex
506
- /use frontend:codex
507
- ```
508
-
509
- ### Attach to an existing local Codex native session
510
169
 
511
- ```text
170
+ # Attach to an existing local Codex native session
512
171
  /ssn codex -d /absolute/path/to/backend
513
172
  /ssn 1
514
173
  ```
515
174
 
516
- For more filtering, aliases, and troubleshooting, see [docs/native-sessions.md](./docs/native-sessions.md).
517
-
518
175
  ## Self-hosted relay hub (optional)
519
176
 
520
- If you run several xacpx instances and want to drive them all from one browser dashboard, you can self-host the **relay hub**. Each instance dials out to the hub over WebSocket and registers; you log in to a multi-tenant web dashboard and manage every instance's sessions — chat, scheduled tasks, and orchestration — from one place. Streaming agent replies render as markdown, and the layout works on mobile.
521
-
522
- The hub ships as an npm package (`@ganglion/xacpx-relay`) with the dashboard **bundled in** — no separate build. It serves everything on a single port (HTTP API + dashboard + the instance WebSocket gateway), and authentication is a single **access token** used for both web login and connector pairing.
177
+ If you run several xacpx instances and want to drive them all from one browser dashboard, you can self-host the **relay hub**. Each instance dials out to the hub over WebSocket and registers; you log in to a multi-tenant web dashboard and manage every instance's sessions — chat, scheduled tasks, and orchestration — from one place. The hub ships as an npm package (`@ganglion/xacpx-relay`) with the dashboard **bundled in**, served on a single port, with a single **access token** for both web login and connector pairing.
523
178
 
524
179
  ```bash
525
- # 1. On the hub host: install (dashboard is bundled — nothing else to build)
526
180
  npm i -g @ganglion/xacpx-relay
181
+ xacpx-relay add token # prints the access token once
182
+ xacpx-relay start # defaults: --host 0.0.0.0 --http-port 8787
527
183
 
528
- # 2. Mint an access token (DB auto-created at ~/.xacpx-relay/relay.db)
529
- xacpx-relay add token
530
- # → prints the token once; use it to log into the dashboard AND to pair connectors
531
-
532
- # 3. Start the hub (defaults: --host 0.0.0.0 --http-port 8787, dashboard auto-detected)
533
- xacpx-relay start
534
-
535
- # 4. On each instance host: add the connector channel and point it at the hub
184
+ # On each instance host:
536
185
  xacpx plugin add @ganglion/xacpx-channel-relay # requires xacpx >= 0.11.0
537
186
  xacpx channel add relay --url wss://relay.example.com --token <access-token> --name my-box
538
187
  xacpx restart
539
188
  ```
540
189
 
541
- In production, terminate TLS at a reverse proxy in front of the single port and have instances dial `wss://`. There's no `stop`/`status` subcommand — manage the process with systemd/pm2/Docker (`Ctrl-C`/`SIGTERM` to stop); update with `xacpx-relay update`.
542
-
543
- Full walkthrough — pairing instances, TLS/reverse-proxy, systemd, backups, troubleshooting: **[Self-Hosting the Relay Hub](https://gadzan.github.io/xacpx/guide/relay-self-hosting)** (or [docs/relay-deployment.md](./docs/relay-deployment.md) for the terse runbook).
190
+ Full walkthrough — pairing, TLS/reverse-proxy, systemd, backups, troubleshooting: **[Self-Hosting the Relay Hub](https://gadzan.github.io/xacpx/guide/relay-self-hosting)** (or [relay-deployment.md](./docs/relay-deployment.md) for the terse runbook).
544
191
 
545
192
  ## Config and runtime files
546
193
 
547
- Default file locations:
548
-
549
194
  - Config file: `~/.xacpx/config.json`
550
195
  - State file: `~/.xacpx/state.json`
551
196
  - Runtime log: `~/.xacpx/runtime/app.log`
552
197
 
553
- More runtime files are placed under `~/.xacpx/runtime/`.
554
-
555
- ## FAQ
556
-
557
- ### What if `/ss new` fails?
558
-
559
- If session creation fails in WeChat, the most common cause is not a wrong `xacpx` command format, but that the underlying session was not created successfully.
560
-
561
- You can try these two steps first:
562
-
563
- 1. Confirm in the terminal that the current project directory and the agent itself work
564
- 2. If you're familiar with `acpx`, manually create a session first, then attach to it from WeChat
565
-
566
- For example, you can create a session locally first:
567
-
568
- ```bash
569
- ./node_modules/.bin/acpx --verbose --cwd /absolute/workspace/path codex sessions new --name existing-demo
570
- ```
571
-
572
- Then attach to it from WeChat:
573
-
574
- ```text
575
- /ss attach demo -a codex --ws backend --name existing-demo
576
- ```
577
-
578
- ### What is the `<id>` in `/mode <id>`?
579
-
580
- The valid values for `/mode` depend on the agent you're currently using; `xacpx` does not normalize these values for you.
581
-
582
- Currently the more clearly known values are:
583
-
584
- - `codex`: `plan`
585
- - `cursor`: `agent`, `plan`, `ask`
586
-
587
- If you're unsure whether a value works, check the corresponding agent's docs first; if you get it wrong, you'll usually get an error such as an invalid argument.
198
+ More runtime files are placed under `~/.xacpx/runtime/`. For the full config field reference, see [config-reference.md](./docs/config-reference.md).
588
199
 
589
200
  ## Running from source
590
201
 
591
- If you're using the repo source directly:
592
-
593
202
  ```bash
594
203
  bun install
595
204
  bun run login
596
205
  bun run dev
597
206
  ```
598
207
 
599
- ## More docs
600
-
601
- If what you're about to do is one of the following, you can continue from here:
602
-
603
- ### Installation and configuration
604
-
605
- - Want to configure WeChat, Feishu, Yuanbao, or a third-party plugin channel: [docs/channel-management.md](./docs/channel-management.md)
606
- - Want to write your own channel plugin: [docs/plugin-development.md](./docs/plugin-development.md)
607
- - Want the full config field reference: [docs/config-reference.md](./docs/config-reference.md)
608
- - Want to change config from WeChat: [docs/config-command.md](./docs/config-command.md)
208
+ For development, debugging, and contribution details, see [developments.md](./docs/developments.md).
609
209
 
610
- ### Everyday use
611
-
612
- - Want the full chat-command reference: [docs/commands.md](./docs/commands.md)
613
- - Want to schedule a one-time future message with scheduled tasks (`/later`): [docs/later-command.md](./docs/later-command.md)
614
- - Want to understand when to delegate and when to open a group: [docs/xacpx-group-usage-guide.md](./docs/xacpx-group-usage-guide.md)
615
-
616
- ### Troubleshooting and verification
617
-
618
- - Want to run tests or understand the test layout: [docs/testing.md](./docs/testing.md)
619
-
620
- ### Development and contribution
210
+ ## More docs
621
211
 
622
- - Want to develop, debug, or contribute from source: [docs/developments.md](./docs/developments.md)
212
+ **Install & configure**
213
+ - [channel-management.md](./docs/channel-management.md) — configure WeChat / Feishu / Yuanbao / third-party channels
214
+ - [plugin-development.md](./docs/plugin-development.md) — write your own channel plugin
215
+ - [config-reference.md](./docs/config-reference.md) — full config field reference
216
+ - [config-command.md](./docs/config-command.md) — change config from chat
217
+
218
+ **Everyday use**
219
+ - [cli-reference.md](./docs/cli-reference.md) — full terminal CLI reference
220
+ - [commands.md](./docs/commands.md) — full chat-command reference
221
+ - [later-command.md](./docs/later-command.md) — scheduled tasks (`/later`)
222
+ - [native-sessions.md](./docs/native-sessions.md) — attach to a local agent's native session
223
+ - [xacpx-group-usage-guide.md](./docs/xacpx-group-usage-guide.md) — when to delegate vs. open a group
224
+ - [external-mcp.md](./docs/external-mcp.md) — external MCP coordinator integration
225
+
226
+ **Troubleshoot & verify**
227
+ - [faq.md](./docs/faq.md) — common questions (`/ss new` fails, `/mode <id>`, …)
228
+ - [doctor-command.md](./docs/doctor-command.md) — `xacpx doctor` diagnostics and `--fix`
229
+ - [testing.md](./docs/testing.md) — test layout and how to run tests
230
+
231
+ **Develop & contribute**
232
+ - [developments.md](./docs/developments.md) — develop, debug, or contribute from source
233
+ - [code-wiki.md](./docs/code-wiki.md) — architecture map
@@ -460,6 +460,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
460
460
  pendingLine: "",
461
461
  formatToolCalls,
462
462
  emittedToolCallIds: new Set,
463
+ toolCalls: new Map,
463
464
  toolEventMode,
464
465
  rawStream,
465
466
  onToolEvent,
@@ -507,7 +508,8 @@ function parseStreamingChunks(state, line) {
507
508
  const wantsStructured = state.toolEventMode === "structured" || state.toolEventMode === "both";
508
509
  const wantsText = (state.toolEventMode === "text" || state.toolEventMode === "both") && state.formatToolCalls;
509
510
  if (wantsStructured && state.onToolEvent) {
510
- const toolEvent = buildToolUseEvent(update);
511
+ const merged = update.toolCallId ? mergeToolCallUpdate(state, update.toolCallId, update) : update;
512
+ const toolEvent = buildToolUseEvent(merged);
511
513
  if (toolEvent)
512
514
  state.onToolEvent(toolEvent);
513
515
  }
@@ -594,6 +596,29 @@ function formatToolCallEvent(update, sessionUpdate) {
594
596
  const statusText = status ? ` (${status})` : "";
595
597
  return `${emoji} ${title}${statusText}${summaryText}`;
596
598
  }
599
+ function isEmptyToolField(v) {
600
+ if (v === undefined || v === null)
601
+ return true;
602
+ if (typeof v === "string")
603
+ return v.trim().length === 0;
604
+ if (Array.isArray(v))
605
+ return v.length === 0;
606
+ if (typeof v === "object")
607
+ return Object.keys(v).length === 0;
608
+ return false;
609
+ }
610
+ function mergeToolCallUpdate(state, toolCallId, update) {
611
+ const prev = state.toolCalls.get(toolCallId) ?? { toolCallId };
612
+ const merged = { ...prev };
613
+ for (const key of ["kind", "title", "rawInput", "content", "rawOutput", "locations", "status"]) {
614
+ const next = update[key];
615
+ if (!isEmptyToolField(next))
616
+ merged[key] = next;
617
+ }
618
+ merged.toolCallId = toolCallId;
619
+ state.toolCalls.set(toolCallId, merged);
620
+ return merged;
621
+ }
597
622
  function buildToolUseEvent(update) {
598
623
  if (!update)
599
624
  return null;
@@ -1945,6 +1970,7 @@ var init_misc = __esm(() => {
1945
1970
  defaultHomeWorkspaceDescription: "Home directory",
1946
1971
  pluginChannelFeishu: "Feishu channel",
1947
1972
  pluginChannelYuanbao: "Tencent Yuanbao channel",
1973
+ pluginChannelRelay: "Relay hub connector (drive this instance from a self-hosted relay hub)",
1948
1974
  pluginChannelInstallHint: (channelType, packageName) => `Channel ${channelType} requires a plugin: xacpx plugin add ${packageName}`,
1949
1975
  orchestrationSuggestion1: "Run /tasks --stuck to locate stuck tasks",
1950
1976
  orchestrationSuggestion2: "/task <id> shows the full timeline to locate errors",
@@ -3041,6 +3067,7 @@ var init_misc2 = __esm(() => {
3041
3067
  defaultHomeWorkspaceDescription: "用户主目录",
3042
3068
  pluginChannelFeishu: "飞书频道",
3043
3069
  pluginChannelYuanbao: "腾讯元宝频道",
3070
+ pluginChannelRelay: "Relay hub 连接器(从自托管 relay hub 遥控这台实例)",
3044
3071
  pluginChannelInstallHint: (channelType, packageName) => `频道 ${channelType} 需要安装插件:xacpx plugin add ${packageName}`,
3045
3072
  orchestrationSuggestion1: "查看 /tasks --stuck 定位卡住的任务",
3046
3073
  orchestrationSuggestion2: "/task <id> 可看完整时间线定位错误点",
package/dist/cli.js CHANGED
@@ -1075,6 +1075,7 @@ var init_misc = __esm(() => {
1075
1075
  defaultHomeWorkspaceDescription: "Home directory",
1076
1076
  pluginChannelFeishu: "Feishu channel",
1077
1077
  pluginChannelYuanbao: "Tencent Yuanbao channel",
1078
+ pluginChannelRelay: "Relay hub connector (drive this instance from a self-hosted relay hub)",
1078
1079
  pluginChannelInstallHint: (channelType, packageName) => `Channel ${channelType} requires a plugin: xacpx plugin add ${packageName}`,
1079
1080
  orchestrationSuggestion1: "Run /tasks --stuck to locate stuck tasks",
1080
1081
  orchestrationSuggestion2: "/task <id> shows the full timeline to locate errors",
@@ -2171,6 +2172,7 @@ var init_misc2 = __esm(() => {
2171
2172
  defaultHomeWorkspaceDescription: "用户主目录",
2172
2173
  pluginChannelFeishu: "飞书频道",
2173
2174
  pluginChannelYuanbao: "腾讯元宝频道",
2175
+ pluginChannelRelay: "Relay hub 连接器(从自托管 relay hub 遥控这台实例)",
2174
2176
  pluginChannelInstallHint: (channelType, packageName) => `频道 ${channelType} 需要安装插件:xacpx plugin add ${packageName}`,
2175
2177
  orchestrationSuggestion1: "查看 /tasks --stuck 定位卡住的任务",
2176
2178
  orchestrationSuggestion2: "/task <id> 可看完整时间线定位错误点",
@@ -19512,6 +19514,12 @@ var init_known_plugins = __esm(() => {
19512
19514
  channels: ["yuanbao"],
19513
19515
  descriptionKey: "pluginChannelYuanbao",
19514
19516
  official: true
19517
+ },
19518
+ {
19519
+ packageName: "@ganglion/xacpx-channel-relay",
19520
+ channels: ["relay"],
19521
+ descriptionKey: "pluginChannelRelay",
19522
+ official: true
19515
19523
  }
19516
19524
  ];
19517
19525
  });
@@ -21780,8 +21788,9 @@ async function buildCoordinatorPrompt(input) {
21780
21788
  `));
21781
21789
  }
21782
21790
  if (input.userText) {
21783
- sections.push([t().coordinatorPrompt.userMessageLabel, input.userText].join(`
21784
- `));
21791
+ const hasOrchestrationContext = sections.length > 0;
21792
+ sections.push(hasOrchestrationContext ? [t().coordinatorPrompt.userMessageLabel, input.userText].join(`
21793
+ `) : input.userText);
21785
21794
  }
21786
21795
  const claimHumanReply = shouldBind && input.chatKey && activePackage?.awaitingReplyMessageId ? {
21787
21796
  coordinatorSession: input.coordinatorSession,
@@ -22380,6 +22389,9 @@ async function handleSessionArchive(context, chatKey, alias, archive) {
22380
22389
  async function promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage, onCommands) {
22381
22390
  if (session3.archived) {
22382
22391
  await context.sessions.setArchived(session3.alias, false);
22392
+ if (!await context.lifecycle.checkTransportSession(session3)) {
22393
+ await context.lifecycle.ensureTransportSession(session3, reply, perfSpan);
22394
+ }
22383
22395
  }
22384
22396
  const effectiveReplyMode = resolveEffectiveReplyMode(context.config, chatKey, session3.replyMode);
22385
22397
  if (!session3.replyMode)
@@ -25023,17 +25035,6 @@ class CommandRouter {
25023
25035
  try {
25024
25036
  await this.transport.cancel(session3);
25025
25037
  } catch {}
25026
- if (this.transport.removeSession) {
25027
- try {
25028
- await this.transport.removeSession(session3);
25029
- } catch (error2) {
25030
- await this.logger.error("session.archive_close_failed", "failed to close acpx session on archive", {
25031
- alias: internalAlias,
25032
- transportSession: session3.transportSession,
25033
- message: error2 instanceof Error ? error2.message : String(error2)
25034
- });
25035
- }
25036
- }
25037
25038
  }
25038
25039
  await this.sessions.setArchived(internalAlias, true);
25039
25040
  }
@@ -31753,6 +31754,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31753
31754
  pendingLine: "",
31754
31755
  formatToolCalls,
31755
31756
  emittedToolCallIds: new Set,
31757
+ toolCalls: new Map,
31756
31758
  toolEventMode,
31757
31759
  rawStream,
31758
31760
  onToolEvent,
@@ -31800,7 +31802,8 @@ function parseStreamingChunks(state, line) {
31800
31802
  const wantsStructured = state.toolEventMode === "structured" || state.toolEventMode === "both";
31801
31803
  const wantsText = (state.toolEventMode === "text" || state.toolEventMode === "both") && state.formatToolCalls;
31802
31804
  if (wantsStructured && state.onToolEvent) {
31803
- const toolEvent = buildToolUseEvent(update);
31805
+ const merged = update.toolCallId ? mergeToolCallUpdate(state, update.toolCallId, update) : update;
31806
+ const toolEvent = buildToolUseEvent(merged);
31804
31807
  if (toolEvent)
31805
31808
  state.onToolEvent(toolEvent);
31806
31809
  }
@@ -31887,6 +31890,29 @@ function formatToolCallEvent(update, sessionUpdate) {
31887
31890
  const statusText = status ? ` (${status})` : "";
31888
31891
  return `${emoji2} ${title}${statusText}${summaryText}`;
31889
31892
  }
31893
+ function isEmptyToolField(v) {
31894
+ if (v === undefined || v === null)
31895
+ return true;
31896
+ if (typeof v === "string")
31897
+ return v.trim().length === 0;
31898
+ if (Array.isArray(v))
31899
+ return v.length === 0;
31900
+ if (typeof v === "object")
31901
+ return Object.keys(v).length === 0;
31902
+ return false;
31903
+ }
31904
+ function mergeToolCallUpdate(state, toolCallId, update) {
31905
+ const prev = state.toolCalls.get(toolCallId) ?? { toolCallId };
31906
+ const merged = { ...prev };
31907
+ for (const key of ["kind", "title", "rawInput", "content", "rawOutput", "locations", "status"]) {
31908
+ const next = update[key];
31909
+ if (!isEmptyToolField(next))
31910
+ merged[key] = next;
31911
+ }
31912
+ merged.toolCallId = toolCallId;
31913
+ state.toolCalls.set(toolCallId, merged);
31914
+ return merged;
31915
+ }
31890
31916
  function buildToolUseEvent(update) {
31891
31917
  if (!update)
31892
31918
  return null;
@@ -34120,6 +34146,57 @@ var init_agent_catalog = __esm(() => {
34120
34146
  };
34121
34147
  });
34122
34148
 
34149
+ // src/config/config-watcher.ts
34150
+ import { watch } from "node:fs";
34151
+ import { basename as basename3, dirname as dirname12 } from "node:path";
34152
+ function startConfigWatcher(options) {
34153
+ const { configPath, onChange, debounceMs = 250, logger: logger2 } = options;
34154
+ const watchFactory = options.watchFactory ?? watch;
34155
+ const dir = dirname12(configPath);
34156
+ const target = basename3(configPath);
34157
+ let timer;
34158
+ let watcher;
34159
+ const fire = () => {
34160
+ timer = undefined;
34161
+ try {
34162
+ onChange();
34163
+ } catch (error2) {
34164
+ logger2?.error("config.watch.callback_failed", "config watch callback threw", {
34165
+ error: error2 instanceof Error ? error2.message : String(error2)
34166
+ });
34167
+ }
34168
+ };
34169
+ try {
34170
+ watcher = watchFactory(dir, { persistent: false }, (_event, filename) => {
34171
+ if (filename !== null && basename3(filename.toString()) !== target)
34172
+ return;
34173
+ if (timer)
34174
+ clearTimeout(timer);
34175
+ timer = setTimeout(fire, debounceMs);
34176
+ });
34177
+ watcher.on("error", (error2) => {
34178
+ logger2?.error("config.watch.error", "config watcher errored", {
34179
+ error: error2 instanceof Error ? error2.message : String(error2)
34180
+ });
34181
+ });
34182
+ } catch (error2) {
34183
+ logger2?.error("config.watch.start_failed", "could not start config watcher", {
34184
+ error: error2 instanceof Error ? error2.message : String(error2)
34185
+ });
34186
+ }
34187
+ return {
34188
+ close: () => {
34189
+ if (timer) {
34190
+ clearTimeout(timer);
34191
+ timer = undefined;
34192
+ }
34193
+ watcher?.close();
34194
+ watcher = undefined;
34195
+ }
34196
+ };
34197
+ }
34198
+ var init_config_watcher = () => {};
34199
+
34123
34200
  // src/main.ts
34124
34201
  var exports_main = {};
34125
34202
  __export(exports_main, {
@@ -34131,7 +34208,7 @@ __export(exports_main, {
34131
34208
  });
34132
34209
  import { randomUUID as randomUUID3 } from "node:crypto";
34133
34210
  import { homedir as homedir13 } from "node:os";
34134
- import { dirname as dirname12, join as join22 } from "node:path";
34211
+ import { dirname as dirname13, join as join22 } from "node:path";
34135
34212
  import { fileURLToPath as fileURLToPath5 } from "node:url";
34136
34213
  function startProgressHeartbeat(orchestration3, config4, logger2, channel) {
34137
34214
  const thresholdSeconds = config4.orchestration.progressHeartbeatSeconds;
@@ -34661,15 +34738,37 @@ async function buildApp(paths, deps = {}) {
34661
34738
  create: async (name, cwd, description) => {
34662
34739
  const updated = await configStore.upsertWorkspace(name, cwd, description);
34663
34740
  replaceRuntimeConfig(config4, updated);
34741
+ controlEvents.emit({ type: "workspaces-changed" });
34664
34742
  return { name, cwd, ...description ? { description } : {} };
34665
34743
  },
34666
34744
  remove: async (name) => {
34667
34745
  const updated = await configStore.removeWorkspace(name);
34668
34746
  replaceRuntimeConfig(config4, updated);
34747
+ controlEvents.emit({ type: "workspaces-changed" });
34669
34748
  }
34670
34749
  },
34671
34750
  uploadStore
34672
34751
  });
34752
+ const workspaceSignature = (cfg) => JSON.stringify(Object.keys(cfg.workspaces).sort().map((name) => {
34753
+ const ws = cfg.workspaces[name];
34754
+ return [name, ws.cwd, ws.description ?? ""];
34755
+ }));
34756
+ const configWatcher = startConfigWatcher({
34757
+ configPath: paths.configPath,
34758
+ logger: logger2,
34759
+ onChange: () => {
34760
+ const before = workspaceSignature(config4);
34761
+ reloadRuntimeConfig().then(() => {
34762
+ if (workspaceSignature(config4) !== before) {
34763
+ controlEvents.emit({ type: "workspaces-changed" });
34764
+ }
34765
+ }).catch((error2) => {
34766
+ logger2.error("config.reload_failed", "failed to reload config after file change", {
34767
+ error: error2 instanceof Error ? error2.message : String(error2)
34768
+ });
34769
+ });
34770
+ }
34771
+ });
34673
34772
  const scheduledScheduler = new ScheduledTaskScheduler(scheduledService, {
34674
34773
  dispatchTask: buildScheduledDispatchTask({
34675
34774
  getSession: (alias) => sessions.getSession(alias),
@@ -34738,6 +34837,7 @@ async function buildApp(paths, deps = {}) {
34738
34837
  reapStaleQueueOwners: () => reapWarmQueueOwners("startup"),
34739
34838
  dispose: async () => {
34740
34839
  scheduledScheduler.stop();
34840
+ configWatcher.close();
34741
34841
  clearInterval(uploadCleanupInterval);
34742
34842
  if (progressHeartbeatInterval !== undefined) {
34743
34843
  clearInterval(progressHeartbeatInterval);
@@ -34799,7 +34899,7 @@ async function main() {
34799
34899
  }
34800
34900
  }
34801
34901
  async function prepareChannelMedia(configPath, config4) {
34802
- const runtimeDir = join22(dirname12(configPath), "runtime");
34902
+ const runtimeDir = join22(dirname13(configPath), "runtime");
34803
34903
  const mediaRootDir = join22(runtimeDir, "media");
34804
34904
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
34805
34905
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -34814,7 +34914,7 @@ function resolveRuntimePaths() {
34814
34914
  throw new Error("Unable to resolve the current user home directory");
34815
34915
  }
34816
34916
  const configPath = coreEnv("CONFIG") ?? join22(coreHomeDir(home), "config.json");
34817
- const runtimeDir = join22(dirname12(configPath), "runtime");
34917
+ const runtimeDir = join22(dirname13(configPath), "runtime");
34818
34918
  return {
34819
34919
  configPath,
34820
34920
  statePath: coreEnv("STATE") ?? join22(coreHomeDir(home), "state.json"),
@@ -34829,12 +34929,12 @@ function resolveBridgeEntryPath() {
34829
34929
  return fileURLToPath5(new URL("./bridge/bridge-main.ts", import.meta.url));
34830
34930
  }
34831
34931
  function resolveAppLogPath(configPath) {
34832
- const rootDir = dirname12(configPath);
34932
+ const rootDir = dirname13(configPath);
34833
34933
  const runtimeDir = join22(rootDir, "runtime");
34834
34934
  return join22(runtimeDir, "app.log");
34835
34935
  }
34836
34936
  function resolvePerfLogPath(configPath) {
34837
- const rootDir = dirname12(configPath);
34937
+ const rootDir = dirname13(configPath);
34838
34938
  const runtimeDir = join22(rootDir, "runtime");
34839
34939
  return join22(runtimeDir, "perf.log");
34840
34940
  }
@@ -34883,6 +34983,7 @@ var init_main = __esm(async () => {
34883
34983
  init_control_service();
34884
34984
  init_upload_store();
34885
34985
  init_agent_catalog();
34986
+ init_config_watcher();
34886
34987
  init_perf_tracer();
34887
34988
  init_bootstrap();
34888
34989
  init_i18n();
@@ -35245,7 +35346,7 @@ var init_daemon_check = __esm(() => {
35245
35346
 
35246
35347
  // src/doctor/checks/logs-check.ts
35247
35348
  import { stat as stat5, readdir as readdir7 } from "node:fs/promises";
35248
- import { basename as basename3, join as join24 } from "node:path";
35349
+ import { basename as basename4, join as join24 } from "node:path";
35249
35350
  import { homedir as homedir15 } from "node:os";
35250
35351
  async function checkLogs(options = {}) {
35251
35352
  const home = options.home ?? process.env.HOME ?? homedir15();
@@ -35275,7 +35376,7 @@ async function checkLogs(options = {}) {
35275
35376
  `error: ${formatError6(error2)}`
35276
35377
  ]);
35277
35378
  }
35278
- const baseNames = [basename3(paths.appLog), basename3(paths.stdoutLog), basename3(paths.stderrLog)];
35379
+ const baseNames = [basename4(paths.appLog), basename4(paths.stdoutLog), basename4(paths.stderrLog)];
35279
35380
  const tracked = new Set(baseNames);
35280
35381
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
35281
35382
  const files = [];
@@ -35606,7 +35707,7 @@ var init_plugin_check = __esm(async () => {
35606
35707
  // src/doctor/checks/runtime-check.ts
35607
35708
  import { constants } from "node:fs";
35608
35709
  import { access as access4, stat as stat6 } from "node:fs/promises";
35609
- import { dirname as dirname13 } from "node:path";
35710
+ import { dirname as dirname14 } from "node:path";
35610
35711
  import { homedir as homedir17 } from "node:os";
35611
35712
  async function checkRuntime(options = {}) {
35612
35713
  const home = options.home ?? process.env.HOME ?? homedir17();
@@ -35762,7 +35863,7 @@ async function checkFileCreatable(label, path17, probe, platform) {
35762
35863
  detail: `${label}: ${path17} (unusable: ${formatError9(error2)})`
35763
35864
  };
35764
35865
  }
35765
- const parentCheck = await checkCreatableAncestorDirectory(dirname13(path17), probe, platform);
35866
+ const parentCheck = await checkCreatableAncestorDirectory(dirname14(path17), probe, platform);
35766
35867
  if (!parentCheck.ok) {
35767
35868
  return {
35768
35869
  ok: false,
@@ -35798,7 +35899,7 @@ async function checkCreatableAncestorDirectory(path17, probe, platform) {
35798
35899
  blockingPath: path17
35799
35900
  };
35800
35901
  }
35801
- const parent = dirname13(path17);
35902
+ const parent = dirname14(path17);
35802
35903
  if (parent === path17) {
35803
35904
  return {
35804
35905
  ok: false,
@@ -36583,7 +36684,7 @@ var init_doctor2 = __esm(async () => {
36583
36684
  init_core_home();
36584
36685
  import { randomUUID as randomUUID4 } from "node:crypto";
36585
36686
  import { homedir as homedir19 } from "node:os";
36586
- import { dirname as dirname14, join as join26, sep as sep2 } from "node:path";
36687
+ import { dirname as dirname15, join as join26, sep as sep2 } from "node:path";
36587
36688
  import { fileURLToPath as fileURLToPath7 } from "node:url";
36588
36689
 
36589
36690
  // src/runtime/migrate-core-home.ts
@@ -53212,7 +53313,7 @@ function safeDaemonLogPaths() {
53212
53313
  const configPath = resolveConfigPathForCurrentEnv();
53213
53314
  const paths = resolveDaemonPathsForCurrentConfig();
53214
53315
  return {
53215
- appLog: join26(dirname14(configPath), "runtime", "app.log"),
53316
+ appLog: join26(dirname15(configPath), "runtime", "app.log"),
53216
53317
  stderrLog: paths.stderrLog
53217
53318
  };
53218
53319
  } catch {
@@ -54,6 +54,8 @@ export type ControlEvent = {
54
54
  cancelled?: boolean;
55
55
  } | {
56
56
  type: "sessions-changed";
57
+ } | {
58
+ type: "workspaces-changed";
57
59
  } | {
58
60
  type: "scheduled-changed";
59
61
  chatKey: string;
@@ -809,6 +809,7 @@ export interface MiscMessages {
809
809
  defaultHomeWorkspaceDescription: string;
810
810
  pluginChannelFeishu: string;
811
811
  pluginChannelYuanbao: string;
812
+ pluginChannelRelay: string;
812
813
  pluginChannelInstallHint: (channelType: string, packageName: string) => string;
813
814
  orchestrationSuggestion1: string;
814
815
  orchestrationSuggestion2: string;
@@ -1050,6 +1050,7 @@ var init_misc = __esm(() => {
1050
1050
  defaultHomeWorkspaceDescription: "Home directory",
1051
1051
  pluginChannelFeishu: "Feishu channel",
1052
1052
  pluginChannelYuanbao: "Tencent Yuanbao channel",
1053
+ pluginChannelRelay: "Relay hub connector (drive this instance from a self-hosted relay hub)",
1053
1054
  pluginChannelInstallHint: (channelType, packageName) => `Channel ${channelType} requires a plugin: xacpx plugin add ${packageName}`,
1054
1055
  orchestrationSuggestion1: "Run /tasks --stuck to locate stuck tasks",
1055
1056
  orchestrationSuggestion2: "/task <id> shows the full timeline to locate errors",
@@ -2146,6 +2147,7 @@ var init_misc2 = __esm(() => {
2146
2147
  defaultHomeWorkspaceDescription: "用户主目录",
2147
2148
  pluginChannelFeishu: "飞书频道",
2148
2149
  pluginChannelYuanbao: "腾讯元宝频道",
2150
+ pluginChannelRelay: "Relay hub 连接器(从自托管 relay hub 遥控这台实例)",
2149
2151
  pluginChannelInstallHint: (channelType, packageName) => `频道 ${channelType} 需要安装插件:xacpx plugin add ${packageName}`,
2150
2152
  orchestrationSuggestion1: "查看 /tasks --stuck 定位卡住的任务",
2151
2153
  orchestrationSuggestion2: "/task <id> 可看完整时间线定位错误点",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",