@muzi3/herdr 0.1.0 → 0.1.2
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 +13 -1
- package/extensions/pi-hot-restart.ts +206 -0
- package/package.json +3 -2
- package/skills/herdr-guide/agents/openai.yaml +0 -3
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|------|------|------|
|
|
7
7
|
| `herdr-guide` | user-invoked 向导 | 引导用户理解、安装、配置与排查 Herdr(`herdr.dev/agent-guide.md`) |
|
|
8
8
|
| `herdr-agent-state` | 扩展 | 通过 Herdr 的 pane socket 上报 agent 状态(working / blocked / idle)及会话引用,供 Herdr 面板展示 |
|
|
9
|
+
| `pi-hot-restart` | 扩展 | 提供 `/pi-hot-restart` 命令:批量重启当前 herdr 里的所有 pi 窗口(跳过 working),并恢复到原 session |
|
|
9
10
|
|
|
10
11
|
## 来源与许可
|
|
11
12
|
|
|
@@ -18,6 +19,8 @@ Herdr 以 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 许可发布
|
|
|
18
19
|
|
|
19
20
|
> 扩展源文件由 Herdr 集成管理,重装或更新集成时会覆盖;请在旁边添加自定义 hooks 而非编辑它。
|
|
20
21
|
|
|
22
|
+
`pi-hot-restart.ts` 是本包自带的补充扩展,与官方 `herdr-agent-state` 互不干扰,在 `package.json` 的 `pi.extensions` 中显式声明(因 `extensions/index.ts` 存在时 pi 不会扫描同目录兄弟文件)。
|
|
23
|
+
|
|
21
24
|
## 安装
|
|
22
25
|
|
|
23
26
|
```bash
|
|
@@ -29,6 +32,15 @@ pi install npm:@muzi3/herdr
|
|
|
29
32
|
- 用户询问 Herdr 是什么、怎么安装/配置/使用,或排查 agent 未识别、快捷键失效等问题时,触发 `herdr-guide` 引导(`/herdr-guide`)
|
|
30
33
|
- 触发词:`herdr` / `herdr 安装` / `herdr 配置` / `终端工作区` / `agent 状态检测` 等
|
|
31
34
|
|
|
35
|
+
### 批量重启 pi 窗口
|
|
36
|
+
|
|
37
|
+
在任意 pi 窗口执行 `/pi-hot-restart`,会:
|
|
38
|
+
|
|
39
|
+
1. 列出当前 herdr 里所有 `pi` 窗口;
|
|
40
|
+
2. 跳过 `agent_status === "working"` 的窗口(含当前发起命令的窗口本身);
|
|
41
|
+
3. 弹出确认框,展示将重启的窗口清单与数量;
|
|
42
|
+
4. 对每个目标窗口:向 pi 前台进程组发 SIGTERM 优雅退出(session 落盘),等待退出后用 `pi --session <path|id>` 在原 pane 恢复。
|
|
43
|
+
|
|
32
44
|
### 扩展环境变量
|
|
33
45
|
|
|
34
46
|
扩展需在 Herdr 集成环境中运行,依赖以下环境变量:
|
|
@@ -39,7 +51,7 @@ pi install npm:@muzi3/herdr
|
|
|
39
51
|
| `HERDR_SOCKET_PATH` | Herdr pane socket 路径 |
|
|
40
52
|
| `HERDR_PANE_ID` | 当前 pane id |
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
未设置这些变量时 `herdr-agent-state` 扩展静默跳过。`pi-hot-restart` 通过 PATH 调用 `herdr` CLI(`agent list` / `pane process-info` / `pane run`),不依赖上述变量。
|
|
43
55
|
|
|
44
56
|
## 参考
|
|
45
57
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-hot-restart
|
|
3
|
+
*
|
|
4
|
+
* 提供 `/pi-hot-restart` 命令:批量重启当前 herdr 里的所有 pi 窗口,
|
|
5
|
+
* 每个窗口重启后恢复到原来的 session。
|
|
6
|
+
*
|
|
7
|
+
* 行为约定:
|
|
8
|
+
* - 跳过 agent_status 为 "working" 的窗口,以及当前发起命令的窗口自身
|
|
9
|
+
* (按 `HERDR_PANE_ID` 排除;敲命令时该窗口是 idle,不能只靠状态排除,
|
|
10
|
+
* 否则会把自己的 pi 进程也 SIGTERM 掉,循环当场中断)。
|
|
11
|
+
* - 重启前弹确认框,列出将重启的窗口与数量。
|
|
12
|
+
* - 通过向 pi 前台进程组发 SIGTERM 触发优雅退出(session 落盘),
|
|
13
|
+
* 退出后用 `herdr pane run` 以 `pi --session <path|id>` 重新拉起。
|
|
14
|
+
*
|
|
15
|
+
* 依赖:
|
|
16
|
+
* - 运行在 herdr 集成环境中(HERDR_ENV=1 等),通过 PATH 调用 `herdr` CLI。
|
|
17
|
+
* - `herdr agent list` / `herdr pane process-info` / `herdr pane run` 子命令。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
22
|
+
|
|
23
|
+
const HERDR = "herdr";
|
|
24
|
+
|
|
25
|
+
interface AgentSession {
|
|
26
|
+
kind: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PiAgent {
|
|
31
|
+
agent: string;
|
|
32
|
+
agent_status: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
pane_id: string;
|
|
35
|
+
terminal_title?: string;
|
|
36
|
+
agent_session?: AgentSession;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ForegroundProcess {
|
|
40
|
+
argv0?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ProcInfo {
|
|
44
|
+
foreground_process_group_id?: number;
|
|
45
|
+
foreground_processes?: ForegroundProcess[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface HerdrListResult {
|
|
49
|
+
result?: { agents?: PiAgent[] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface HerdrProcInfoResult {
|
|
53
|
+
result?: { process_info?: ProcInfo };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 执行 herdr CLI,无论退出码如何都返回 stdout/stderr/code,方便解析错误 JSON。 */
|
|
57
|
+
function runHerdr(args: string[]): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
execFile(
|
|
60
|
+
HERDR,
|
|
61
|
+
args,
|
|
62
|
+
{ env: process.env, maxBuffer: 64 * 1024 * 1024 },
|
|
63
|
+
(error, stdout, stderr) => {
|
|
64
|
+
resolve({
|
|
65
|
+
stdout: stdout ?? "",
|
|
66
|
+
stderr: stderr ?? "",
|
|
67
|
+
code: error ? Number((error as NodeJS.ErrnoException).code ?? 1) : 0,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function herdrJson<T>(args: string[]): Promise<T> {
|
|
75
|
+
const { stdout, stderr, code } = await runHerdr(args);
|
|
76
|
+
let parsed: unknown;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(stdout);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error(`herdr ${args.join(" ")} 输出不是 JSON (code=${code}): ${stderr || stdout}`);
|
|
81
|
+
}
|
|
82
|
+
if (code !== 0 && typeof parsed === "object" && parsed !== null && "error" in parsed) {
|
|
83
|
+
throw new Error(`herdr ${args.join(" ")} 失败: ${String((parsed as { error: unknown }).error)}`);
|
|
84
|
+
}
|
|
85
|
+
return parsed as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sleep(ms: number): Promise<void> {
|
|
89
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shellQuote(s: string): string {
|
|
93
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 取当前 herdr 中所有已识别的 pi 窗口。 */
|
|
97
|
+
async function listPiAgents(): Promise<PiAgent[]> {
|
|
98
|
+
const data = await herdrJson<HerdrListResult>(["agent", "list"]);
|
|
99
|
+
return (data?.result?.agents ?? []).filter((a) => a.agent === "pi");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 该 pane 的前台进程组中是否还存在 pi 进程。 */
|
|
103
|
+
async function hasPiForeground(paneId: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
const data = await herdrJson<HerdrProcInfoResult>(["pane", "process-info", "--pane", paneId]);
|
|
106
|
+
const procs = data?.result?.process_info?.foreground_processes ?? [];
|
|
107
|
+
return procs.some((p) => p.argv0 === "pi");
|
|
108
|
+
} catch {
|
|
109
|
+
// 拿不到进程信息时按「已退出」处理,避免卡死等待。
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 向前台进程组发 SIGTERM,只杀 pi + 同组子进程,保留 shell。 */
|
|
115
|
+
async function terminatePane(paneId: string): Promise<boolean> {
|
|
116
|
+
try {
|
|
117
|
+
const data = await herdrJson<HerdrProcInfoResult>(["pane", "process-info", "--pane", paneId]);
|
|
118
|
+
const pgid = data?.result?.process_info?.foreground_process_group_id;
|
|
119
|
+
if (!pgid) return false;
|
|
120
|
+
process.kill(-pgid, "SIGTERM");
|
|
121
|
+
return true;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 轮询等待 pi 真正退出;超时则抛错(避免在旧 pi 仍活着时重启)。 */
|
|
128
|
+
async function waitExit(paneId: string, timeoutMs = 15000): Promise<void> {
|
|
129
|
+
const deadline = Date.now() + timeoutMs;
|
|
130
|
+
while (Date.now() < deadline) {
|
|
131
|
+
if (!(await hasPiForeground(paneId))) return;
|
|
132
|
+
await sleep(300);
|
|
133
|
+
}
|
|
134
|
+
throw new Error("等待 pi 退出超时");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 在 pane 里以原 session 重新拉起 pi。 */
|
|
138
|
+
async function relaunch(agent: PiAgent): Promise<void> {
|
|
139
|
+
const session = agent.agent_session?.value;
|
|
140
|
+
if (!session) throw new Error(`pane ${agent.pane_id} 无 session 信息,无法恢复`);
|
|
141
|
+
const cmd = `cd ${shellQuote(agent.cwd)} && pi --session ${shellQuote(session)}`;
|
|
142
|
+
const { code, stderr } = await runHerdr(["pane", "run", agent.pane_id, cmd]);
|
|
143
|
+
if (code !== 0) throw new Error(stderr || `herdr pane run 退出码 ${code}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default function (pi: ExtensionAPI) {
|
|
147
|
+
pi.registerCommand("pi-hot-restart", {
|
|
148
|
+
description: "批量重启当前 herdr 里的所有 pi 窗口(跳过 working 与当前窗口),并恢复到原 session",
|
|
149
|
+
handler: async (_args, ctx) => {
|
|
150
|
+
let agents: PiAgent[];
|
|
151
|
+
try {
|
|
152
|
+
agents = await listPiAgents();
|
|
153
|
+
} catch (e) {
|
|
154
|
+
ctx.ui.notify(`获取 pi 窗口失败: ${(e as Error).message}`, "error");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (agents.length === 0) {
|
|
159
|
+
ctx.ui.notify("没有发现 pi 窗口", "info");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const self = process.env.HERDR_PANE_ID;
|
|
164
|
+
const targets = agents.filter((a) => a.agent_status !== "working" && a.pane_id !== self);
|
|
165
|
+
const skipped = agents.filter((a) => a.agent_status === "working" || a.pane_id === self);
|
|
166
|
+
|
|
167
|
+
if (targets.length === 0) {
|
|
168
|
+
ctx.ui.notify(`没有可重启的 pi 窗口(共 ${agents.length} 个,其余为 working 或当前窗口)`, "info");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const list = targets
|
|
173
|
+
.map((a) => ` - ${a.terminal_title ?? a.pane_id} (${a.cwd})`)
|
|
174
|
+
.join("\n");
|
|
175
|
+
const skipNote = skipped.length ? `\n\n将跳过 ${skipped.length} 个窗口(working 或当前窗口)` : "";
|
|
176
|
+
const ok = await ctx.ui.confirm(
|
|
177
|
+
"pi-hot-restart",
|
|
178
|
+
`将重启 ${targets.length} 个 pi 窗口:\n${list}${skipNote}`,
|
|
179
|
+
);
|
|
180
|
+
if (!ok) return;
|
|
181
|
+
|
|
182
|
+
let done = 0;
|
|
183
|
+
const failures: string[] = [];
|
|
184
|
+
for (const agent of targets) {
|
|
185
|
+
try {
|
|
186
|
+
const killed = await terminatePane(agent.pane_id);
|
|
187
|
+
if (killed) {
|
|
188
|
+
await waitExit(agent.pane_id);
|
|
189
|
+
}
|
|
190
|
+
await sleep(300); // 给 shell 一点时间回到 prompt
|
|
191
|
+
await relaunch(agent);
|
|
192
|
+
done++;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
failures.push(`${agent.terminal_title ?? agent.pane_id}: ${(e as Error).message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (failures.length) {
|
|
199
|
+
ctx.ui.notify(`重启完成 ${done}/${targets.length} 个,失败 ${failures.length} 个`, "warning");
|
|
200
|
+
ctx.ui.notify(failures.join("\n"), "error");
|
|
201
|
+
} else {
|
|
202
|
+
ctx.ui.notify(`已重启 ${done} 个 pi 窗口`, "info");
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muzi3/herdr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Herdr for pi: the herdr operate skill (control Herdr from inside a pane), the herdr-guide skill (teach a human to set up and troubleshoot Herdr), and the herdr-agent-state extension (report agent state to Herdr).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": [
|
|
23
|
-
"./extensions"
|
|
23
|
+
"./extensions/index.ts",
|
|
24
|
+
"./extensions/pi-hot-restart.ts"
|
|
24
25
|
],
|
|
25
26
|
"skills": [
|
|
26
27
|
"./skills"
|