march-cli 0.1.12 → 0.1.14

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 (50) hide show
  1. package/package.json +2 -1
  2. package/src/agent/command-exec-tool.mjs +42 -8
  3. package/src/agent/{read-file-tool.mjs → file-tools/read-file-tool.mjs} +2 -2
  4. package/src/agent/file-tools/read-image-tool.mjs +76 -0
  5. package/src/agent/runner/runner-utils.mjs +6 -0
  6. package/src/agent/runner.mjs +17 -16
  7. package/src/agent/runtime/ipc/ipc-peer.mjs +99 -0
  8. package/src/agent/runtime/ipc/process-ipc-transport.mjs +16 -0
  9. package/src/agent/runtime/remote-runner-client.mjs +73 -0
  10. package/src/agent/runtime/remote-ui-client.mjs +19 -0
  11. package/src/agent/runtime/runner-ipc-target.mjs +126 -0
  12. package/src/agent/runtime/runner-process-client.mjs +47 -0
  13. package/src/agent/runtime/runner-process-entry.mjs +93 -0
  14. package/src/agent/runtime/runner-runtime-host.mjs +1 -0
  15. package/src/agent/runtime/ui-event-bridge.mjs +85 -0
  16. package/src/agent/screen-tools/list-windows-tool.mjs +39 -0
  17. package/src/agent/screen-tools/screen-tool.mjs +49 -0
  18. package/src/agent/screen-tools/windows-screen.mjs +133 -0
  19. package/src/agent/session/session-options.mjs +2 -1
  20. package/src/agent/tool-summary.mjs +112 -0
  21. package/src/agent/tools.mjs +12 -5
  22. package/src/agent/turn/turn-events.mjs +46 -0
  23. package/src/agent/turn/turn-runner.mjs +2 -1
  24. package/src/agent/vision-capability.mjs +14 -0
  25. package/src/cli/args.mjs +8 -0
  26. package/src/cli/commands/copy-command.mjs +16 -2
  27. package/src/cli/commands/status-command.mjs +7 -4
  28. package/src/cli/commands/thinking-command.mjs +10 -3
  29. package/src/cli/repl-loop.mjs +3 -1
  30. package/src/cli/startup/create-runtime-runner.mjs +61 -0
  31. package/src/cli/startup/startup-banner.mjs +64 -10
  32. package/src/cli/tui/layout/main-pane-layout.mjs +16 -7
  33. package/src/cli/tui/selection-screen.mjs +83 -34
  34. package/src/cli/tui/status/status-bar.mjs +155 -18
  35. package/src/cli/tui/tool-rendering.mjs +3 -113
  36. package/src/cli/tui/tui-handlers.mjs +1 -1
  37. package/src/cli/tui/ui-theme.mjs +14 -5
  38. package/src/cli/ui.mjs +1 -1
  39. package/src/config/config-json.mjs +11 -0
  40. package/src/context/engine.mjs +10 -9
  41. package/src/context/profiles.mjs +39 -0
  42. package/src/context/system-core/base.md +2 -1
  43. package/src/main.mjs +42 -35
  44. package/src/provider/accept-command.mjs +89 -0
  45. package/src/provider/command.mjs +21 -0
  46. package/src/provider/custom-provider.mjs +5 -4
  47. package/src/provider/share-command.mjs +79 -0
  48. package/src/provider/share-payload.mjs +52 -0
  49. package/src/supergrok/tool.mjs +6 -6
  50. package/src/context/center-memory.mjs +0 -14
@@ -0,0 +1,47 @@
1
+ import { fork } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { createProcessRuntimeIpcPeer } from "./ipc/process-ipc-transport.mjs";
4
+ import { createRemoteRunnerClient } from "./remote-runner-client.mjs";
5
+ import { createRuntimeUiEventTarget } from "./ui-event-bridge.mjs";
6
+
7
+ const DEFAULT_ENTRY = new URL("./runner-process-entry.mjs", import.meta.url);
8
+
9
+ export async function createRunnerProcessClient({
10
+ runnerOptions,
11
+ ui,
12
+ onModelPayload = null,
13
+ entry = fileURLToPath(DEFAULT_ENTRY),
14
+ forkImpl = fork,
15
+ timeoutMs = 0,
16
+ } = {}) {
17
+ const child = forkImpl(entry, [], {
18
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
19
+ });
20
+ const peer = createProcessRuntimeIpcPeer({
21
+ processLike: child,
22
+ target: {
23
+ ...createRuntimeUiEventTarget(ui),
24
+ modelPayload: (event) => onModelPayload?.(event),
25
+ },
26
+ timeoutMs,
27
+ });
28
+ const runner = createRemoteRunnerClient(peer);
29
+ try {
30
+ await runner.init(runnerOptions);
31
+ } catch (error) {
32
+ peer.dispose();
33
+ child.kill?.();
34
+ throw error;
35
+ }
36
+
37
+ const remoteDispose = runner.dispose;
38
+ const dispose = async () => {
39
+ try {
40
+ await remoteDispose.call(runner);
41
+ } finally {
42
+ child.kill?.();
43
+ }
44
+ };
45
+ runner.dispose = dispose;
46
+ return { runner, child, dispose };
47
+ }
@@ -0,0 +1,93 @@
1
+ import { join } from "node:path";
2
+ import { createRunner } from "../runner.mjs";
3
+ import { createProcessRuntimeIpcPeer } from "./ipc/process-ipc-transport.mjs";
4
+ import { createRemoteRuntimeUiClient } from "./remote-ui-client.mjs";
5
+ import { createRunnerIpcTarget } from "./runner-ipc-target.mjs";
6
+ import { createMarchAuthStorage } from "../../auth/storage.mjs";
7
+ import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
8
+ import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
9
+ import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
10
+ import { initializeMcp } from "../../mcp/index.mjs";
11
+ import { createWebToolsFromConfig } from "../../web/tools.mjs";
12
+ import { createPermissionController } from "../../cli/permissions.mjs";
13
+ import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
14
+ import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
15
+ import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
16
+ import { createDesktopTurnNotifier } from "../../notification/desktop-notifier.mjs";
17
+
18
+ const peer = createProcessRuntimeIpcPeer({
19
+ target: createRunnerIpcTarget({ createRunnerImpl: createIsolatedRunner }),
20
+ });
21
+
22
+ process.once("disconnect", () => peer.dispose());
23
+
24
+ async function createIsolatedRunner(options = {}) {
25
+ const ui = createRemoteRuntimeUiClient(peer);
26
+ const memoryStore = new MarkdownMemoryStore({ root: options.memoryRoot });
27
+ const memoryTools = createMarkdownMemoryTools(memoryStore);
28
+ const shellRuntime = options.shellRuntime ? createCliShellRuntime({ cwd: options.cwd }) : null;
29
+ const mcpInit = await initializeMcp({ projectDir: options.cwd });
30
+ const logger = createLogger({ logDir: options.logDir ?? (options.stateRoot ? join(options.stateRoot, "logs") : undefined) });
31
+ installProcessLogHandlers(logger);
32
+
33
+ const runner = await createRunner({
34
+ cwd: options.cwd,
35
+ modelId: options.modelId,
36
+ provider: options.provider,
37
+ serviceTier: options.serviceTier,
38
+ providers: options.providers,
39
+ stateRoot: options.stateRoot,
40
+ ui,
41
+ memoryRoot: options.memoryRoot,
42
+ profilePaths: options.profilePaths,
43
+ memoryStore,
44
+ memoryTools,
45
+ shellRuntime,
46
+ mcpTools: mcpInit.mcpTools,
47
+ mcpInjections: mcpInit.mcpInjections,
48
+ mcpClientManager: mcpInit.clientManager,
49
+ webTools: createWebToolsFromConfig(options.config ?? {}),
50
+ namespace: options.namespace,
51
+ projectMarchDir: options.projectMarchDir,
52
+ extensionPaths: options.extensionPaths ?? [],
53
+ sessionManager: resolvePiSessionManager({
54
+ cwd: options.cwd,
55
+ projectMarchDir: options.projectMarchDir,
56
+ enabled: true,
57
+ }),
58
+ useRuntimeHost: true,
59
+ syncPiSidecar: true,
60
+ lifecycleHooks: options.lifecycleHooks ?? [],
61
+ lifecycleDiagnostics: options.lifecycleDiagnostics ?? [],
62
+ authStorage: createMarchAuthStorage({
63
+ provider: options.provider ?? "deepseek",
64
+ providers: options.providers,
65
+ cwd: options.cwd,
66
+ }).authStorage,
67
+ maxTurns: options.config?.maxTurns ?? undefined,
68
+ trimBatch: options.config?.trimBatch ?? undefined,
69
+ hostedTools: options.config?.hostedTools,
70
+ permissionController: createPermissionController({ mode: options.permissionMode }),
71
+ modelContextDumper: createModelContextDumper(options.modelContextDumper ?? { enabled: false }),
72
+ turnNotifier: createDesktopTurnNotifier({
73
+ enabled: Boolean(options.config?.notifications?.turnEnd),
74
+ config: options.config?.notifications,
75
+ }),
76
+ logger,
77
+ onModelPayload: (event) => peer.notify("modelPayload", pickModelPayloadEvent(event)),
78
+ });
79
+
80
+ const originalDispose = runner.dispose;
81
+ runner.dispose = async () => {
82
+ try {
83
+ await originalDispose.call(runner);
84
+ } finally {
85
+ memoryStore.close?.();
86
+ }
87
+ };
88
+ return runner;
89
+ }
90
+
91
+ function pickModelPayloadEvent({ estimatedTokens, provider, model, kind, turnId } = {}) {
92
+ return { estimatedTokens, provider, model, kind, turnId };
93
+ }
@@ -62,6 +62,7 @@ export async function createRunnerRuntimeHost({
62
62
  authStorage,
63
63
  projectMarchDir,
64
64
  hostedTools,
65
+ getCurrentModel: () => sessionBinding.get()?.model ?? null,
65
66
  });
66
67
  },
67
68
  });
@@ -0,0 +1,85 @@
1
+ export function createRuntimeUiEventTarget(ui) {
2
+ return {
3
+ uiEvent: (event) => dispatchRuntimeUiEvent(ui, event),
4
+ uiRequest: (event) => dispatchRuntimeUiEvent(ui, event),
5
+ };
6
+ }
7
+
8
+ export function createRuntimeUiEventBus() {
9
+ const listeners = new Set();
10
+ return {
11
+ on(listener) {
12
+ listeners.add(listener);
13
+ return () => listeners.delete(listener);
14
+ },
15
+ emit(event) {
16
+ for (const listener of [...listeners]) listener(event);
17
+ },
18
+ async request(event) {
19
+ let response;
20
+ for (const listener of [...listeners]) {
21
+ const result = await listener(event);
22
+ if (response === undefined && result !== undefined) response = result;
23
+ }
24
+ return response;
25
+ },
26
+ };
27
+ }
28
+
29
+ export function createRuntimeUiBridge(ui, { eventBus = createRuntimeUiEventBus() } = {}) {
30
+ const detach = eventBus.on((event) => dispatchRuntimeUiEvent(ui, event));
31
+ return {
32
+ ui: createRuntimeUiClient(eventBus),
33
+ eventBus,
34
+ detach,
35
+ };
36
+ }
37
+
38
+ export function createRuntimeUiClient(eventBus) {
39
+ return {
40
+ turnStart: () => eventBus.emit({ type: "turn_start" }),
41
+ turnEnd: () => eventBus.emit({ type: "turn_end" }),
42
+ assistantReplyEnd: () => eventBus.emit({ type: "assistant_reply_end" }),
43
+ textDelta: (delta) => eventBus.emit({ type: "text_delta", delta }),
44
+ thinkingStart: () => eventBus.emit({ type: "thinking_start" }),
45
+ thinkingDelta: (delta) => eventBus.emit({ type: "thinking_delta", delta }),
46
+ thinkingEnd: (tokens) => eventBus.emit({ type: "thinking_end", tokens }),
47
+ toolStart: (name, args) => eventBus.emit({ type: "tool_start", name, args }),
48
+ toolEnd: (name, isError, result) => eventBus.emit({ type: "tool_end", name, isError, result }),
49
+ retryStart: (event) => eventBus.emit({ type: "retry_start", ...event }),
50
+ retryEnd: (event) => eventBus.emit({ type: "retry_end", ...event }),
51
+ status: (text) => eventBus.emit({ type: "status", text }),
52
+ memoryHint: ({ source, hints }) => eventBus.emit({ type: "memory_hint", source, hints }),
53
+ editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
54
+ requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
55
+ };
56
+ }
57
+
58
+ export function dispatchRuntimeUiEvent(ui, event) {
59
+ switch (event.type) {
60
+ case "turn_start": return ui.turnStart?.();
61
+ case "turn_end": return ui.turnEnd?.();
62
+ case "assistant_reply_end": return ui.assistantReplyEnd?.();
63
+ case "text_delta": return ui.textDelta?.(event.delta);
64
+ case "thinking_start": return ui.thinkingStart?.();
65
+ case "thinking_delta": return ui.thinkingDelta?.(event.delta);
66
+ case "thinking_end": return ui.thinkingEnd?.(event.tokens);
67
+ case "tool_start": return ui.toolStart?.(event.name, event.args);
68
+ case "tool_end": return ui.toolEnd?.(event.name, event.isError, event.result);
69
+ case "retry_start": return ui.retryStart?.(pickRetryStart(event));
70
+ case "retry_end": return ui.retryEnd?.(pickRetryEnd(event));
71
+ case "status": return ui.status?.(event.text);
72
+ case "memory_hint": return ui.memoryHint?.({ source: event.source, hints: event.hints });
73
+ case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
74
+ case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
75
+ default: return undefined;
76
+ }
77
+ }
78
+
79
+ function pickRetryStart({ attempt, maxAttempts, delayMs, errorMessage }) {
80
+ return { attempt, maxAttempts, delayMs, errorMessage };
81
+ }
82
+
83
+ function pickRetryEnd({ success, attempt, finalError }) {
84
+ return { success, attempt, finalError };
85
+ }
@@ -0,0 +1,39 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { listWindowsWindows } from "./windows-screen.mjs";
4
+
5
+ export function createListWindowsTool({ listWindowsImpl = listWindowsWindows } = {}) {
6
+ return defineTool({
7
+ name: "list_windows",
8
+ label: "List Windows",
9
+ description: "List visible desktop windows so the model can choose a windowId for screen({ target: 'window', windowId }).",
10
+ parameters: Type.Object({
11
+ limit: Type.Optional(Type.Number({ description: "Maximum windows to return; default 30" })),
12
+ }),
13
+ execute: async (_toolCallId, params = {}) => listWindowsTool({ listWindowsImpl, ...params }),
14
+ });
15
+ }
16
+
17
+ export function listWindowsTool({ listWindowsImpl = listWindowsWindows, limit = 30 } = {}) {
18
+ const result = listWindowsImpl();
19
+ if (!result?.ok) return textResult(`Error listing windows: ${result?.message || "unknown error"}`, { error: true });
20
+ const windows = (result.windows ?? []).slice(0, normalizeLimit(limit));
21
+ if (windows.length === 0) return textResult("No visible windows found.", { windows: [] });
22
+ const lines = ["Visible windows:"];
23
+ for (const item of windows) {
24
+ const process = item.process ? ` (${item.process})` : "";
25
+ const bounds = item.bounds ? ` ${item.bounds.width}x${item.bounds.height}+${item.bounds.x},${item.bounds.y}` : "";
26
+ const minimized = item.minimized ? " minimized" : "";
27
+ lines.push(`- ${item.id}${process}${bounds}${minimized}: ${item.title}`);
28
+ }
29
+ lines.push("Use screen({ target: 'window', windowId }) to capture a listed window.");
30
+ return textResult(lines.join("\n"), { windows });
31
+ }
32
+
33
+ function normalizeLimit(limit) {
34
+ return Number.isFinite(limit) && limit > 0 ? Math.min(Math.floor(limit), 100) : 30;
35
+ }
36
+
37
+ function textResult(text, details = {}) {
38
+ return { content: [{ type: "text", text }], details };
39
+ }
@@ -0,0 +1,49 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { currentModelImageInputError } from "../vision-capability.mjs";
4
+ import { captureScreenWindows } from "./windows-screen.mjs";
5
+
6
+ export function createScreenTool({ getCurrentModel = null, captureScreenImpl = captureScreenWindows } = {}) {
7
+ return defineTool({
8
+ name: "screen",
9
+ label: "Screen Capture",
10
+ description: "Capture the current desktop or a visible window and send it to the model as an image attachment.",
11
+ parameters: Type.Object({
12
+ target: Type.Optional(Type.String({ description: "desktop (default) or window" })),
13
+ windowId: Type.Optional(Type.String({ description: "Window id from list_windows when target is window" })),
14
+ }),
15
+ execute: async (_toolCallId, params = {}) => captureScreenTool({ getCurrentModel, captureScreenImpl, ...params }),
16
+ });
17
+ }
18
+
19
+ export function captureScreenTool({ getCurrentModel = null, captureScreenImpl = captureScreenWindows, target = "desktop", windowId = null } = {}) {
20
+ const capabilityError = currentModelImageInputError(getCurrentModel);
21
+ if (capabilityError) return screenError(capabilityError, { unsupportedModel: true });
22
+ const normalizedTarget = target === "window" ? "window" : "desktop";
23
+ const result = captureScreenImpl({ target: normalizedTarget, windowId });
24
+ if (!result?.ok) return screenError(`Error capturing screen: ${result?.message || "unknown error"}`, { target: normalizedTarget, windowId });
25
+
26
+ const bounds = result.bounds ?? {};
27
+ const label = normalizedTarget === "window" ? `window ${result.windowId || windowId}` : "desktop";
28
+ return {
29
+ content: [
30
+ { type: "text", text: `Captured ${label} screenshot\nMIME: ${result.mimeType || "image/png"}\nBounds: ${formatBounds(bounds)}` },
31
+ { type: "image", data: result.data, mimeType: result.mimeType || "image/png" },
32
+ ],
33
+ details: {
34
+ target: normalizedTarget,
35
+ windowId: result.windowId ?? windowId ?? undefined,
36
+ bounds,
37
+ mimeType: result.mimeType || "image/png",
38
+ },
39
+ };
40
+ }
41
+
42
+ function screenError(text, details = {}) {
43
+ return { content: [{ type: "text", text }], details: { ...details, error: true } };
44
+ }
45
+
46
+ function formatBounds(bounds) {
47
+ const { x = 0, y = 0, width = 0, height = 0 } = bounds ?? {};
48
+ return `${width}x${height} at ${x},${y}`;
49
+ }
@@ -0,0 +1,133 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ const POWERSHELL = process.env.SystemRoot ? `${process.env.SystemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` : "powershell.exe";
4
+ const MAX_BUFFER = 80 * 1024 * 1024;
5
+
6
+ export function listWindowsWindows() {
7
+ return runJson(LIST_WINDOWS_SCRIPT);
8
+ }
9
+
10
+ export function captureScreenWindows({ target = "desktop", windowId = null } = {}) {
11
+ if (target === "window") {
12
+ if (!windowId) return { ok: false, message: "windowId is required when target is window" };
13
+ return runJson(CAPTURE_WINDOW_SCRIPT.replace("__WINDOW_ID__", escapePowershellString(windowId)));
14
+ }
15
+ return runJson(CAPTURE_DESKTOP_SCRIPT);
16
+ }
17
+
18
+ function runJson(script) {
19
+ if (process.platform !== "win32") return { ok: false, message: `screen tools are not supported on ${process.platform}` };
20
+ try {
21
+ const output = execFileSync(POWERSHELL, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
22
+ encoding: "utf8",
23
+ maxBuffer: MAX_BUFFER,
24
+ windowsHide: true,
25
+ }).trim();
26
+ return JSON.parse(output);
27
+ } catch (err) {
28
+ return { ok: false, message: `failed to run Windows screen capture: ${err.stderr || err.message}` };
29
+ }
30
+ }
31
+
32
+ function escapePowershellString(value) {
33
+ return String(value).replaceAll("'", "''");
34
+ }
35
+
36
+ const WIN32_TYPE = String.raw`
37
+ using System;
38
+ using System.Text;
39
+ using System.Runtime.InteropServices;
40
+ public static class MarchWin32 {
41
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
42
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
43
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
44
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
45
+ [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
46
+ [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
47
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
48
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
49
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
50
+ }
51
+ `;
52
+
53
+ const LIST_WINDOWS_SCRIPT = String.raw`
54
+ Add-Type -TypeDefinition @'
55
+ ${WIN32_TYPE}
56
+ '@
57
+ $items = New-Object System.Collections.Generic.List[object]
58
+ [MarchWin32]::EnumWindows({ param($hwnd, $lparam)
59
+ if (-not [MarchWin32]::IsWindowVisible($hwnd)) { return $true }
60
+ $length = [MarchWin32]::GetWindowTextLength($hwnd)
61
+ if ($length -le 0) { return $true }
62
+ $builder = New-Object System.Text.StringBuilder ($length + 1)
63
+ [void][MarchWin32]::GetWindowText($hwnd, $builder, $builder.Capacity)
64
+ $rect = New-Object MarchWin32+RECT
65
+ if (-not [MarchWin32]::GetWindowRect($hwnd, [ref]$rect)) { return $true }
66
+ $width = $rect.Right - $rect.Left
67
+ $height = $rect.Bottom - $rect.Top
68
+ if ($width -le 0 -or $height -le 0) { return $true }
69
+ $pidValue = 0
70
+ [void][MarchWin32]::GetWindowThreadProcessId($hwnd, [ref]$pidValue)
71
+ $processName = $null
72
+ try { $processName = (Get-Process -Id $pidValue -ErrorAction Stop).ProcessName } catch {}
73
+ $items.Add([ordered]@{
74
+ id = ("0x{0:x}" -f $hwnd.ToInt64())
75
+ title = $builder.ToString()
76
+ process = $processName
77
+ pid = $pidValue
78
+ bounds = [ordered]@{ x = $rect.Left; y = $rect.Top; width = $width; height = $height }
79
+ minimized = [MarchWin32]::IsIconic($hwnd)
80
+ }) | Out-Null
81
+ return $true
82
+ }, [IntPtr]::Zero) | Out-Null
83
+ [ordered]@{ ok = $true; windows = $items } | ConvertTo-Json -Compress -Depth 5
84
+ `;
85
+
86
+ const CAPTURE_DESKTOP_SCRIPT = String.raw`
87
+ Add-Type -AssemblyName System.Windows.Forms
88
+ Add-Type -AssemblyName System.Drawing
89
+ $bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
90
+ $bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
91
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
92
+ $graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size)
93
+ $stream = New-Object System.IO.MemoryStream
94
+ $bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
95
+ $graphics.Dispose(); $bitmap.Dispose()
96
+ [ordered]@{
97
+ ok = $true; data = [Convert]::ToBase64String($stream.ToArray()); mimeType = "image/png";
98
+ target = "desktop"; bounds = [ordered]@{ x = $bounds.Left; y = $bounds.Top; width = $bounds.Width; height = $bounds.Height }
99
+ } | ConvertTo-Json -Compress -Depth 5
100
+ `;
101
+
102
+ const CAPTURE_WINDOW_SCRIPT = String.raw`
103
+ Add-Type -AssemblyName System.Drawing
104
+ Add-Type -TypeDefinition @'
105
+ ${WIN32_TYPE}
106
+ '@
107
+ $rawId = '__WINDOW_ID__'
108
+ $hex = $rawId -replace '^0x',''
109
+ try { $hwnd = [IntPtr]::new([Convert]::ToInt64($hex, 16)) } catch {
110
+ [ordered]@{ ok = $false; message = "invalid windowId: $rawId" } | ConvertTo-Json -Compress; exit 0
111
+ }
112
+ if ([MarchWin32]::IsIconic($hwnd)) {
113
+ [ordered]@{ ok = $false; message = "window is minimized and cannot be captured" } | ConvertTo-Json -Compress; exit 0
114
+ }
115
+ $rect = New-Object MarchWin32+RECT
116
+ if (-not [MarchWin32]::GetWindowRect($hwnd, [ref]$rect)) {
117
+ [ordered]@{ ok = $false; message = "window not found: $rawId" } | ConvertTo-Json -Compress; exit 0
118
+ }
119
+ $width = $rect.Right - $rect.Left; $height = $rect.Bottom - $rect.Top
120
+ if ($width -le 0 -or $height -le 0) {
121
+ [ordered]@{ ok = $false; message = "window has empty bounds: $rawId" } | ConvertTo-Json -Compress; exit 0
122
+ }
123
+ $bitmap = New-Object System.Drawing.Bitmap $width, $height
124
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
125
+ $graphics.CopyFromScreen($rect.Left, $rect.Top, 0, 0, (New-Object System.Drawing.Size $width, $height))
126
+ $stream = New-Object System.IO.MemoryStream
127
+ $bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
128
+ $graphics.Dispose(); $bitmap.Dispose()
129
+ [ordered]@{
130
+ ok = $true; data = [Convert]::ToBase64String($stream.ToArray()); mimeType = "image/png";
131
+ target = "window"; windowId = $rawId; bounds = [ordered]@{ x = $rect.Left; y = $rect.Top; width = $width; height = $height }
132
+ } | ConvertTo-Json -Compress -Depth 5
133
+ `;
@@ -17,6 +17,7 @@ export function resolveRunnerSessionOptions({
17
17
  permissionController = null,
18
18
  authStorage = null,
19
19
  projectMarchDir = null,
20
+ getCurrentModel = null,
20
21
  }) {
21
22
  if (engine.cwd !== cwd) {
22
23
  throw new Error(`Runtime session cwd mismatch: engine=${engine.cwd}, session=${cwd}`);
@@ -28,7 +29,7 @@ export function resolveRunnerSessionOptions({
28
29
  ?? (provider && modelId ? getModel(provider, modelId) : null);
29
30
  if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
30
31
 
31
- const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, permissionController, authStorage, projectMarchDir });
32
+ const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, permissionController, authStorage, projectMarchDir, getCurrentModel: () => getCurrentModel?.() ?? model });
32
33
  const customToolNames = customTools.map((tool) => tool.name);
33
34
  const tools = [
34
35
  ...customToolNames.filter((name) => name === "read"),
@@ -0,0 +1,112 @@
1
+ export function formatToolStartLine(name, args = {}) {
2
+ if (name === "edit_file") {
3
+ const path = compactPath(args?.path ?? "");
4
+ const editCount = Array.isArray(args?.edits) ? args.edits.length : 0;
5
+ const mode = args?.mode ?? "patch";
6
+ const summary = mode === "patch" ? `${editCount} edit${editCount === 1 ? "" : "s"}` : mode;
7
+ return joinToolParts("◆", name, [path, summary]);
8
+ }
9
+ if (name === "command_exec") return joinToolParts("◆", name, [compactText(args?.command ?? "")]);
10
+ if (name === "terminal_send") return joinToolParts("◆", name, [args?.shell_id, formatTerminalSendAction(args)]);
11
+ if (name?.startsWith?.("terminal_")) return joinToolParts("◆", name, [args?.shell_id, formatTerminalDetails(args)]);
12
+ if (name === "external_web_search") return joinToolParts("◆", name, [quoteCompact(args?.query ?? "")]);
13
+ if (name === "web_fetch") return joinToolParts("◆", name, [compactText(args?.url ?? "")]);
14
+ if (name === "context_stats") return joinToolParts("◆", name, []);
15
+ if (name === "read") {
16
+ const path = compactPath(args?.path ?? args?.filePath ?? "");
17
+ return joinToolParts("→", name, [path, formatReadRange(args)]);
18
+ }
19
+ if (name === "grep") {
20
+ const path = compactPath(args?.path ?? "");
21
+ return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
22
+ }
23
+ if (name === "glob") {
24
+ const path = compactPath(args?.path ?? "");
25
+ return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
26
+ }
27
+ if (name === "find") {
28
+ const path = compactPath(args?.path ?? "");
29
+ return joinToolParts("✱", name, [quoteCompact(args?.pattern ?? ""), path]);
30
+ }
31
+ return joinToolParts("◆", name, [formatSmallOptions(args)]);
32
+ }
33
+
34
+ export function formatToolSuccessSummary(name, result, out = "") {
35
+ if (name === "grep") {
36
+ const matches = result?.details?.results?.length ?? countMatchLines(out);
37
+ return `${matches} match${matches === 1 ? "" : "es"}`;
38
+ }
39
+ if (name === "glob") {
40
+ const matches = Array.isArray(result?.details?.matches) ? result.details.matches.length : countNonEmptyLines(out);
41
+ return `${matches} file${matches === 1 ? "" : "s"}`;
42
+ }
43
+ if (name === "find") {
44
+ const matches = result?.details?.count ?? countNonEmptyLines(out);
45
+ return `${matches} file${matches === 1 ? "" : "s"}`;
46
+ }
47
+ if (name === "memory_open") {
48
+ return compactText(result?.details?.entry?.name ?? compactPath(result?.details?.path ?? ""));
49
+ }
50
+ return "";
51
+ }
52
+
53
+ function joinToolParts(icon, name, parts) {
54
+ const clean = parts.map((part) => String(part ?? "").trim()).filter(Boolean);
55
+ return `${icon} ${name}${clean.length ? ` · ${clean.join(" · ")}` : ""}`;
56
+ }
57
+
58
+ function formatReadRange(args = {}) {
59
+ if (args.offset == null && args.limit == null) return "";
60
+ if (args.offset != null && args.limit != null) return `lines ${args.offset}-${Number(args.offset) + Number(args.limit) - 1}`;
61
+ if (args.offset != null) return `from line ${args.offset}`;
62
+ return `limit ${args.limit}`;
63
+ }
64
+
65
+ function formatTerminalSendAction(args = {}) {
66
+ const hasText = typeof args.text === "string" && args.text.length > 0;
67
+ const key = args.key ? String(args.key) : "";
68
+ if (hasText && key) return `text+${key}`;
69
+ if (hasText) return args.text.includes("\n") || args.text.includes("\r") ? "text+enter" : "text";
70
+ return key || "send";
71
+ }
72
+
73
+ function formatTerminalDetails(args = {}) {
74
+ const details = [];
75
+ if (args.pattern) details.push(quoteCompact(args.pattern));
76
+ if (args.cols && args.rows) details.push(`${args.cols}x${args.rows}`);
77
+ if (args.command) details.push(compactText(args.command));
78
+ return details.join(" · ");
79
+ }
80
+
81
+ function formatSmallOptions(args = {}) {
82
+ const parts = [];
83
+ for (const [key, value] of Object.entries(args ?? {})) {
84
+ if (value == null || typeof value === "object") continue;
85
+ parts.push(`${key}=${compactText(value)}`);
86
+ if (parts.length >= 2) break;
87
+ }
88
+ return parts.join(", ");
89
+ }
90
+
91
+ function compactPath(path) {
92
+ return String(path ?? "").split(/[/\\]/).filter(Boolean).slice(-4).join("\\");
93
+ }
94
+
95
+ function quoteCompact(value) {
96
+ return JSON.stringify(compactText(value));
97
+ }
98
+
99
+ function compactText(value, limit = 80) {
100
+ const text = String(value ?? "").replace(/\s+/g, " ").trim();
101
+ return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
102
+ }
103
+
104
+ function countMatchLines(text) {
105
+ const match = String(text ?? "").match(/(\d+)\s+matches?\b/i);
106
+ if (match) return Number(match[1]);
107
+ return countNonEmptyLines(text);
108
+ }
109
+
110
+ function countNonEmptyLines(text) {
111
+ return String(text ?? "").split("\n").filter(Boolean).length;
112
+ }
@@ -1,22 +1,29 @@
1
- import { defineTool } from "@earendil-works/pi-coding-agent";
2
- import { Type } from "typebox";
3
1
  import { createCommandExecTool } from "./command-exec-tool.mjs";
4
2
  import { createContextStatsTool } from "./context-stats-tool.mjs";
5
3
  import { createEditFileTool } from "./file-edit-tool.mjs";
6
- import { createReadFileTool } from "./read-file-tool.mjs";
4
+ import { createReadFileTool } from "./file-tools/read-file-tool.mjs";
5
+ import { createReadImageTool } from "./file-tools/read-image-tool.mjs";
6
+ import { createScreenTool } from "./screen-tools/screen-tool.mjs";
7
+ import { createListWindowsTool } from "./screen-tools/list-windows-tool.mjs";
7
8
  import { toolText } from "./tool-result.mjs";
8
9
  import { createShellTools } from "../shell/tools.mjs";
9
- import { createWebTools } from "../web/tools.mjs";
10
10
  import { initImageGen } from "../image-gen/index.mjs";
11
11
  import { createSuperGrokTool } from "../supergrok/tool.mjs";
12
- export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], permissionController = null, authStorage = null, projectMarchDir = null }) {
12
+
13
+ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], permissionController = null, authStorage = null, projectMarchDir = null, getCurrentModel = null }) {
13
14
  const commandExecTool = createCommandExecTool({ cwd });
14
15
  const contextStatsTool = createContextStatsTool({ engine });
15
16
  const editFileTool = createEditFileTool({ engine, ui, lspService });
16
17
  const readFileTool = createReadFileTool({ engine });
18
+ const readImageTool = createReadImageTool({ engine, getCurrentModel });
19
+ const screenTool = createScreenTool({ getCurrentModel });
20
+ const listWindowsTool = createListWindowsTool();
17
21
 
18
22
  const tools = [
19
23
  readFileTool,
24
+ readImageTool,
25
+ screenTool,
26
+ listWindowsTool,
20
27
  contextStatsTool,
21
28
  commandExecTool,
22
29
  editFileTool,