@sukeai/pi-logfwd 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 robotnoname
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # pi-logfwd
2
+
3
+ 实时命令日志转发:当 pi 运行命令时,把输出**实时流式**转发回来(而不是内置 bash 工具那样攒到最后一次性返回 + 截断)。
4
+
5
+ - **bash_logged 工具**:pi 扩展,经 Go 二进制 `pi-logfwd` 在伪终端(PTY)中运行命令,JSONL 事件流逐块推送,可选追加日志文件。
6
+ - **pi-logfwd 二进制**:跨平台预编译二进制,作为 npm 平台包随主包一起分发(见下方「平台支持」)。
7
+
8
+ 动机:pi 内置 bash/process 工具是缓冲式的——输出攒到最后一次性返回,且截断为末尾 2000 行 / 50KB;没有 TTY、没有交互输入通道。pi-logfwd 补上:实时流式输出、PTY 支持、日志落盘。
9
+
10
+ > 密码提示与 GUI 授权弹窗**不支持**(PTY 只能渲染提示、无人应答;弹窗无法程序化操作)——此时告诉用户手动执行。
11
+
12
+ ## 架构
13
+
14
+ ```
15
+ pi (agent)
16
+ │ bash_logged 工具(@sukeai/pi-logfwd 扩展)
17
+
18
+ pi-logfwd run -- "shell script" ← 接收 shell 脚本 / 任意命令
19
+ │ 内部:PTY 分配(默认)或管道(--no-pty)
20
+ │ 实时:每块输出 → JSONL 事件 → stdout + 可选 --log-file
21
+
22
+ { "ts":…, "event":"start", "pid":123, "command":"echo hi" }
23
+ { "ts":…, "event":"output", "stream":"stdout", "data":"hi\n" }
24
+ { "ts":…, "event":"exit", "code":0, "durationMs":12 }
25
+ ```
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ # 推荐:npm 安装(自动带上当前平台的预编译二进制)
31
+ pi install npm:@sukeai/pi-logfwd
32
+
33
+ # 从 GitHub(仓库公开后可用;需在 settings 或命令行指定版本 tag)
34
+ pi install git:github.com/lazyfury/pi-log-forwarder@v0.1.0
35
+
36
+ # 本地路径开发(不安装依赖,适合改源码)
37
+ pi install /path/to/pi-log-forwarder
38
+
39
+ # 试用一次(不写 settings)
40
+ pi -e npm:@sukeai/pi-logfwd
41
+ ```
42
+
43
+ 装完在 pi 里 `/reload`,即可调用 `bash_logged` 工具(参数 `command` / `timeout` / `cwd` / `logFile` / `noPty`)。
44
+
45
+ ## 平台支持
46
+
47
+ 二进制分发模型:npm **platform companion 包**(esbuild 同款机制)。主包把全部平台包列为 `optionalDependencies`,npm 只安装与当前 `os`/`cpu` 匹配的那一个,其余静默跳过;扩展在运行时按平台定位二进制。
48
+
49
+ | 平台 | 预编译包 | 状态 |
50
+ | --- | --- | --- |
51
+ | macOS arm64 | `@sukeai/pi-logfwd-darwin-arm64` | ✅ 实机验证 |
52
+ | macOS amd64 | `@sukeai/pi-logfwd-darwin-amd64` | ⚠️ 交叉编译,未实机验证 |
53
+ | Linux arm64 | `@sukeai/pi-logfwd-linux-arm64` | ⚠️ 交叉编译,未实机验证 |
54
+ | Linux amd64 | `@sukeai/pi-logfwd-linux-amd64` | ⚠️ 交叉编译,未实机验证 |
55
+ | Windows | 无 | ❌ 不支持 |
56
+
57
+ **Windows 为什么不支持**:`pi-logfwd` 的 PTY 层用 creack/pty,它在 Windows 上直接返回 `ErrUnsupported`(`--no-pty` 管道模式理论上可行,但需额外改 shell 默认值/信号处理,成本高收益低),因此不发布 win32 平台包。
58
+
59
+ **不支持的平台如何提醒**:全部平台包被 npm 跳过 → 二进制缺失 → `bash_logged` 不会静默报 ENOENT,而是返回明确说明:
60
+
61
+ - **win32**:提示「Windows 不受支持(creack/pty ErrUnsupported),建议在 WSL/容器中运行 pi」;自行编译仅管道版可设 `PI_LOG_FWD_BIN` 绕过。
62
+ - **其他缺二进制**:给出三种装法(`pi install npm:@sukeai/pi-logfwd` / `go build` / 放入 PATH 或 `~/.pi/agent/bin`)。
63
+
64
+ ### 二进制解析顺序(每次调用时)
65
+
66
+ ```
67
+ 1. env PI_LOG_FWD_BIN (显式指定,设了但缺失会直接报错)
68
+ 2. 平台 companion 包 (@sukeai/pi-logfwd-<os>-<arch>)
69
+ 3. ~/.pi/agent/bin/pi-logfwd (兼容旧的本地安装方式)
70
+ 4. PATH 上的 pi-logfwd (兼容旧的 PATH 安装方式)
71
+ ```
72
+
73
+ ## pi-logfwd CLI 用法(独立于 pi 使用)
74
+
75
+ ```bash
76
+ pi-logfwd run [flags] [--] <shell-script> # PTY 模式(默认),JSONL 事件流
77
+ pi-logfwd run [flags] - # 从 stdin 读脚本
78
+ pi-logfwd version | help
79
+
80
+ # flags: --no-pty --plain --timeout D --cwd DIR --log-file FILE
81
+ ```
82
+
83
+ 示例:
84
+
85
+ ```bash
86
+ pi-logfwd run 'echo hi; echo err >&2' # JSONL
87
+ pi-logfwd run --plain 'make test' # 人类可读
88
+ cat deploy.sh | pi-logfwd run --log-file /tmp/deploy.log -
89
+ pi-logfwd run --timeout 30s 'npm run build'
90
+ ```
91
+
92
+ 退出码:透传子进程退出码;超时被杀死为 124;信号终止为 128+信号。
93
+
94
+ ## 开发者
95
+
96
+ ### 仓库结构
97
+
98
+ ```
99
+ cmd/pi-logfwd/ Go 源码(main.go / runner.go)
100
+ extension/extension.ts pi 扩展:注册 bash_logged 工具(平台解析 + 提醒逻辑)
101
+ package.json 主包(pi manifest + optionalDependencies 平台包列表)
102
+ scripts/release.sh 交叉编译 + 发布(Go build → 平台包 → npm publish)
103
+ scripts/set-version.js 主包/平台包版本同步
104
+ .pi/skills/log-forwarding.md pi 项目 skill(用法 + 边界调研结论)
105
+ ```
106
+
107
+ ### 本地构建(不经 npm)
108
+
109
+ ```bash
110
+ go build -o ~/.pi/agent/bin/pi-logfwd ./cmd/pi-logfwd # 单二进制,无配置依赖
111
+ # 或放 PATH;扩展解析顺序第 3/4 步会找到它
112
+ ```
113
+
114
+ ### 发布(Go 二进制 + npm 包一体)
115
+
116
+ 所有包共用同一版本号(主包 + 4 个平台包),一条命令完成:
117
+
118
+ ```bash
119
+ scripts/release.sh 0.1.0 # 构建矩阵 + 打包 dry-run(验证内容,不发)
120
+ scripts/release.sh 0.1.0 --publish # 真发布
121
+ # 然后 git tag v0.1.0 && git push --tags
122
+ ```
123
+
124
+ 发布流程:`go build`(CGO_ENABLED=0,`GOOS/GOARCH` 矩阵)→ 每个平台生成只含二进制的 npm 平台包(`os`/`cpu` 字段匹配)→ 依次 `npm publish --access public` → 主包最后发。
125
+
126
+ > 平台包命名 `@sukeai/pi-logfwd-<os>-<arch>`,每个包 `bin: { "pi-logfwd": "bin/pi-logfwd" }`——npm 会把匹配平台的二进制链入 `node_modules/.bin`。
127
+
128
+ ### 备注
129
+
130
+ - 新增平台:在 `scripts/release.sh` 的 `PLATFORMS` 加一行 + `package.json` 的 `optionalDependencies` 加对应项;Windows 除外(见上)。
131
+ - 本仓库 LICENSE 沿用 pi-fun-placeholder 先例(MIT / robotnoname);如需改署名,替换 `LICENSE` 与两处 `package.json` 的 license 说明即可。
@@ -0,0 +1,226 @@
1
+ /**
2
+ * pi-logfwd - pi package extension registering a "bash_logged" tool.
3
+ *
4
+ * Runs commands through the Go binary `pi-logfwd` (PTY, real-time log
5
+ * forwarding, optional log file). Streaming chunks are pushed into the
6
+ * conversation via onUpdate, so output appears as it happens instead of
7
+ * being buffered and truncated by the built-in bash tool.
8
+ *
9
+ * Binary resolution order (checked on every execute):
10
+ * 1. env PI_LOG_FWD_BIN (explicit override)
11
+ * 2. bundled platform package (@sukeai/pi-logfwd-<os>-<arch>,
12
+ * installed via optionalDependencies)
13
+ * 3. ~/.pi/agent/bin/pi-logfwd (legacy local install)
14
+ * 4. PATH 上的 pi-logfwd (legacy, spawn-by-name fallback)
15
+ *
16
+ * Platform support: darwin (arm64/amd64) + linux (arm64/amd64) ship prebuilt
17
+ * binaries through npm optionalDependencies; npm installs only the package
18
+ * matching the current os/cpu and silently skips the rest. Windows is NOT
19
+ * supported (creack/pty returns ErrUnsupported on Windows, so the PTY core
20
+ * cannot work) - on win32 the tool returns an explicit notice instead of a
21
+ * cryptic spawn failure.
22
+ *
23
+ * Install:
24
+ * pi install npm:@sukeai/pi-logfwd # 推荐:随包自动装当前平台二进制
25
+ *
26
+ * Dev / manual overrides:
27
+ * PI_LOG_FWD_BIN=/path/to/pi-logfwd # 指向自建/自编译二进制
28
+ * cd cmd/pi-logfwd && go build -o ~/.pi/agent/bin/pi-logfwd ./cmd/pi-logfwd
29
+ */
30
+ import { spawn } from "node:child_process";
31
+ import { createInterface } from "node:readline";
32
+ import { existsSync } from "node:fs";
33
+ import { createRequire } from "node:module";
34
+ import { homedir } from "node:os";
35
+ import { dirname, join } from "node:path";
36
+ import { fileURLToPath } from "node:url";
37
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
38
+ import { Text } from "@earendil-works/pi-tui";
39
+ import { Type } from "typebox";
40
+
41
+ const require = createRequire(import.meta.url);
42
+ const MAX_CHARS = 60_000;
43
+ const STREAM_INTERVAL_MS = 150;
44
+
45
+ /** Prebuilt binary platform packages published alongside this package. */
46
+ const COMPANION_PREFIX = "@sukeai/pi-logfwd-";
47
+ const COMPANION_ARCHES = new Set(["arm64", "x64"]);
48
+
49
+ type Resolution =
50
+ | { ok: true; bin: string }
51
+ | { ok: false; bin: string | null; notice: string };
52
+
53
+ /**
54
+ * Resolve the pi-logfwd binary for the current platform. Returns a binary
55
+ * path when found, or a human-readable notice explaining how to fix it
56
+ * (or why the platform is unsupported).
57
+ */
58
+ function resolveBin(): Resolution {
59
+ // 1. Explicit override. If it is set but missing, say so - do not
60
+ // silently fall through, the user clearly wanted this exact path.
61
+ const fromEnv = process.env.PI_LOG_FWD_BIN;
62
+ if (fromEnv) {
63
+ if (existsSync(fromEnv)) return { ok: true, bin: fromEnv };
64
+ return {
65
+ ok: false,
66
+ bin: null,
67
+ notice: `PI_LOG_FWD_BIN 指向的文件不存在: ${fromEnv}`,
68
+ };
69
+ }
70
+
71
+ // 2. Windows is unsupported by design: creack/pty (the PTY layer pi-logfwd
72
+ // relies on) returns ErrUnsupported on Windows, so no win32 prebuilt
73
+ // package is published and the PTY core cannot work there.
74
+ if (process.platform === "win32") {
75
+ return {
76
+ ok: false,
77
+ bin: null,
78
+ notice:
79
+ "Windows 不受支持: pi-logfwd 的 PTY 依赖 (creack/pty) 在 Windows 上返回 ErrUnsupported,未发布 win32 预编译包。\n" +
80
+ "建议: 在 WSL / 容器中运行 pi 以使用 bash_logged。\n" +
81
+ "自行交叉编译仅管道版本 (--no-pty,无 PTY) 后,可设 PI_LOG_FWD_BIN 指向该二进制以绕过此提示。",
82
+ };
83
+ }
84
+
85
+ // 3. Bundled platform package (installed by npm from optionalDependencies,
86
+ // only the one matching os/cpu is present).
87
+ const archOk = COMPANION_ARCHES.has(process.arch);
88
+ if (archOk) {
89
+ const companion = COMPANION_PREFIX + `${process.platform}-${process.arch}`;
90
+ try {
91
+ const pkgRoot = dirname(require.resolve(`${companion}/package.json`));
92
+ const bin = join(pkgRoot, "bin", "pi-logfwd");
93
+ if (existsSync(bin)) return { ok: true, bin };
94
+ } catch {
95
+ /* companion not installed - keep resolving */
96
+ }
97
+ }
98
+
99
+ // 4. Legacy local install location (keep working for existing setups).
100
+ const agentBin = join(homedir(), ".pi", "agent", "bin", "pi-logfwd");
101
+ if (existsSync(agentBin)) return { ok: true, bin: agentBin };
102
+
103
+ // 5. PATH fallback: spawn by name and let ENOENT surface a clear error.
104
+ const platformTag =
105
+ process.platform === "darwin" || process.platform === "linux"
106
+ ? `${process.platform}-${process.arch}`
107
+ : process.platform;
108
+ return {
109
+ ok: false,
110
+ bin: "pi-logfwd",
111
+ notice:
112
+ `未找到 pi-logfwd 二进制 (平台 ${platformTag}${archOk ? "" : ",无对应预编译包"})。安装方式任选其一:\n` +
113
+ " 1. pi install npm:@sukeai/pi-logfwd # 推荐: 随包自动安装当前平台预编译二进制\n" +
114
+ " 2. go build -o <bin> ./cmd/pi-logfwd # 本仓库自行编译后设 PI_LOG_FWD_BIN=<bin>\n" +
115
+ " 3. 把 pi-logfwd 放入 PATH 或 ~/.pi/agent/bin/pi-logfwd",
116
+ };
117
+ }
118
+
119
+ export default function (pi: ExtensionAPI) {
120
+ pi.registerTool({
121
+ name: "bash_logged",
122
+ label: "Bash (streamed log forward)",
123
+ 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 里完整显示命令(不定义时兜底渲染只显示工具名)
133
+ renderCall(args, theme, _context) {
134
+ const command = typeof args.command === "string" ? args.command : "";
135
+ const commandDisplay = command || theme.fg("toolOutput", "...");
136
+ let text = theme.fg("toolTitle", theme.bold(`bash_logged ${commandDisplay}`));
137
+ if (typeof args.timeout === "number") {
138
+ text += theme.fg("muted", ` (timeout ${args.timeout}s)`);
139
+ }
140
+ return new Text(text, 0, 0);
141
+ },
142
+ parameters: Type.Object({
143
+ command: Type.String({ description: "Shell script or command to run" }),
144
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: none)" })),
145
+ cwd: Type.Optional(Type.String({ description: "Working directory (default: session cwd)" })),
146
+ logFile: Type.Optional(Type.String({ description: "Append forwarded logs to this file" })),
147
+ noPty: Type.Optional(
148
+ Type.Boolean({ description: "Run without a pseudo-terminal (stdout/stderr stay separate)" }),
149
+ ),
150
+ }),
151
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
152
+ // Resolve the binary on every call: the package (and with it the
153
+ // companion binary) may be added/updated between pi restarts, and a
154
+ // missing binary should produce a clear notice instead of ENOENT.
155
+ const res = resolveBin();
156
+ if (!res.ok) {
157
+ return {
158
+ content: [{ type: "text", text: `bash_logged 不可用:\n${res.notice}` }],
159
+ details: { exitCode: null, forwarded: false, reason: "binary-unavailable" },
160
+ };
161
+ }
162
+
163
+ const args = ["run"];
164
+ if (params.noPty) args.push("--no-pty");
165
+ if (params.cwd) args.push("--cwd", params.cwd);
166
+ if (params.logFile) args.push("--log-file", params.logFile);
167
+ if (params.timeout) args.push("--timeout", `${params.timeout}s`);
168
+ args.push("--", params.command);
169
+
170
+ const child = spawn(res.bin, args, { cwd: ctx.cwd, env: process.env });
171
+
172
+ let accumulated = "";
173
+ let lastStream = 0;
174
+ const push = (chunk: string) => {
175
+ accumulated += chunk;
176
+ if (accumulated.length > MAX_CHARS * 2) accumulated = accumulated.slice(-MAX_CHARS);
177
+ const now = Date.now();
178
+ if (now - lastStream > STREAM_INTERVAL_MS) {
179
+ lastStream = now;
180
+ onUpdate?.({ content: [{ type: "text", text: accumulated.slice(-MAX_CHARS) }] });
181
+ }
182
+ };
183
+
184
+ const rl = createInterface({ input: child.stdout });
185
+ rl.on("line", (line) => {
186
+ try {
187
+ const ev = JSON.parse(line);
188
+ if (ev.event === "output") push(ev.data);
189
+ } catch {
190
+ /* ignore malformed lines */
191
+ }
192
+ });
193
+
194
+ let exitCode: number | null = null;
195
+ let errText = "";
196
+ child.stderr.on("data", (d: Buffer) => (errText += d.toString()));
197
+ child.on("error", (e: Error) => {
198
+ if (e && (e as NodeJS.ErrnoException).code === "ENOENT") {
199
+ errText += `\nbash_logged 不可用: ${res.bin} 不存在或不可执行。\n${res.notice}`;
200
+ } else {
201
+ errText += `pi-logfwd: ${e.message}\n`;
202
+ }
203
+ });
204
+ child.on("close", (c) => (exitCode = c));
205
+ signal?.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
206
+
207
+ await new Promise<void>((resolve) => {
208
+ let settled = false;
209
+ const finish = () => {
210
+ if (settled) return;
211
+ settled = true;
212
+ resolve();
213
+ };
214
+ child.on("close", finish);
215
+ child.on("error", finish);
216
+ });
217
+
218
+ const text = (accumulated || "(no output)").slice(-MAX_CHARS);
219
+ const resultText = errText ? `${text}\n${errText.trimEnd()}` : text;
220
+ return {
221
+ content: [{ type: "text", text: resultText }],
222
+ details: { exitCode, forwarded: true, logFile: params.logFile ?? null },
223
+ };
224
+ },
225
+ });
226
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@sukeai/pi-logfwd",
3
+ "version": "0.1.0",
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 实时命令日志转发扩展",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "pi-extension",
11
+ "extension",
12
+ "log-forwarder",
13
+ "pty",
14
+ "logs",
15
+ "streaming"
16
+ ],
17
+ "files": [
18
+ "extension/extension.ts",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "main": "extension/extension.ts",
23
+ "peerDependencies": {
24
+ "@earendil-works/pi-coding-agent": "*",
25
+ "@earendil-works/pi-tui": "*",
26
+ "typebox": "*"
27
+ },
28
+ "optionalDependencies": {
29
+ "@sukeai/pi-logfwd-darwin-arm64": "0.1.0",
30
+ "@sukeai/pi-logfwd-darwin-amd64": "0.1.0",
31
+ "@sukeai/pi-logfwd-linux-arm64": "0.1.0",
32
+ "@sukeai/pi-logfwd-linux-amd64": "0.1.0"
33
+ },
34
+ "pi": {
35
+ "extensions": [
36
+ "./extension/extension.ts"
37
+ ]
38
+ }
39
+ }