@bacnh85/pi-windows-tools 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/extensions/index.ts +49 -15
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
3
5
|
import { detectShell, detectAllShells, getDefaultShell } from "./lib/shell-detect";
|
|
4
6
|
import type { WindowsShellKind } from "./lib/shell-detect";
|
|
5
7
|
import { executeCommand as execCmd } from "./lib/shell-exec";
|
|
@@ -12,7 +14,6 @@ import { execFileSync } from "node:child_process";
|
|
|
12
14
|
const sk = Type.Union([Type.Literal("pwsh"), Type.Literal("powershell"), Type.Literal("cmd"), Type.Literal("git-bash"), Type.Literal("wsl")]);
|
|
13
15
|
const tp = Type.Optional(Type.Number({ description: "Timeout in ms." }));
|
|
14
16
|
const cs = { timeout_ms: tp };
|
|
15
|
-
const EOL = "\n";
|
|
16
17
|
|
|
17
18
|
function tr(text: string) { return Promise.resolve({ content: [{ type: "text" as const, text }], details: {} }); }
|
|
18
19
|
function rs(shell?: WindowsShellKind): WindowsShellKind {
|
|
@@ -22,20 +23,21 @@ function rs(shell?: WindowsShellKind): WindowsShellKind {
|
|
|
22
23
|
return getDefaultShell().kind;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
// ponytail: in-memory audit log
|
|
26
|
+
// ponytail: in-memory audit log
|
|
26
27
|
const _log: { shell: string; command: string; exitCode: number | null; timedOut: boolean }[] = [];
|
|
27
28
|
function _fmt() {
|
|
28
29
|
if (!_log.length) return "No commands executed yet.";
|
|
29
30
|
return _log.map((e, i) => {
|
|
30
31
|
const tag = e.timedOut ? " [TIMED OUT]" : "";
|
|
31
|
-
const cmd = e.command.length > 300 ? e.command.slice(0, 300) + "
|
|
32
|
+
const cmd = e.command.length > 300 ? e.command.slice(0, 300) + "\u2026" : e.command;
|
|
32
33
|
return `[${i + 1}] ${e.shell} exit:${e.exitCode}${tag}\n cmd: ${cmd}`;
|
|
33
|
-
}).join(
|
|
34
|
+
}).join("\n\n");
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export default function piWindowsToolsExtension(pi: ExtensionAPI) {
|
|
38
|
+
// ── Shell tools ──
|
|
37
39
|
pi.registerTool({ name: "windows_shell_detect", label: "Windows: Detect Shells", description: "Detect available Windows shells.", promptSnippet: "Detect available Windows shells", promptGuidelines: ["Use to check what shells are available."], parameters: Type.Object({ ...cs }),
|
|
38
|
-
execute() { return tr(detectAllShells().map(s => ` ${s.available ? "
|
|
40
|
+
execute() { return tr(detectAllShells().map(s => ` ${s.available ? "\u2713" : "\u2717"} ${s.displayName}${s.version ? " " + s.version : ""}`).join("\n")); } });
|
|
39
41
|
|
|
40
42
|
pi.registerTool({ name: "windows_shell_exec", label: "Windows: Execute Command", description: "Execute a command through a Windows shell.", promptSnippet: "Execute a command through a Windows shell",
|
|
41
43
|
promptGuidelines: ["Use instead of generic bash on Windows.", 'Use shell:"wsl" for WSL.', "Dangerous commands require confirmation."],
|
|
@@ -45,19 +47,45 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
|
|
|
45
47
|
const safe = classifyCommand(p.command);
|
|
46
48
|
const r = await execCmd(p.command, opts);
|
|
47
49
|
_log.push({ shell: opts.shell as string, command: p.command, exitCode: r.exitCode, timedOut: r.timedOut });
|
|
48
|
-
let o = `Exit code: ${r.exitCode}
|
|
49
|
-
if (r.timedOut) o += "Status: TIMED OUT"
|
|
50
|
-
if (r.cancelled) o += "Status: CANCELLED"
|
|
51
|
-
if (r.stdout) o +=
|
|
52
|
-
if (r.stderr) o +=
|
|
53
|
-
if (safe.risk === "confirm") o +=
|
|
50
|
+
let o = `Exit code: ${r.exitCode}\n`;
|
|
51
|
+
if (r.timedOut) o += "Status: TIMED OUT\n";
|
|
52
|
+
if (r.cancelled) o += "Status: CANCELLED\n";
|
|
53
|
+
if (r.stdout) o += `\n--- stdout ---\n${r.stdout}\n`;
|
|
54
|
+
if (r.stderr) o += `\n--- stderr ---\n${r.stderr}\n`;
|
|
55
|
+
if (safe.risk === "confirm") o += `\n\u26a0\ufe0f ${safe.reasons.join("; ")}`;
|
|
54
56
|
return tr(o);
|
|
55
57
|
} });
|
|
56
58
|
|
|
59
|
+
// ── File edit tool (reliable replacement for built-in edit) ──
|
|
60
|
+
pi.registerTool({ name: "windows_file_edit", label: "Windows: Edit File", description: "Replace literal text in a file. Reads actual file bytes so no line-ending/whitespace mismatch issues.",
|
|
61
|
+
promptSnippet: "Edit a file reliably using Node.js",
|
|
62
|
+
promptGuidelines: [
|
|
63
|
+
"Use instead of the built-in edit tool on all platforms.",
|
|
64
|
+
"Reads actual file bytes so replacements always match regardless of line endings.",
|
|
65
|
+
"Throws a clear error if oldText is not found.",
|
|
66
|
+
],
|
|
67
|
+
parameters: Type.Object({
|
|
68
|
+
path: Type.String({ description: "Path to the file (absolute or relative to cwd)." }),
|
|
69
|
+
oldText: Type.String({ description: "Literal text to find and replace." }),
|
|
70
|
+
newText: Type.String({ description: "Replacement text." }),
|
|
71
|
+
...cs,
|
|
72
|
+
}),
|
|
73
|
+
execute(_id, p, _s, _u, ctx) {
|
|
74
|
+
const filePath = resolve(ctx?.cwd || process.cwd(), p.path);
|
|
75
|
+
const content = readFileSync(filePath, "utf8");
|
|
76
|
+
const updated = content.replace(p.oldText, p.newText);
|
|
77
|
+
if (updated === content) throw new Error(`Not found or no change: ${filePath}`);
|
|
78
|
+
|
|
79
|
+
writeFileSync(filePath, updated, "utf8");
|
|
80
|
+
return tr(`Edited ${filePath}`);
|
|
81
|
+
} });
|
|
82
|
+
|
|
83
|
+
// ── Audit tools ──
|
|
57
84
|
pi.registerTool({ name: "windows_audit_log", label: "Windows: Audit Log", description: "Show command history and exit codes.", promptSnippet: "Show Windows command audit log", promptGuidelines: ["Use to see what was executed."],
|
|
58
85
|
parameters: Type.Object({ clear: Type.Optional(Type.Boolean({ description: "Clear after viewing." })), ...cs }),
|
|
59
86
|
execute(_id, p) { const out = _fmt(); if (p.clear) _log.length = 0; return tr(out); } });
|
|
60
87
|
|
|
88
|
+
// ── Path tools ──
|
|
61
89
|
pi.registerTool({ name: "windows_path_to_windows", label: "Windows: Convert to Windows Format", description: "Convert POSIX/WSL path to C:\\...", promptSnippet: "Convert path to Windows", promptGuidelines: ["Use when you have /c/ or /mnt/c/ path."], parameters: Type.Object({ path: Type.String(), ...cs }),
|
|
62
90
|
execute(_id, p) { return tr(pathUtils.toWindowsPath(p.path)); } });
|
|
63
91
|
pi.registerTool({ name: "windows_path_to_wsl", label: "Windows: Convert to WSL", description: "Convert Windows path to /mnt/c/...", promptSnippet: "Convert path to WSL", promptGuidelines: ["Use to pass Windows path to WSL."], parameters: Type.Object({ path: Type.String(), ...cs }),
|
|
@@ -66,16 +94,21 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
|
|
|
66
94
|
execute(_id, p) { return tr(pathUtils.toGitBashPath(p.path)); } });
|
|
67
95
|
pi.registerTool({ name: "windows_path_quote", label: "Windows: Quote Path", description: "Quote a path for a Windows shell.", promptSnippet: "Quote path for shell", promptGuidelines: ["Each shell has different quoting rules."], parameters: Type.Object({ path: Type.String(), shell: Type.Optional(sk), ...cs }),
|
|
68
96
|
execute(_id, p) { return tr(pathUtils.quoteForShell(p.path, rs(p.shell as WindowsShellKind | undefined))); } });
|
|
97
|
+
|
|
98
|
+
// ── Safety tools ──
|
|
69
99
|
pi.registerTool({ name: "windows_safety_classify", label: "Windows: Classify Safety", description: "Check if command is dangerous.", promptSnippet: "Classify command safety", promptGuidelines: ["Returns 'safe' or 'confirm'."], parameters: Type.Object({ command: Type.String(), ...cs }),
|
|
70
|
-
execute(_id, p) { const r = classifyCommand(p.command); return tr(`Risk: ${r.risk}${r.reasons.length ? "\nReasons:\n
|
|
100
|
+
execute(_id, p) { const r = classifyCommand(p.command); return tr(`Risk: ${r.risk}${r.reasons.length ? "\nReasons:\n \u2022 " + r.reasons.join("\n \u2022 ") : ""}`); } });
|
|
101
|
+
|
|
102
|
+
// ── Doctor tools ──
|
|
71
103
|
pi.registerTool({ name: "windows_doctor", label: "Windows: Doctor", description: "Detect installed developer tools.", promptSnippet: "Run Windows doctor", promptGuidelines: ["Checks PATH, WSL, long paths, dev mode."], parameters: Type.Object({ format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("json")])), ...cs }),
|
|
72
104
|
execute(_id, p) { const r = runDoctor(); return tr(p.format === "json" ? JSON.stringify(r, null, 2) : formatDoctorReport(r)); } });
|
|
73
105
|
pi.registerTool({ name: "windows_tool_discover", label: "Windows: Discover Tool", description: "Check if a tool is in PATH.", promptSnippet: "Check tool availability", promptGuidelines: ["Use to verify a tool is installed."], parameters: Type.Object({ name: Type.String(), ...cs }),
|
|
74
|
-
execute(_id, p) { try { const r = execFileSync("where", [p.name], { encoding: "utf8", timeout: 3000 }); return tr(
|
|
106
|
+
execute(_id, p) { try { const r = execFileSync("where", [p.name], { encoding: "utf8", timeout: 3000 }); return tr(`\u2713 ${p.name} at:\n${r.split(/\r?\n/).filter(Boolean).map(x => " " + x).join("\n")}`); } catch { return tr(`\u2717 ${p.name} not in PATH`); } } });
|
|
75
107
|
pi.registerTool({ name: "windows_wsl_list_distros", label: "Windows: List WSL Distros", description: "List installed WSL distros.", promptSnippet: "List WSL distros", promptGuidelines: ["See what distros are available."], parameters: Type.Object({ ...cs }),
|
|
76
|
-
execute() { try { const r = execFileSync("wsl.exe", ["-l", "-q"], { encoding: "utf8", timeout: 5000 }); const d = r.split(/\r?\n/).map(s => s.trim()).filter(s => s && !s.toLowerCase().includes("noinstall") && !s.startsWith("Windows")); return tr(d.length ? "Installed WSL distros:\n
|
|
108
|
+
execute() { try { const r = execFileSync("wsl.exe", ["-l", "-q"], { encoding: "utf8", timeout: 5000 }); const d = r.split(/\r?\n/).map(s => s.trim()).filter(s => s && !s.toLowerCase().includes("noinstall") && !s.startsWith("Windows")); return tr(d.length ? "Installed WSL distros:\n \u2022 " + d.join("\n \u2022 ") : "No WSL distros found."); } catch { return tr("WSL not available."); } } });
|
|
77
109
|
|
|
78
|
-
|
|
110
|
+
// ── Commands ──
|
|
111
|
+
pi.registerCommand("windows-doctor", { description: "Run Windows Tools Doctor.", handler: async (_a, ctx) => { ctx?.ui?.notify?.("Windows Doctor complete.", "info"); process.stdout.write(formatDoctorReport(runDoctor()) + "\n"); } });
|
|
79
112
|
pi.registerCommand("windows-shell", { description: "Show/set default shell.", handler: async (a, ctx) => {
|
|
80
113
|
const arg = (a || "").trim().toLowerCase();
|
|
81
114
|
if (arg) {
|
|
@@ -89,6 +122,7 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
|
|
|
89
122
|
process.stdout.write(`Current shell: ${c.displayName} (${c.kind})\nExecutable: ${c.executable}\n`);
|
|
90
123
|
} });
|
|
91
124
|
|
|
125
|
+
// ── Agent prompt ──
|
|
92
126
|
pi.on("before_agent_start", async (event) => {
|
|
93
127
|
if (process.env.PI_WINDOWS_TOOLS_ENABLED === "false" || process.platform !== "win32") return;
|
|
94
128
|
return { systemPrompt: `${event.systemPrompt}\n\n---\n\n## Windows Environment\n\n${buildShellGuidance(rs())}` };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-windows-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi extension for Windows-native tool manipulation — shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|