@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.
Files changed (2) hide show
  1. package/extensions/index.ts +49 -15
  2. package/package.json +1 -1
@@ -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, no cap, sequence-counter instead of timestamps
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) + "" : e.command;
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(EOL + EOL);
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 ? "" : ""} ${s.displayName}${s.version ? ` ${s.version}` : ""}`).join(EOL)); } });
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}${EOL}`;
49
- if (r.timedOut) o += "Status: TIMED OUT" + EOL;
50
- if (r.cancelled) o += "Status: CANCELLED" + EOL;
51
- if (r.stdout) o += `${EOL}── stdout ──${EOL}${r.stdout}${EOL}`;
52
- if (r.stderr) o += `${EOL}── stderr ──${EOL}${r.stderr}${EOL}`;
53
- if (safe.risk === "confirm") o += `${EOL}⚠️ ${safe.reasons.join("; ")}`;
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 × " + r.reasons.join("\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(`✓ ${p.name} at:\n${r.split(/\r?\n/).filter(Boolean).map(x => ` ${x}`).join(EOL)}`); } catch { return tr(`✗ ${p.name} not in PATH`); } } });
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 × " + d.join("\n × ") : "No WSL distros found."); } catch { return tr("WSL not available."); } } });
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
- 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()) + EOL); } });
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.1.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",