march-cli 0.1.13 → 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.
- package/package.json +2 -1
- package/src/agent/{read-file-tool.mjs → file-tools/read-file-tool.mjs} +2 -2
- package/src/agent/file-tools/read-image-tool.mjs +76 -0
- package/src/agent/runner.mjs +1 -0
- package/src/agent/runtime/runner-runtime-host.mjs +1 -0
- package/src/agent/screen-tools/list-windows-tool.mjs +39 -0
- package/src/agent/screen-tools/screen-tool.mjs +49 -0
- package/src/agent/screen-tools/windows-screen.mjs +133 -0
- package/src/agent/session/session-options.mjs +2 -1
- package/src/agent/tools.mjs +12 -5
- package/src/agent/vision-capability.mjs +14 -0
- package/src/cli/args.mjs +8 -0
- package/src/cli/tui/status/status-bar.mjs +2 -1
- package/src/config/config-json.mjs +11 -0
- package/src/context/system-core/base.md +2 -1
- package/src/main.mjs +7 -6
- package/src/provider/accept-command.mjs +89 -0
- package/src/provider/command.mjs +21 -0
- package/src/provider/custom-provider.mjs +5 -4
- package/src/provider/share-command.mjs +79 -0
- package/src/provider/share-payload.mjs +52 -0
- package/src/supergrok/tool.mjs +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "march-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "March CLI — terminal-native coding agent with context reconstruction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.mjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"dev": "cd .. && node march-cli/bin/march.mjs",
|
|
16
16
|
"test": "node test/smoke.test.mjs",
|
|
17
|
+
"test:fast": "node test/fast.test.mjs",
|
|
17
18
|
"test:real": "npm run test:shell-runtime-real && npm run test:shell-tui-real && npm run test:tui-key-real",
|
|
18
19
|
"test:shell-runtime-real": "node test/shell-real-runtime.acceptance.mjs",
|
|
19
20
|
"test:shell-tui-real": "node test/shell-tui-real.acceptance.mjs",
|
|
@@ -2,7 +2,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import { toolText } from "
|
|
5
|
+
import { toolText } from "../tool-result.mjs";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_LIMIT = 30;
|
|
8
8
|
const DEFAULT_DIRECTORY_LIMIT = 200;
|
|
@@ -47,7 +47,7 @@ export function readFileSlice({ engine, path, offset = 1, limit = DEFAULT_LIMIT
|
|
|
47
47
|
const count = clampLimit(limit);
|
|
48
48
|
const selected = lines.slice(start - 1, start - 1 + count);
|
|
49
49
|
const end = start + selected.length - 1;
|
|
50
|
-
const body = selected.map((line, index) => `${start + index}
|
|
50
|
+
const body = selected.map((line, index) => `${start + index}| ${line}`).join("\n");
|
|
51
51
|
const header = `--- ${absPath} (lines ${start}-${end} of ${lines.length}) ---`;
|
|
52
52
|
const remaining = lines.length - end;
|
|
53
53
|
const footer = remaining > 0 ? `\n\n[${remaining} more lines in file. Use offset=${end + 1} to continue.]` : "";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { currentModelImageInputError } from "../vision-capability.mjs";
|
|
6
|
+
const IMAGE_MIME_BY_EXT = new Map([
|
|
7
|
+
[".png", "image/png"],
|
|
8
|
+
[".jpg", "image/jpeg"],
|
|
9
|
+
[".jpeg", "image/jpeg"],
|
|
10
|
+
[".webp", "image/webp"],
|
|
11
|
+
[".gif", "image/gif"],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function createReadImageTool({ engine, getCurrentModel = null }) {
|
|
15
|
+
return defineTool({
|
|
16
|
+
name: "read_image",
|
|
17
|
+
label: "Read Image",
|
|
18
|
+
description: "Read a local image file and send it to the model as an image attachment. Supports png, jpg/jpeg, webp, and gif.",
|
|
19
|
+
parameters: Type.Object({
|
|
20
|
+
path: Type.String({ description: "Absolute or relative path to the image file" }),
|
|
21
|
+
}),
|
|
22
|
+
execute: async (_toolCallId, params) => {
|
|
23
|
+
const capabilityError = currentModelImageInputError(getCurrentModel);
|
|
24
|
+
if (capabilityError) return imageError(capabilityError, { unsupportedModel: true });
|
|
25
|
+
return readImageFile({ engine, ...params });
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readImageFile({ engine, path }) {
|
|
31
|
+
const absPath = engine.resolvePath(path);
|
|
32
|
+
let stat;
|
|
33
|
+
try {
|
|
34
|
+
stat = statSync(absPath);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return imageError(`Error reading image ${absPath}: ${err.message}`, { path: absPath });
|
|
37
|
+
}
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
return imageError(`Error reading image ${absPath}: this is a directory. Use ls(path) or find(pattern, path) to inspect it.`, { path: absPath, isDirectory: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mimeType = IMAGE_MIME_BY_EXT.get(extname(absPath).toLowerCase());
|
|
43
|
+
if (!mimeType) {
|
|
44
|
+
return imageError(`Error reading image ${absPath}: unsupported image type. Supported types: png, jpg, jpeg, webp, gif.`, { path: absPath });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
data = readFileSync(absPath).toString("base64");
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return imageError(`Error reading image ${absPath}: ${err.message}`, { path: absPath });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const size = formatSize(stat.size);
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{ type: "text", text: `Read image file: ${absPath}\nMIME: ${mimeType}\nSize: ${size}` },
|
|
58
|
+
{ type: "image", data, mimeType },
|
|
59
|
+
],
|
|
60
|
+
details: {
|
|
61
|
+
path: absPath,
|
|
62
|
+
mimeType,
|
|
63
|
+
sizeBytes: stat.size,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function imageError(text, details = {}) {
|
|
69
|
+
return { content: [{ type: "text", text }], details: { ...details, error: true } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatSize(bytes) {
|
|
73
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
74
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
75
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
76
|
+
}
|
package/src/agent/runner.mjs
CHANGED
|
@@ -83,6 +83,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
83
83
|
cwd, provider, modelId, modelRegistry, engine, ui: runtimeUi,
|
|
84
84
|
memoryTools, shellRuntime, lspService, mcpTools, webTools, permissionController,
|
|
85
85
|
authStorage: resolvedAuth, projectMarchDir,
|
|
86
|
+
getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
|
|
86
87
|
});
|
|
87
88
|
const { session } = await createAgentSessionImpl({
|
|
88
89
|
cwd, agentDir: stateRoot, ...sessionOptions,
|
|
@@ -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"),
|
package/src/agent/tools.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function modelSupportsImageInput(model) {
|
|
2
|
+
if (!model || typeof model !== "object") return false;
|
|
3
|
+
if (Array.isArray(model.input) && model.input.includes("image")) return true;
|
|
4
|
+
if (model.capabilities?.images === true || model.capabilities?.vision === true) return true;
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function currentModelImageInputError(getCurrentModel) {
|
|
9
|
+
if (typeof getCurrentModel !== "function") return null;
|
|
10
|
+
const model = getCurrentModel();
|
|
11
|
+
if (modelSupportsImageInput(model)) return null;
|
|
12
|
+
const label = model ? `${model.name || model.id || "unknown"} (${model.provider || "unknown provider"})` : "unknown";
|
|
13
|
+
return `Current model does not support image input: ${label}. Switch to a vision-capable model before using read_image or screen.`;
|
|
14
|
+
}
|
package/src/cli/args.mjs
CHANGED
|
@@ -11,6 +11,8 @@ export function parseCliArgs(argv) {
|
|
|
11
11
|
extension: { type: "string", short: "e", multiple: true },
|
|
12
12
|
extension: { type: "string", short: "e", multiple: true },
|
|
13
13
|
config: { type: "boolean" },
|
|
14
|
+
"include-key": { type: "boolean" },
|
|
15
|
+
"profile-only": { type: "boolean" },
|
|
14
16
|
"dump-context": { type: "boolean" },
|
|
15
17
|
"pi-sessions": { type: "boolean" },
|
|
16
18
|
"pi-runtime-host": { type: "boolean" },
|
|
@@ -33,6 +35,8 @@ export function parseCliArgs(argv) {
|
|
|
33
35
|
extensions: values.extension ?? [],
|
|
34
36
|
dumpContext: values["dump-context"] ?? false,
|
|
35
37
|
providerConfig: values.config ?? false,
|
|
38
|
+
includeKey: values["include-key"] ?? false,
|
|
39
|
+
profileOnly: values["profile-only"] ?? false,
|
|
36
40
|
piSessions: values["pi-sessions"] ?? false,
|
|
37
41
|
piRuntimeHost: values["pi-runtime-host"] ?? false,
|
|
38
42
|
shellRuntime: values["no-shell-runtime"] ? false : true,
|
|
@@ -50,6 +54,8 @@ Usage:
|
|
|
50
54
|
march [options] (starts REPL)
|
|
51
55
|
march login [provider] Login to an OAuth provider
|
|
52
56
|
march provider --config Configure provider credentials
|
|
57
|
+
march provider share [id] Share a provider profile
|
|
58
|
+
march provider accept <token>
|
|
53
59
|
march websearch --config Configure web search credentials
|
|
54
60
|
|
|
55
61
|
Options:
|
|
@@ -58,6 +64,8 @@ Options:
|
|
|
58
64
|
--resume <id> Resume a pi session by default
|
|
59
65
|
--json JSON output mode (no TUI)
|
|
60
66
|
--config With provider/websearch command, open configuration
|
|
67
|
+
--include-key With provider share, include API key
|
|
68
|
+
--profile-only With provider share, omit API key
|
|
61
69
|
--dump-context Write every prompt sent to the model under .march/context-dumps/
|
|
62
70
|
--pi-sessions Force pi JSONL SessionManager persistence
|
|
63
71
|
--pi-runtime-host Force pi AgentSessionRuntime host path
|
|
@@ -75,7 +75,8 @@ export class StatusBar {
|
|
|
75
75
|
const mode = formatModeLabel(parts.mode || DEFAULT_STATUS_TEXT);
|
|
76
76
|
const activity = parts.activity ? statusBar.muted(`${parts.activity} · `) : "";
|
|
77
77
|
const right = [parts.model, parts.thinking].filter(Boolean).join(" • ");
|
|
78
|
-
const
|
|
78
|
+
const paintWidth = inputPaintWidth(innerWidth);
|
|
79
|
+
const line = composeMetaLine({ left: `${activity}${mode}`, right, width: paintWidth, muteLeft: false });
|
|
79
80
|
return ["", `${insetLeft}${line}${insetRight}`];
|
|
80
81
|
}
|
|
81
82
|
}
|
|
@@ -38,6 +38,17 @@ export function upsertProviderProfile({ path = globalConfigJsonPath(), id, type,
|
|
|
38
38
|
return config;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function upsertSharedProviderProfile({ path = globalConfigJsonPath(), id, provider }) {
|
|
42
|
+
const config = readConfigJson(path);
|
|
43
|
+
const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
|
|
44
|
+
? config.providers
|
|
45
|
+
: {};
|
|
46
|
+
providers[id] = provider;
|
|
47
|
+
config.providers = providers;
|
|
48
|
+
writeConfigJson(path, config);
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export function upsertModelSelection({ path = globalConfigJsonPath(), provider, model, serviceTier }) {
|
|
42
53
|
const config = readConfigJson(path);
|
|
43
54
|
config.provider = provider;
|
|
@@ -53,7 +53,8 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
|
|
|
53
53
|
</git_contract>
|
|
54
54
|
|
|
55
55
|
<memory_system>
|
|
56
|
-
- [memory_hint source="..."] blocks in recent_chat
|
|
56
|
+
- [memory_hint source="..."] blocks in recent_chat are lightweight recall hints matched from prior thinking output. Treat them as possibly relevant pointers, not as complete facts.
|
|
57
|
+
- If a memory hint may help the current task, use memory_open(id) to read the full memory before relying on it. Ignore hints that are clearly unrelated or too low-value for the task.
|
|
57
58
|
- Use memory_search(query) for full-text search across all memories.
|
|
58
59
|
- To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
|
|
59
60
|
- Use memory_save() to create memories or update whole fields. Before creating a new memory, first search/open related memories and merge updates into an existing memory when they share the same topic, project, or decision thread; prefer modifying the existing memory file over creating a scattered new one. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
|
package/src/main.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { createWebToolsFromConfig } from "./web/tools.mjs";
|
|
|
30
30
|
import { createModelContextDumper } from "./debug/model-context-dumper.mjs";
|
|
31
31
|
import { createLogger, installProcessLogHandlers } from "./debug/logger.mjs";
|
|
32
32
|
import { defaultProfilePaths, ensureProfileFiles } from "./context/profiles.mjs";
|
|
33
|
-
import {
|
|
33
|
+
import { runProviderCommand } from "./provider/command.mjs";
|
|
34
34
|
import { runWebSearchConfigCommand } from "./web/config-command.mjs";
|
|
35
35
|
import { createDesktopTurnNotifier } from "./notification/desktop-notifier.mjs";
|
|
36
36
|
import { registerSuperGrokOAuthProvider } from "./supergrok/oauth-provider.mjs";
|
|
@@ -60,11 +60,12 @@ export async function run(argv) {
|
|
|
60
60
|
return 1;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
if (args.command?.name === "provider") {
|
|
64
|
+
return await runProviderCommand(args);
|
|
65
|
+
}
|
|
66
|
+
if (args.command?.name === "websearch") {
|
|
67
|
+
if (args.providerConfig) return await runWebSearchConfigCommand({ homeDir: homedir() });
|
|
68
|
+
process.stderr.write("Usage: march websearch --config\n");
|
|
68
69
|
return 1;
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { globalConfigJsonPath, readConfigJson, upsertSharedProviderProfile } from "../config/config-json.mjs";
|
|
2
|
+
import { selectWithKeyboard } from "../cli/input/select-with-keyboard.mjs";
|
|
3
|
+
import { hasApiKey, parseProviderShareToken } from "./share-payload.mjs";
|
|
4
|
+
|
|
5
|
+
export async function runProviderAcceptCommand({
|
|
6
|
+
homeDir,
|
|
7
|
+
token,
|
|
8
|
+
input = process.stdin,
|
|
9
|
+
output = process.stdout,
|
|
10
|
+
select = selectWithKeyboard,
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (!token) {
|
|
13
|
+
output.write("Usage: march provider accept <march-provider-v1-token>\n");
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let payload;
|
|
18
|
+
try {
|
|
19
|
+
payload = parseProviderShareToken(token);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
output.write(`Invalid provider share token: ${error.message}\n`);
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
output.write(formatImportPreview(payload));
|
|
26
|
+
const path = globalConfigJsonPath(homeDir);
|
|
27
|
+
const config = readConfigJson(path);
|
|
28
|
+
const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers) ? config.providers : {};
|
|
29
|
+
if (providers[payload.providerId]) {
|
|
30
|
+
const action = await select({
|
|
31
|
+
input,
|
|
32
|
+
output,
|
|
33
|
+
message: `Provider "${payload.providerId}" already exists`,
|
|
34
|
+
items: [
|
|
35
|
+
{ label: "Overwrite existing provider", value: "overwrite" },
|
|
36
|
+
{ label: "Cancel", value: "cancel" },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
if (action !== "overwrite") {
|
|
40
|
+
output.write("Provider import cancelled.\n");
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
const action = await select({
|
|
45
|
+
input,
|
|
46
|
+
output,
|
|
47
|
+
message: "Import provider?",
|
|
48
|
+
items: [
|
|
49
|
+
{ label: "Import", value: "import" },
|
|
50
|
+
{ label: "Cancel", value: "cancel" },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
if (action !== "import") {
|
|
54
|
+
output.write("Provider import cancelled.\n");
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
upsertSharedProviderProfile({ path, id: payload.providerId, provider: payload.provider });
|
|
60
|
+
output.write(`Imported provider: ${payload.providerId}\n`);
|
|
61
|
+
output.write(`Config: ${path}\n`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatImportPreview(payload) {
|
|
66
|
+
const provider = payload.provider;
|
|
67
|
+
const models = Array.isArray(provider.models) ? provider.models : [];
|
|
68
|
+
const lines = [
|
|
69
|
+
"Provider to import:",
|
|
70
|
+
` Id: ${payload.providerId}`,
|
|
71
|
+
` Name: ${typeof provider.name === "string" && provider.name ? provider.name : "-"}`,
|
|
72
|
+
` Type: ${provider.type}`,
|
|
73
|
+
];
|
|
74
|
+
if (typeof provider.baseUrl === "string" && provider.baseUrl) lines.push(` Base URL: ${provider.baseUrl}`);
|
|
75
|
+
if (typeof provider.api === "string" && provider.api) lines.push(` API: ${provider.api}`);
|
|
76
|
+
if (models.length) {
|
|
77
|
+
lines.push(` Models: ${models.length}`);
|
|
78
|
+
for (const model of models.slice(0, 5)) lines.push(` - ${formatModel(model)}`);
|
|
79
|
+
if (models.length > 5) lines.push(` ... ${models.length - 5} more`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(` API key: ${hasApiKey(provider) ? "included" : "not included"}`);
|
|
82
|
+
lines.push("");
|
|
83
|
+
return `${lines.join("\n")}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatModel(model) {
|
|
87
|
+
if (model && typeof model === "object" && !Array.isArray(model)) return model.name || model.id || "<unnamed>";
|
|
88
|
+
return String(model);
|
|
89
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { runProviderConfigCommand } from "./config-command.mjs";
|
|
3
|
+
import { runProviderShareCommand } from "./share-command.mjs";
|
|
4
|
+
import { runProviderAcceptCommand } from "./accept-command.mjs";
|
|
5
|
+
|
|
6
|
+
export async function runProviderCommand(args, { homeDir = homedir(), stderr = process.stderr } = {}) {
|
|
7
|
+
if (args.providerConfig) return await runProviderConfigCommand({ homeDir });
|
|
8
|
+
if (args.command.args[0] === "share") {
|
|
9
|
+
return await runProviderShareCommand({
|
|
10
|
+
homeDir,
|
|
11
|
+
providerId: args.command.args[1],
|
|
12
|
+
includeKey: args.includeKey,
|
|
13
|
+
profileOnly: args.profileOnly,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (args.command.args[0] === "accept") {
|
|
17
|
+
return await runProviderAcceptCommand({ homeDir, token: args.command.args[1] });
|
|
18
|
+
}
|
|
19
|
+
stderr.write("Usage: march provider --config | march provider share [id] | march provider accept <token>\n");
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
@@ -52,7 +52,7 @@ function normalizeModels(providerId, models, { api, baseUrl }) {
|
|
|
52
52
|
api: normalizeApi(providerId, model.api ?? api),
|
|
53
53
|
baseUrl: typeof model.baseUrl === "string" && model.baseUrl.trim() ? model.baseUrl : baseUrl,
|
|
54
54
|
reasoning: typeof model.reasoning === "boolean" ? model.reasoning : false,
|
|
55
|
-
input: normalizeInput(model.input),
|
|
55
|
+
input: normalizeInput(model.input, model.capabilities),
|
|
56
56
|
cost: normalizeCost(model.cost),
|
|
57
57
|
contextWindow: normalizePositiveNumber(model.contextWindow, 128000),
|
|
58
58
|
maxTokens: normalizePositiveNumber(model.maxTokens, 4096),
|
|
@@ -75,9 +75,10 @@ function requireString(providerId, field, value) {
|
|
|
75
75
|
return value;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function normalizeInput(input) {
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
function normalizeInput(input, capabilities = null) {
|
|
79
|
+
const normalized = Array.isArray(input) ? input.filter((item) => item === "text" || item === "image") : [];
|
|
80
|
+
if ((capabilities?.images === true || capabilities?.vision === true) && !normalized.includes("image")) normalized.push("image");
|
|
81
|
+
if (!normalized.includes("text")) normalized.unshift("text");
|
|
81
82
|
return normalized.length > 0 ? normalized : ["text"];
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { globalConfigJsonPath, readConfigJson } from "../config/config-json.mjs";
|
|
2
|
+
import { selectWithKeyboard } from "../cli/input/select-with-keyboard.mjs";
|
|
3
|
+
import { cloneProviderForShare, createProviderShareToken, hasApiKey } from "./share-payload.mjs";
|
|
4
|
+
|
|
5
|
+
export async function runProviderShareCommand({
|
|
6
|
+
homeDir,
|
|
7
|
+
providerId,
|
|
8
|
+
includeKey = false,
|
|
9
|
+
profileOnly = false,
|
|
10
|
+
input = process.stdin,
|
|
11
|
+
output = process.stdout,
|
|
12
|
+
select = selectWithKeyboard,
|
|
13
|
+
} = {}) {
|
|
14
|
+
if (includeKey && profileOnly) {
|
|
15
|
+
output.write("Choose either --include-key or --profile-only, not both.\n");
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config = readConfigJson(globalConfigJsonPath(homeDir));
|
|
20
|
+
const providers = normalizeProviders(config.providers);
|
|
21
|
+
const selectedProviderId = providerId ?? await selectProvider({ providers, input, output, select });
|
|
22
|
+
if (!selectedProviderId) {
|
|
23
|
+
output.write(Object.keys(providers).length ? "Provider share cancelled.\n" : "No providers configured. Run: march provider --config\n");
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const provider = providers[selectedProviderId];
|
|
28
|
+
if (!provider) {
|
|
29
|
+
output.write(`Provider not found: ${selectedProviderId}\n`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const includeApiKey = includeKey || (!profileOnly && await selectShareMode({ input, output, select }));
|
|
34
|
+
const sharedProvider = cloneProviderForShare(provider, { includeApiKey });
|
|
35
|
+
const token = createProviderShareToken({
|
|
36
|
+
providerId: selectedProviderId,
|
|
37
|
+
provider: sharedProvider,
|
|
38
|
+
mode: includeApiKey ? "full" : "profile-only",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
output.write(`Provider: ${selectedProviderId}\n`);
|
|
42
|
+
output.write(`Mode: ${includeApiKey ? "Full config, including API key" : "Profile only, without API key"}\n`);
|
|
43
|
+
output.write(`API key: ${hasApiKey(sharedProvider) ? "included" : "not included"}\n\n`);
|
|
44
|
+
output.write(`march provider accept ${token}\n`);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeProviders(providers) {
|
|
49
|
+
return providers && typeof providers === "object" && !Array.isArray(providers) ? providers : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function selectProvider({ providers, input, output, select }) {
|
|
53
|
+
const items = Object.entries(providers).map(([id, provider]) => ({
|
|
54
|
+
value: id,
|
|
55
|
+
label: formatProviderLabel(id, provider),
|
|
56
|
+
}));
|
|
57
|
+
return await select({ input, output, message: "Choose provider to share", items });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function selectShareMode({ input, output, select }) {
|
|
61
|
+
const mode = await select({
|
|
62
|
+
input,
|
|
63
|
+
output,
|
|
64
|
+
message: "Choose share mode",
|
|
65
|
+
items: [
|
|
66
|
+
{ label: "Full config, including API key", value: "full" },
|
|
67
|
+
{ label: "Profile only, without API key", value: "profile-only" },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
return mode === "full";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatProviderLabel(id, provider) {
|
|
74
|
+
const name = typeof provider?.name === "string" && provider.name ? provider.name : "-";
|
|
75
|
+
const type = typeof provider?.type === "string" && provider.type ? provider.type : "unknown";
|
|
76
|
+
const modelCount = Array.isArray(provider?.models) ? `${provider.models.length} model${provider.models.length === 1 ? "" : "s"}` : "built-in";
|
|
77
|
+
const key = hasApiKey(provider) ? "API key configured" : "no API key";
|
|
78
|
+
return `${id} ${name} ${type} ${modelCount} ${key}`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const KIND = "march.provider.share";
|
|
2
|
+
const VERSION = 1;
|
|
3
|
+
const PREFIX = "march-provider-v1.";
|
|
4
|
+
|
|
5
|
+
export function createProviderShareToken({ providerId, provider, mode }) {
|
|
6
|
+
const payload = {
|
|
7
|
+
kind: KIND,
|
|
8
|
+
version: VERSION,
|
|
9
|
+
mode,
|
|
10
|
+
containsApiKey: hasApiKey(provider),
|
|
11
|
+
providerId,
|
|
12
|
+
provider,
|
|
13
|
+
};
|
|
14
|
+
return `${PREFIX}${Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseProviderShareToken(token) {
|
|
18
|
+
const text = String(token ?? "").trim();
|
|
19
|
+
if (!text.startsWith(PREFIX)) throw new Error(`Provider share token must start with ${PREFIX}`);
|
|
20
|
+
let payload;
|
|
21
|
+
try {
|
|
22
|
+
payload = JSON.parse(Buffer.from(text.slice(PREFIX.length), "base64url").toString("utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error("Invalid provider share token");
|
|
25
|
+
}
|
|
26
|
+
validateProviderSharePayload(payload);
|
|
27
|
+
return payload;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function cloneProviderForShare(provider, { includeApiKey }) {
|
|
31
|
+
const clone = structuredClone(provider);
|
|
32
|
+
if (!includeApiKey && clone.auth && typeof clone.auth === "object" && !Array.isArray(clone.auth)) {
|
|
33
|
+
delete clone.auth.apiKey;
|
|
34
|
+
}
|
|
35
|
+
return clone;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasApiKey(provider) {
|
|
39
|
+
return typeof provider?.auth?.apiKey === "string" && provider.auth.apiKey.length > 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateProviderSharePayload(payload) {
|
|
43
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("Invalid provider share payload");
|
|
44
|
+
if (payload.kind !== KIND || payload.version !== VERSION) throw new Error("Unsupported provider share token");
|
|
45
|
+
if (typeof payload.providerId !== "string" || !payload.providerId.trim()) throw new Error("Provider share token is missing providerId");
|
|
46
|
+
validateProviderProfile(payload.provider);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateProviderProfile(provider) {
|
|
50
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) throw new Error("Provider share token is missing provider config");
|
|
51
|
+
if (typeof provider.type !== "string" || !provider.type.trim()) throw new Error("Provider config is missing type");
|
|
52
|
+
}
|
package/src/supergrok/tool.mjs
CHANGED
|
@@ -13,14 +13,14 @@ export function createSuperGrokTool({ authStorage, projectMarchDir, resolveCrede
|
|
|
13
13
|
name: "supergrok",
|
|
14
14
|
label: "SuperGrok",
|
|
15
15
|
description:
|
|
16
|
-
"Use SuperGrok
|
|
17
|
-
"
|
|
18
|
-
promptSnippet: "supergrok(action, query, options?) -
|
|
16
|
+
"Use SuperGrok for complex research tasks that would otherwise require multiple searches or source comparison. " +
|
|
17
|
+
"It is backed by an agent team that can search repeatedly, verify across sources, and synthesize an answer.",
|
|
18
|
+
promptSnippet: "supergrok(action, query, options?) - Prefer SuperGrok for complex web/X research or Grok image generation",
|
|
19
19
|
promptGuidelines: [
|
|
20
|
-
"
|
|
21
|
-
"Use action=x_search for
|
|
20
|
+
"Prefer action=web_search for broad, ambiguous, current, or multi-step research.",
|
|
21
|
+
"Use action=x_search for targeted X/Twitter posts, reactions, profiles, and threads.",
|
|
22
22
|
"Use action=image_generate when the user asks Grok/SuperGrok to create an image.",
|
|
23
|
-
"
|
|
23
|
+
"Use narrower tools instead for simple lookups, exact URL fetching, targeted X search, or non-Grok image generation.",
|
|
24
24
|
],
|
|
25
25
|
parameters: Type.Object({
|
|
26
26
|
action: Type.String({ enum: ACTIONS, description: "SuperGrok capability to invoke" }),
|