@sukeai/pi-logfwd 0.1.1 → 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 CHANGED
@@ -2,18 +2,20 @@
2
2
 
3
3
  实时命令日志转发:当 pi 运行命令时,把输出**实时流式**转发回来(而不是内置 bash 工具那样攒到最后一次性返回 + 截断)。
4
4
 
5
- - **bash_logged 工具**:pi 扩展,经 Go 二进制 `pi-logfwd` 在伪终端(PTY)中运行命令,JSONL 事件流逐块推送,可选追加日志文件。
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
- bash_logged 工具(@sukeai/pi-logfwd 扩展)
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`,即可调用 `bash_logged` 工具(参数 `command` / `timeout` / `cwd` / `logFile` / `noPty`)。
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 跳过 → 二进制缺失 → `bash_logged` 不会静默报 ENOENT,而是返回明确说明:
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,7 +99,7 @@ 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 扩展:注册 bash_logged 工具(平台解析 + 提醒逻辑)
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 主包/平台包版本同步
@@ -1,10 +1,19 @@
1
1
  /**
2
- * pi-logfwd - pi package extension registering a "bash_logged" tool.
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 and truncated by the built-in bash tool.
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 以使用 bash_logged。\n" +
93
+ "建议: 在 WSL / 容器中运行 pi 以使用本 bash 工具(实时转发)。\n" +
81
94
  "自行交叉编译仅管道版本 (--no-pty,无 PTY) 后,可设 PI_LOG_FWD_BIN 指向该二进制以绕过此提示。",
82
95
  };
83
96
  }
@@ -118,22 +131,26 @@ function resolveBin(): Resolution {
118
131
 
119
132
  export default function (pi: ExtensionAPI) {
120
133
  pi.registerTool({
121
- name: "bash_logged",
122
- label: "Bash (streamed log forward)",
134
+ name: "bash",
135
+ label: "bash",
123
136
  description:
124
- "Run a shell command or script through pi-logfwd and stream its logs back in real time. " +
125
- "Prefer bash_logged over bash when you need: (a) real-time output streaming instead of " +
126
- "end-buffered/truncated output, (b) a command that requires a pseudo-terminal (PTY), or " +
127
- "(c) a persistent log file. " +
128
- "NOT supported by any pi tool (including this one): interactive secret prompts " +
129
- "(sudo/ssh password) and GUI authorization dialogs - PTY can render a prompt but nothing " +
130
- "can answer it. For those, tell the user to run the command manually.",
131
- promptSnippet: "Run a shell command with real-time log forwarding (PTY support)",
132
- // 自定义 renderCall:TUI 里完整显示命令,并追加一行 log-fwd 标签标识控制来源
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 实时转发」
133
150
  renderCall(args, theme, _context) {
134
151
  const command = typeof args.command === "string" ? args.command : "";
135
152
  const commandDisplay = command || theme.fg("toolOutput", "...");
136
- let text = theme.fg("toolTitle", theme.bold(`bash_logged ${commandDisplay}`));
153
+ let text = theme.fg("toolTitle", theme.bold(commandDisplay));
137
154
  if (typeof args.timeout === "number") {
138
155
  text += theme.fg("muted", ` (timeout ${args.timeout}s)`);
139
156
  }
@@ -156,7 +173,7 @@ export default function (pi: ExtensionAPI) {
156
173
  const res = resolveBin();
157
174
  if (!res.ok) {
158
175
  return {
159
- content: [{ type: "text", text: `bash_logged 不可用:\n${res.notice}` }],
176
+ content: [{ type: "text", text: `bash (pi-logfwd) 不可用:\n${res.notice}` }],
160
177
  details: { exitCode: null, forwarded: false, reason: "binary-unavailable" },
161
178
  };
162
179
  }
@@ -170,15 +187,25 @@ export default function (pi: ExtensionAPI) {
170
187
 
171
188
  const child = spawn(res.bin, args, { cwd: ctx.cwd, env: process.env });
172
189
 
173
- let accumulated = "";
190
+ // 全量镜像到临时文件(原内置 bash 的“完整输出”兜底):输出 <= MAX_CHARS 时删除
191
+ // 临时文件;超长时在结果末尾报告文件路径。会话文本始终只保留尾部 MAX_CHARS。
192
+ let totalChars = 0;
193
+ let tail = "";
174
194
  let lastStream = 0;
195
+ let logPath: string | null = null;
196
+ let logStream: import("node:fs").WriteStream | null = null;
175
197
  const push = (chunk: string) => {
176
- accumulated += chunk;
177
- if (accumulated.length > MAX_CHARS * 2) accumulated = accumulated.slice(-MAX_CHARS);
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);
178
205
  const now = Date.now();
179
206
  if (now - lastStream > STREAM_INTERVAL_MS) {
180
207
  lastStream = now;
181
- onUpdate?.({ content: [{ type: "text", text: accumulated.slice(-MAX_CHARS) }] });
208
+ onUpdate?.({ content: [{ type: "text", text: tail }] });
182
209
  }
183
210
  };
184
211
 
@@ -197,7 +224,7 @@ export default function (pi: ExtensionAPI) {
197
224
  child.stderr.on("data", (d: Buffer) => (errText += d.toString()));
198
225
  child.on("error", (e: Error) => {
199
226
  if (e && (e as NodeJS.ErrnoException).code === "ENOENT") {
200
- errText += `\nbash_logged 不可用: ${res.bin} 不存在或不可执行。\n${res.notice}`;
227
+ errText += `\nbash (pi-logfwd) 不可用: ${res.bin} 不存在或不可执行。\n${res.notice}`;
201
228
  } else {
202
229
  errText += `pi-logfwd: ${e.message}\n`;
203
230
  }
@@ -216,8 +243,22 @@ export default function (pi: ExtensionAPI) {
216
243
  child.on("error", finish);
217
244
  });
218
245
 
219
- const text = (accumulated || "(no output)").slice(-MAX_CHARS);
220
- const resultText = errText ? `${text}\n${errText.trimEnd()}` : text;
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})`;
221
262
  return {
222
263
  content: [{ type: "text", text: resultText }],
223
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.1.1",
4
- "description": "pi package: bash_logged tool that streams command logs in real time through the pi-logfwd Go binary (PTY, JSONL events, optional log file) / pi 实时命令日志转发扩展",
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.1.1",
30
- "@sukeai/pi-logfwd-darwin-amd64": "0.1.1",
31
- "@sukeai/pi-logfwd-linux-arm64": "0.1.1",
32
- "@sukeai/pi-logfwd-linux-amd64": "0.1.1"
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": [