@zhijiewang/openharness 1.2.0 → 1.4.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.
@@ -0,0 +1,114 @@
1
+ import { z } from "zod";
2
+ import { spawn } from "node:child_process";
3
+ const inputSchema = z.object({
4
+ command: z.string().describe("Background command to watch"),
5
+ pattern: z.string().optional().describe("Regex pattern to match output lines"),
6
+ timeout: z.number().optional().describe("Max watch time in ms (default 60000)"),
7
+ maxLines: z.number().optional().describe("Max output lines to collect (default 100)"),
8
+ });
9
+ export const MonitorTool = {
10
+ name: "Monitor",
11
+ description: "Watch a background process and collect output. Optionally filter by regex pattern.",
12
+ inputSchema,
13
+ riskLevel: "medium",
14
+ isReadOnly() { return true; },
15
+ isConcurrencySafe() { return true; },
16
+ async call(input, context) {
17
+ const timeout = input.timeout ?? 60_000;
18
+ const maxLines = input.maxLines ?? 100;
19
+ const pattern = input.pattern ? new RegExp(input.pattern) : null;
20
+ return new Promise((resolve) => {
21
+ const lines = [];
22
+ let settled = false;
23
+ const proc = spawn(input.command, {
24
+ shell: true,
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ windowsHide: true,
27
+ });
28
+ const timer = setTimeout(() => {
29
+ if (!settled) {
30
+ settled = true;
31
+ proc.kill();
32
+ resolve({
33
+ output: lines.length > 0
34
+ ? lines.join('\n') + `\n\n[Monitor timed out after ${timeout / 1000}s — ${lines.length} lines collected]`
35
+ : `[Monitor timed out after ${timeout / 1000}s — no output]`,
36
+ isError: false,
37
+ });
38
+ }
39
+ }, timeout);
40
+ const handleLine = (line) => {
41
+ if (settled)
42
+ return;
43
+ if (pattern && !pattern.test(line))
44
+ return;
45
+ lines.push(line.trimEnd());
46
+ // Stream output chunk if callback available
47
+ if (context.onOutputChunk && context.callId) {
48
+ context.onOutputChunk(context.callId, line + '\n');
49
+ }
50
+ if (lines.length >= maxLines) {
51
+ settled = true;
52
+ clearTimeout(timer);
53
+ proc.kill();
54
+ resolve({
55
+ output: lines.join('\n') + `\n\n[Collected ${maxLines} lines — stopped]`,
56
+ isError: false,
57
+ });
58
+ }
59
+ };
60
+ let stdoutBuffer = '';
61
+ proc.stdout?.on('data', (chunk) => {
62
+ stdoutBuffer += chunk.toString();
63
+ const parts = stdoutBuffer.split('\n');
64
+ stdoutBuffer = parts.pop() ?? '';
65
+ for (const line of parts)
66
+ handleLine(line);
67
+ });
68
+ let stderrBuffer = '';
69
+ proc.stderr?.on('data', (chunk) => {
70
+ stderrBuffer += chunk.toString();
71
+ const parts = stderrBuffer.split('\n');
72
+ stderrBuffer = parts.pop() ?? '';
73
+ for (const line of parts)
74
+ handleLine(line);
75
+ });
76
+ proc.on('exit', (code) => {
77
+ if (!settled) {
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ // Flush remaining buffers
81
+ if (stdoutBuffer)
82
+ handleLine(stdoutBuffer);
83
+ if (stderrBuffer)
84
+ handleLine(stderrBuffer);
85
+ resolve({
86
+ output: lines.length > 0
87
+ ? lines.join('\n') + `\n\n[Process exited with code ${code ?? 'unknown'} — ${lines.length} lines]`
88
+ : `[Process exited with code ${code ?? 'unknown'} — no output]`,
89
+ isError: (code ?? 0) !== 0,
90
+ });
91
+ }
92
+ });
93
+ proc.on('error', (err) => {
94
+ if (!settled) {
95
+ settled = true;
96
+ clearTimeout(timer);
97
+ resolve({
98
+ output: `Monitor error: ${err.message}`,
99
+ isError: true,
100
+ });
101
+ }
102
+ });
103
+ });
104
+ },
105
+ prompt() {
106
+ return `Watch a background process and collect its output. Optionally filter lines by regex pattern.
107
+ Parameters:
108
+ - command (string, required): The command to run and watch
109
+ - pattern (string, optional): Regex to filter output lines
110
+ - timeout (number, optional): Max time in ms (default 60000)
111
+ - maxLines (number, optional): Max lines to collect (default 100)`;
112
+ },
113
+ };
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import type { Tool } from "../../Tool.js";
3
+ declare const inputSchema: z.ZodObject<{
4
+ command: z.ZodString;
5
+ timeout: z.ZodOptional<z.ZodNumber>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ command: string;
8
+ timeout?: number | undefined;
9
+ }, {
10
+ command: string;
11
+ timeout?: number | undefined;
12
+ }>;
13
+ export declare const PowerShellTool: Tool<typeof inputSchema>;
14
+ export {};
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { execSync } from "node:child_process";
3
+ const inputSchema = z.object({
4
+ command: z.string().describe("PowerShell command to execute"),
5
+ timeout: z.number().optional().describe("Timeout in ms (default 120000)"),
6
+ });
7
+ export const PowerShellTool = {
8
+ name: "PowerShell",
9
+ description: "Execute PowerShell commands (Windows only). Use for Windows-specific tasks like registry access, COM objects, or .NET calls.",
10
+ inputSchema,
11
+ riskLevel: "high",
12
+ isReadOnly() { return false; },
13
+ isConcurrencySafe() { return false; },
14
+ async call(input) {
15
+ if (process.platform !== 'win32') {
16
+ return { output: "PowerShell is only available on Windows. Use Bash instead.", isError: true };
17
+ }
18
+ const timeout = input.timeout ?? 120_000;
19
+ try {
20
+ const output = execSync(`powershell.exe -NoProfile -NonInteractive -Command "${input.command.replace(/"/g, '\\"')}"`, { encoding: 'utf-8', timeout, maxBuffer: 10 * 1024 * 1024, windowsHide: true });
21
+ return { output: output.trim(), isError: false };
22
+ }
23
+ catch (err) {
24
+ const output = String(err.stdout ?? err.stderr ?? err.message ?? 'PowerShell error');
25
+ return { output: output.slice(0, 100_000), isError: true };
26
+ }
27
+ },
28
+ prompt() {
29
+ return "Execute PowerShell commands on Windows. Use for registry, COM, .NET, and Windows-specific operations.";
30
+ },
31
+ };
32
+ //# sourceMappingURL=index.js.map
package/dist/tools.js CHANGED
@@ -42,6 +42,8 @@ import { KillProcessTool } from "./tools/KillProcessTool/index.js";
42
42
  import { RemoteTriggerTool } from "./tools/RemoteTriggerTool/index.js";
43
43
  import { MultiEditTool } from "./tools/MultiEditTool/index.js";
44
44
  import { PipelineTool } from "./tools/PipelineTool/index.js";
45
+ import { PowerShellTool } from "./tools/PowerShellTool/index.js";
46
+ import { MonitorTool } from "./tools/MonitorTool/index.js";
45
47
  /**
46
48
  * Returns all registered tools.
47
49
  *
@@ -96,6 +98,8 @@ export function getAllTools() {
96
98
  KillProcessTool,
97
99
  RemoteTriggerTool,
98
100
  MultiEditTool,
101
+ PowerShellTool,
102
+ MonitorTool,
99
103
  ];
100
104
  return [
101
105
  ...core,
@@ -7,6 +7,13 @@ const EDIT_SAFE_TOOLS = new Set([
7
7
  "FileRead", "FileWrite", "FileEdit", "Glob", "Grep", "LS",
8
8
  "ImageRead", "NotebookEdit",
9
9
  ]);
10
+ /** Parse a tool specifier like "Bash(npm run *)" into tool name + pattern */
11
+ function parseToolSpecifier(specifier) {
12
+ const match = specifier.match(/^(\w+)\((.+)\)$/);
13
+ if (match)
14
+ return { toolName: match[1], argPattern: match[2] };
15
+ return { toolName: specifier };
16
+ }
10
17
  /** Match a tool name against a pattern (supports trailing * for prefix matching) */
11
18
  function matchToolPattern(pattern, toolName) {
12
19
  if (pattern.endsWith("*")) {
@@ -14,14 +21,47 @@ function matchToolPattern(pattern, toolName) {
14
21
  }
15
22
  return pattern === toolName;
16
23
  }
24
+ /**
25
+ * Match an argument pattern against a value using glob-style matching.
26
+ * Supports: * (any chars), ** (any path segments)
27
+ */
28
+ function matchArgGlob(pattern, value) {
29
+ // Convert glob to regex: * → [^/]*, ** → .*, escape other regex chars
30
+ const regexStr = pattern
31
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex chars (except * and ?)
32
+ .replace(/\*\*/g, '{{DOUBLESTAR}}')
33
+ .replace(/\*/g, '[^/]*')
34
+ .replace(/\{\{DOUBLESTAR\}\}/g, '.*');
35
+ try {
36
+ return new RegExp(`^${regexStr}$`).test(value);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
17
42
  /** Find the first matching tool permission rule */
18
43
  function findToolRule(rules, toolName, toolInput) {
19
44
  if (!rules || rules.length === 0)
20
45
  return undefined;
21
46
  return rules.find(r => {
22
- if (!matchToolPattern(r.tool, toolName))
47
+ const { toolName: specToolName, argPattern } = parseToolSpecifier(r.tool);
48
+ // Check tool name match (with prefix * support)
49
+ if (!matchToolPattern(specToolName, toolName))
23
50
  return false;
24
- // If rule has a pattern, match against Bash command content only
51
+ // If rule has an inline argument pattern (e.g., "Bash(npm run *)")
52
+ if (argPattern && toolInput) {
53
+ const input = toolInput;
54
+ // For Bash: match against command string
55
+ if (toolName === 'Bash' && typeof input.command === 'string') {
56
+ return matchArgGlob(argPattern, input.command);
57
+ }
58
+ // For file tools: match against file_path
59
+ if (['Edit', 'Write', 'Read'].includes(toolName) && typeof input.file_path === 'string') {
60
+ return matchArgGlob(argPattern, input.file_path);
61
+ }
62
+ return false; // Has pattern but no matching field
63
+ }
64
+ // Legacy: separate pattern field (regex) for Bash commands
25
65
  if (r.pattern && toolInput && toolName === "Bash") {
26
66
  const command = toolInput?.command;
27
67
  if (typeof command === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {