@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 +74 -463
- package/dist/bridge/bridge-main.js +28 -1
- package/dist/cli.js +127 -26
- package/dist/control/control-event-bus.d.ts +2 -0
- package/dist/i18n/types.d.ts +1 -0
- package/dist/plugin-api.js +2 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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**:
|
|
115
|
-
4. **Check status or 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
|
-
###
|
|
99
|
+
### Reply modes
|
|
136
100
|
|
|
137
|
-
`xacpx` supports three
|
|
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;
|
|
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
|
-
|
|
116
|
+
## Command cheat sheet
|
|
153
117
|
|
|
154
|
-
|
|
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
|
-
|
|
120
|
+
**Terminal (on the host):**
|
|
164
121
|
|
|
165
122
|
| Command | Description |
|
|
166
123
|
|------|------|
|
|
167
|
-
| `xacpx login` | Log in
|
|
168
|
-
| `xacpx
|
|
169
|
-
| `xacpx
|
|
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
|
|
181
|
-
| `xacpx
|
|
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
|
-
|
|
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
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
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
|
|
333
|
-
| `/
|
|
334
|
-
| `/
|
|
335
|
-
| `/
|
|
336
|
-
| `/
|
|
337
|
-
| `/
|
|
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
|
-
|
|
347
|
-
/ss codex -d /absolute/path/to/repo
|
|
348
|
-
/use <alias>
|
|
349
|
-
/cancel
|
|
350
|
-
```
|
|
146
|
+
## Multi-agent orchestration & MCP
|
|
351
147
|
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 = [
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
53316
|
+
appLog: join26(dirname15(configPath), "runtime", "app.log"),
|
|
53216
53317
|
stderrLog: paths.stderrLog
|
|
53217
53318
|
};
|
|
53218
53319
|
} catch {
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/plugin-api.js
CHANGED
|
@@ -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> 可看完整时间线定位错误点",
|