@sukeai/pi-logfwd 0.1.0 → 0.2.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 +7 -6
- package/extension/extension.ts +76 -34
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
实时命令日志转发:当 pi 运行命令时,把输出**实时流式**转发回来(而不是内置 bash 工具那样攒到最后一次性返回 + 截断)。
|
|
4
4
|
|
|
5
|
-
- **
|
|
5
|
+
- **bash 工具(覆盖内置)**:pi 扩展注册名为 `bash`,同名覆盖 pi 内置的缓冲式 bash——模型每次调用 bash 都固定经 Go 二进制 `pi-logfwd` 在伪终端(PTY)中运行命令,JSONL 事件流逐块推送,可选追加日志文件。
|
|
6
6
|
- **pi-logfwd 二进制**:跨平台预编译二进制,作为 npm 平台包随主包一起分发(见下方「平台支持」)。
|
|
7
7
|
|
|
8
8
|
动机:pi 内置 bash/process 工具是缓冲式的——输出攒到最后一次性返回,且截断为末尾 2000 行 / 50KB;没有 TTY、没有交互输入通道。pi-logfwd 补上:实时流式输出、PTY 支持、日志落盘。
|
|
9
9
|
|
|
10
|
+
> **为什么不叫 bash_logged**:早期版本注册成独立的 `bash_logged` 工具,与内置 `bash` 并列——模型每次执行命令都在两个工具间“随缘二选一”,导致 pi-logfwd 有时生效有时不生效(实测 23 个会话中仅 4 个用到,shell 调用占比约 3%)。0.2.0 起直接注册名为 `bash` 覆盖内置工具,触发从此 100% 确定:要么不用这个包,要用就全走实时转发。
|
|
11
|
+
|
|
10
12
|
> 密码提示与 GUI 授权弹窗**不支持**(PTY 只能渲染提示、无人应答;弹窗无法程序化操作)——此时告诉用户手动执行。
|
|
11
13
|
|
|
12
14
|
## 架构
|
|
13
15
|
|
|
14
16
|
```
|
|
15
17
|
pi (agent)
|
|
16
|
-
│
|
|
18
|
+
│ bash 工具(@sukeai/pi-logfwd 扩展,覆盖内置 bash)
|
|
17
19
|
▼
|
|
18
20
|
pi-logfwd run -- "shell script" ← 接收 shell 脚本 / 任意命令
|
|
19
21
|
│ 内部:PTY 分配(默认)或管道(--no-pty)
|
|
@@ -40,7 +42,7 @@ pi install /path/to/pi-log-forwarder
|
|
|
40
42
|
pi -e npm:@sukeai/pi-logfwd
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
装完在 pi 里 `/reload
|
|
45
|
+
装完在 pi 里 `/reload`,内置 `bash` 即被替换——所有 shell 命令自动走实时转发(参数 `command` / `timeout` / `cwd` / `logFile` / `noPty` 可用)。想还原内置 bash:`pi remove npm:@sukeai/pi-logfwd` 后 `/reload`。
|
|
44
46
|
|
|
45
47
|
## 平台支持
|
|
46
48
|
|
|
@@ -56,7 +58,7 @@ pi -e npm:@sukeai/pi-logfwd
|
|
|
56
58
|
|
|
57
59
|
**Windows 为什么不支持**:`pi-logfwd` 的 PTY 层用 creack/pty,它在 Windows 上直接返回 `ErrUnsupported`(`--no-pty` 管道模式理论上可行,但需额外改 shell 默认值/信号处理,成本高收益低),因此不发布 win32 平台包。
|
|
58
60
|
|
|
59
|
-
**不支持的平台如何提醒**:全部平台包被 npm 跳过 → 二进制缺失 → `
|
|
61
|
+
**不支持的平台如何提醒**:全部平台包被 npm 跳过 → 二进制缺失 → `bash`(pi-logfwd)不会静默报 ENOENT,而是返回明确说明:
|
|
60
62
|
|
|
61
63
|
- **win32**:提示「Windows 不受支持(creack/pty ErrUnsupported),建议在 WSL/容器中运行 pi」;自行编译仅管道版可设 `PI_LOG_FWD_BIN` 绕过。
|
|
62
64
|
- **其他缺二进制**:给出三种装法(`pi install npm:@sukeai/pi-logfwd` / `go build` / 放入 PATH 或 `~/.pi/agent/bin`)。
|
|
@@ -97,11 +99,10 @@ pi-logfwd run --timeout 30s 'npm run build'
|
|
|
97
99
|
|
|
98
100
|
```
|
|
99
101
|
cmd/pi-logfwd/ Go 源码(main.go / runner.go)
|
|
100
|
-
extension/extension.ts pi 扩展:注册
|
|
102
|
+
extension/extension.ts pi 扩展:注册 bash 工具覆盖内置(平台解析 + 提醒逻辑)
|
|
101
103
|
package.json 主包(pi manifest + optionalDependencies 平台包列表)
|
|
102
104
|
scripts/release.sh 交叉编译 + 发布(Go build → 平台包 → npm publish)
|
|
103
105
|
scripts/set-version.js 主包/平台包版本同步
|
|
104
|
-
.pi/skills/log-forwarding.md pi 项目 skill(用法 + 边界调研结论)
|
|
105
106
|
```
|
|
106
107
|
|
|
107
108
|
### 本地构建(不经 npm)
|
package/extension/extension.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-logfwd - pi package extension registering a "
|
|
2
|
+
* pi-logfwd - pi package extension registering a "bash" tool that REPLACES
|
|
3
|
+
* pi's built-in (buffered) bash.
|
|
4
|
+
*
|
|
5
|
+
* 为什么注册名用 bash:pi 的工具注册表里与内置工具同名的扩展工具会覆盖内置定义
|
|
6
|
+
* (_refreshToolRegistry:先放 built-in,再按工具名 set 扩展工具)。注册成 bash
|
|
7
|
+
* 后,模型每次调用 bash 都固定走 pi-logfwd(PTY + 实时流式转发 + 可选日志文件),
|
|
8
|
+
* 不再存在 "bash / bash_logged 由模型随缘二选一" 的触发不规律问题。
|
|
3
9
|
*
|
|
4
10
|
* Runs commands through the Go binary `pi-logfwd` (PTY, real-time log
|
|
5
11
|
* forwarding, optional log file). Streaming chunks are pushed into the
|
|
6
12
|
* conversation via onUpdate, so output appears as it happens instead of
|
|
7
|
-
* being buffered
|
|
13
|
+
* being buffered until the process exits. Output longer than MAX_CHARS is
|
|
14
|
+
* mirrored to a temp file (reported at the end) instead of silently losing
|
|
15
|
+
* its head - the safety net pi's built-in bash used to provide via
|
|
16
|
+
* temp files.
|
|
8
17
|
*
|
|
9
18
|
* Binary resolution order (checked on every execute):
|
|
10
19
|
* 1. env PI_LOG_FWD_BIN (explicit override)
|
|
@@ -23,15 +32,19 @@
|
|
|
23
32
|
* Install:
|
|
24
33
|
* pi install npm:@sukeai/pi-logfwd # 推荐:随包自动装当前平台二进制
|
|
25
34
|
*
|
|
35
|
+
* 装好后 /reload 即覆盖内置 bash(所有 shell 命令经 pi-logfwd 实时转发);
|
|
36
|
+
* 想还原内置 bash:pi remove npm:@sukeai/pi-logfwd 后 /reload。
|
|
37
|
+
*
|
|
26
38
|
* Dev / manual overrides:
|
|
27
39
|
* PI_LOG_FWD_BIN=/path/to/pi-logfwd # 指向自建/自编译二进制
|
|
28
40
|
* cd cmd/pi-logfwd && go build -o ~/.pi/agent/bin/pi-logfwd ./cmd/pi-logfwd
|
|
29
41
|
*/
|
|
30
42
|
import { spawn } from "node:child_process";
|
|
31
43
|
import { createInterface } from "node:readline";
|
|
32
|
-
import { existsSync } from "node:fs";
|
|
44
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
45
|
+
import { rm } from "node:fs/promises";
|
|
33
46
|
import { createRequire } from "node:module";
|
|
34
|
-
import { homedir } from "node:os";
|
|
47
|
+
import { homedir, tmpdir } from "node:os";
|
|
35
48
|
import { dirname, join } from "node:path";
|
|
36
49
|
import { fileURLToPath } from "node:url";
|
|
37
50
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -39,7 +52,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
39
52
|
import { Type } from "typebox";
|
|
40
53
|
|
|
41
54
|
const require = createRequire(import.meta.url);
|
|
42
|
-
const MAX_CHARS = 60_000;
|
|
55
|
+
const MAX_CHARS = 60_000; // 会话文本保留的尾部上限;超出部分镜像到临时文件并报告路径
|
|
43
56
|
const STREAM_INTERVAL_MS = 150;
|
|
44
57
|
|
|
45
58
|
/** Prebuilt binary platform packages published alongside this package. */
|
|
@@ -77,7 +90,7 @@ function resolveBin(): Resolution {
|
|
|
77
90
|
bin: null,
|
|
78
91
|
notice:
|
|
79
92
|
"Windows 不受支持: pi-logfwd 的 PTY 依赖 (creack/pty) 在 Windows 上返回 ErrUnsupported,未发布 win32 预编译包。\n" +
|
|
80
|
-
"建议: 在 WSL / 容器中运行 pi
|
|
93
|
+
"建议: 在 WSL / 容器中运行 pi 以使用本 bash 工具(实时转发)。\n" +
|
|
81
94
|
"自行交叉编译仅管道版本 (--no-pty,无 PTY) 后,可设 PI_LOG_FWD_BIN 指向该二进制以绕过此提示。",
|
|
82
95
|
};
|
|
83
96
|
}
|
|
@@ -118,27 +131,32 @@ function resolveBin(): Resolution {
|
|
|
118
131
|
|
|
119
132
|
export default function (pi: ExtensionAPI) {
|
|
120
133
|
pi.registerTool({
|
|
121
|
-
name: "
|
|
122
|
-
label: "
|
|
134
|
+
name: "bash",
|
|
135
|
+
label: "bash",
|
|
123
136
|
description:
|
|
124
|
-
"Run a
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
"Run a bash command in the current working directory through pi-logfwd (PTY, real-time " +
|
|
138
|
+
"log forwarding). Output is streamed back in real time instead of being buffered until " +
|
|
139
|
+
"the process exits; the conversation keeps the last 60000 chars and longer output is " +
|
|
140
|
+
"mirrored to a temp file whose path is reported at the end. This tool replaces pi's " +
|
|
141
|
+
"built-in buffered bash. NOT supported: interactive secret prompts (sudo/ssh password) " +
|
|
142
|
+
"and GUI authorization dialogs - PTY can render a prompt but nothing can answer it, so " +
|
|
143
|
+
"tell the user to run those manually. Optionally provide a timeout (seconds), cwd, " +
|
|
144
|
+
"logFile to append to, or noPty.",
|
|
145
|
+
promptSnippet: "Execute bash commands (ls, grep, find, etc.) with real-time streamed output (PTY)",
|
|
146
|
+
promptGuidelines: [
|
|
147
|
+
"You can inspect PI_* environment variables for current model and session details.",
|
|
148
|
+
],
|
|
149
|
+
// 自定义 renderCall:TUI 里完整显示命令,并追加一行 log-fwd 标签标识「经 pi-logfwd 实时转发」
|
|
150
|
+
renderCall(args, theme, _context) {
|
|
151
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
152
|
+
const commandDisplay = command || theme.fg("toolOutput", "...");
|
|
153
|
+
let text = theme.fg("toolTitle", theme.bold(commandDisplay));
|
|
154
|
+
if (typeof args.timeout === "number") {
|
|
155
|
+
text += theme.fg("muted", ` (timeout ${args.timeout}s)`);
|
|
156
|
+
}
|
|
157
|
+
const tag = theme.fg("dim", theme.bold("log-fwd"));
|
|
158
|
+
return new Text(`${text}\n${tag}`, 0, 0);
|
|
159
|
+
},
|
|
142
160
|
parameters: Type.Object({
|
|
143
161
|
command: Type.String({ description: "Shell script or command to run" }),
|
|
144
162
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: none)" })),
|
|
@@ -155,7 +173,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
155
173
|
const res = resolveBin();
|
|
156
174
|
if (!res.ok) {
|
|
157
175
|
return {
|
|
158
|
-
content: [{ type: "text", text: `
|
|
176
|
+
content: [{ type: "text", text: `bash (pi-logfwd) 不可用:\n${res.notice}` }],
|
|
159
177
|
details: { exitCode: null, forwarded: false, reason: "binary-unavailable" },
|
|
160
178
|
};
|
|
161
179
|
}
|
|
@@ -169,15 +187,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
187
|
|
|
170
188
|
const child = spawn(res.bin, args, { cwd: ctx.cwd, env: process.env });
|
|
171
189
|
|
|
172
|
-
|
|
190
|
+
// 全量镜像到临时文件(原内置 bash 的“完整输出”兜底):输出 <= MAX_CHARS 时删除
|
|
191
|
+
// 临时文件;超长时在结果末尾报告文件路径。会话文本始终只保留尾部 MAX_CHARS。
|
|
192
|
+
let totalChars = 0;
|
|
193
|
+
let tail = "";
|
|
173
194
|
let lastStream = 0;
|
|
195
|
+
let logPath: string | null = null;
|
|
196
|
+
let logStream: import("node:fs").WriteStream | null = null;
|
|
174
197
|
const push = (chunk: string) => {
|
|
175
|
-
|
|
176
|
-
if (
|
|
198
|
+
totalChars += chunk.length;
|
|
199
|
+
if (!logStream) {
|
|
200
|
+
logPath = join(tmpdir(), `pi-logfwd-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.log`);
|
|
201
|
+
logStream = createWriteStream(logPath, { flags: "a" });
|
|
202
|
+
}
|
|
203
|
+
logStream.write(chunk);
|
|
204
|
+
tail = (tail + chunk).slice(-MAX_CHARS);
|
|
177
205
|
const now = Date.now();
|
|
178
206
|
if (now - lastStream > STREAM_INTERVAL_MS) {
|
|
179
207
|
lastStream = now;
|
|
180
|
-
onUpdate?.({ content: [{ type: "text", text:
|
|
208
|
+
onUpdate?.({ content: [{ type: "text", text: tail }] });
|
|
181
209
|
}
|
|
182
210
|
};
|
|
183
211
|
|
|
@@ -196,7 +224,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
224
|
child.stderr.on("data", (d: Buffer) => (errText += d.toString()));
|
|
197
225
|
child.on("error", (e: Error) => {
|
|
198
226
|
if (e && (e as NodeJS.ErrnoException).code === "ENOENT") {
|
|
199
|
-
errText += `\
|
|
227
|
+
errText += `\nbash (pi-logfwd) 不可用: ${res.bin} 不存在或不可执行。\n${res.notice}`;
|
|
200
228
|
} else {
|
|
201
229
|
errText += `pi-logfwd: ${e.message}\n`;
|
|
202
230
|
}
|
|
@@ -215,8 +243,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
215
243
|
child.on("error", finish);
|
|
216
244
|
});
|
|
217
245
|
|
|
218
|
-
|
|
219
|
-
|
|
246
|
+
// 关闭日志流;只在文本被截断(输出超长)时保留临时文件
|
|
247
|
+
if (logStream) {
|
|
248
|
+
await new Promise<void>((r) => logStream!.end(() => r()));
|
|
249
|
+
if (totalChars <= MAX_CHARS && logPath) {
|
|
250
|
+
try {
|
|
251
|
+
await rm(logPath, { force: true });
|
|
252
|
+
} catch {
|
|
253
|
+
/* best-effort cleanup */
|
|
254
|
+
}
|
|
255
|
+
logPath = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const text = tail || "(no output)";
|
|
260
|
+
let resultText = errText ? `${text}\n${errText.trimEnd()}` : text;
|
|
261
|
+
if (logPath) resultText += `\n(输出超长,完整日志已存 ${logPath})`;
|
|
220
262
|
return {
|
|
221
263
|
content: [{ type: "text", text: resultText }],
|
|
222
264
|
details: { exitCode, forwarded: true, logFile: params.logFile ?? null },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sukeai/pi-logfwd",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "pi package:
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "pi package: replaces the built-in bash tool with real-time log forwarding via the pi-logfwd Go binary (PTY, JSONL events, optional log file) / pi 实时命令日志转发扩展(bash 覆盖内置)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"typebox": "*"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"@sukeai/pi-logfwd-darwin-arm64": "0.
|
|
30
|
-
"@sukeai/pi-logfwd-darwin-amd64": "0.
|
|
31
|
-
"@sukeai/pi-logfwd-linux-arm64": "0.
|
|
32
|
-
"@sukeai/pi-logfwd-linux-amd64": "0.
|
|
29
|
+
"@sukeai/pi-logfwd-darwin-arm64": "0.2.0",
|
|
30
|
+
"@sukeai/pi-logfwd-darwin-amd64": "0.2.0",
|
|
31
|
+
"@sukeai/pi-logfwd-linux-arm64": "0.2.0",
|
|
32
|
+
"@sukeai/pi-logfwd-linux-amd64": "0.2.0"
|
|
33
33
|
},
|
|
34
34
|
"pi": {
|
|
35
35
|
"extensions": [
|