Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f4d11e4c4 | ||
|
|
14763a002a |
@@ -220,6 +220,8 @@ namespace QuickClaude
|
|||||||
Finished?.Invoke(error);
|
Finished?.Invoke(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File writes also go through a guard hook ("QuickClaude.exe --guard-write") that blocks the
|
||||||
|
// Desktop and AppData, where OpenRules lets things open without asking.
|
||||||
// Shell commands go through QuickClaude's own confirm box: a PreToolUse hook that runs
|
// Shell commands go through QuickClaude's own confirm box: a PreToolUse hook that runs
|
||||||
// "QuickClaude.exe --confirm" (exec form, so no shell quoting). The dialog denies after
|
// "QuickClaude.exe --confirm" (exec form, so no shell quoting). The dialog denies after
|
||||||
// 2 minutes; the hook timeout is a bit longer so Claude Code never cuts it off first.
|
// 2 minutes; the hook timeout is a bit longer so Claude Code never cuts it off first.
|
||||||
@@ -237,6 +239,12 @@ namespace QuickClaude
|
|||||||
{
|
{
|
||||||
matcher = "Bash|PowerShell|Monitor", // Monitor also runs shell commands
|
matcher = "Bash|PowerShell|Monitor", // Monitor also runs shell commands
|
||||||
hooks = new[] { new { type = "command", command = exe, args = new[] { "--confirm" }, timeout = 150 } }
|
hooks = new[] { new { type = "command", command = exe, args = new[] { "--confirm" }, timeout = 150 } }
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
// Never lets Claude write to the Desktop or AppData (see WriteGuard)
|
||||||
|
matcher = "Write|Edit|MultiEdit|NotebookEdit",
|
||||||
|
hooks = new[] { new { type = "command", command = exe, args = new[] { "--guard-write" }, timeout = 30 } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+134
-6
@@ -20,8 +20,12 @@ namespace QuickClaude
|
|||||||
readonly TextBox input = new TextBox();
|
readonly TextBox input = new TextBox();
|
||||||
readonly TextBlock status = new TextBlock();
|
readonly TextBlock status = new TextBlock();
|
||||||
readonly TextBlock grip = new TextBlock();
|
readonly TextBlock grip = new TextBlock();
|
||||||
|
// Holds the transcript. The transcript inside is always sized to a whole number of lines
|
||||||
|
// and sits at the bottom, so no line is ever shown cut in half (see FitTranscript).
|
||||||
|
readonly Border transcriptHost = new Border { ClipToBounds = true };
|
||||||
|
double lineHeight = 13;
|
||||||
WF.NotifyIcon tray;
|
WF.NotifyIcon tray;
|
||||||
WF.ToolStripMenuItem fontMenu, sizeMenu, topItem, blackItem, whiteItem, lookItem, editItem, autoItem, folderItem;
|
WF.ToolStripMenuItem fontMenu, sizeMenu, snapItem, topItem, blackItem, whiteItem, lookItem, editItem, autoItem, folderItem;
|
||||||
bool lastWasTool;
|
bool lastWasTool;
|
||||||
bool quitting;
|
bool quitting;
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ namespace QuickClaude
|
|||||||
if (transcript.Text.Length == 0)
|
if (transcript.Text.Length == 0)
|
||||||
transcript.Text = "QuickClaude. Type below and press Enter. Shift+Enter for a new line, Esc stops Claude, Ctrl+Alt+C hides or shows this note.\n";
|
transcript.Text = "QuickClaude. Type below and press Enter. Shift+Enter for a new line, Esc stops Claude, Ctrl+Alt+C hides or shows this note.\n";
|
||||||
|
|
||||||
Loaded += (s, e) => { transcript.ScrollToEnd(); input.Focus(); };
|
Loaded += (s, e) => { MeasureLineHeight(); transcript.ScrollToEnd(); input.Focus(); };
|
||||||
PreviewMouseLeftButtonUp += ClickToInput;
|
PreviewMouseLeftButtonUp += ClickToInput;
|
||||||
SourceInitialized += (s, e) => RegisterHotkey();
|
SourceInitialized += (s, e) => RegisterHotkey();
|
||||||
LocationChanged += (s, e) => SavePlace();
|
LocationChanged += (s, e) => SavePlace();
|
||||||
@@ -119,8 +123,12 @@ namespace QuickClaude
|
|||||||
transcript.TextWrapping = TextWrapping.Wrap;
|
transcript.TextWrapping = TextWrapping.Wrap;
|
||||||
transcript.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
|
transcript.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
|
||||||
transcript.IsUndoEnabled = false;
|
transcript.IsUndoEnabled = false;
|
||||||
Grid.SetRow(transcript, 1);
|
transcript.VerticalAlignment = VerticalAlignment.Bottom;
|
||||||
rootGrid.Children.Add(transcript);
|
transcriptHost.Background = HitFill;
|
||||||
|
transcriptHost.Child = transcript;
|
||||||
|
transcriptHost.SizeChanged += (s, e) => FitTranscript();
|
||||||
|
Grid.SetRow(transcriptHost, 1);
|
||||||
|
rootGrid.Children.Add(transcriptHost);
|
||||||
|
|
||||||
// Input line
|
// Input line
|
||||||
var bottom = new Grid { Margin = new Thickness(0, 6, 0, 0) };
|
var bottom = new Grid { Margin = new Thickness(0, 6, 0, 0) };
|
||||||
@@ -150,6 +158,35 @@ namespace QuickClaude
|
|||||||
((Grid)Content).ContextMenu = DarkMenu.Make(null, NewConversation);
|
((Grid)Content).ContextMenu = DarkMenu.Make(null, NewConversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Height of one line of transcript text at the current font and size. Measured as the
|
||||||
|
// difference between two lines and one, so it's exactly what the text box steps by.
|
||||||
|
void MeasureLineHeight()
|
||||||
|
{
|
||||||
|
var probe = new TextBlock { FontFamily = Mono, FontSize = TextSize, Text = "Mg" };
|
||||||
|
probe.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||||
|
double one = probe.DesiredSize.Height;
|
||||||
|
probe.Text = "Mg\nMg";
|
||||||
|
probe.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||||
|
double two = probe.DesiredSize.Height;
|
||||||
|
lineHeight = two - one > 1 ? two - one : Math.Max(1, one);
|
||||||
|
FitTranscript();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the transcript exactly as tall as the whole lines that fit, bottom-aligned, and keep
|
||||||
|
// it scrolled to the end so the top line is whole too. Runs on every size change (window
|
||||||
|
// resize, the input box growing, font changes).
|
||||||
|
void FitTranscript()
|
||||||
|
{
|
||||||
|
double avail = transcriptHost.ActualHeight;
|
||||||
|
if (avail <= 0 || lineHeight <= 0) return;
|
||||||
|
int lines = Math.Max(1, (int)Math.Floor((avail + 0.01) / lineHeight));
|
||||||
|
transcript.Height = lines * lineHeight;
|
||||||
|
transcript.ScrollToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leftover space under the last whole line, in WPF units
|
||||||
|
double TranscriptGap() => Math.Max(0, transcriptHost.ActualHeight - (double.IsNaN(transcript.Height) ? 0 : transcript.Height));
|
||||||
|
|
||||||
void SetFontSize(double size)
|
void SetFontSize(double size)
|
||||||
{
|
{
|
||||||
size = Math.Clamp(size, MinTextSize, MaxTextSize);
|
size = Math.Clamp(size, MinTextSize, MaxTextSize);
|
||||||
@@ -161,6 +198,7 @@ namespace QuickClaude
|
|||||||
input.FontSize = size;
|
input.FontSize = size;
|
||||||
status.FontSize = size;
|
status.FontSize = size;
|
||||||
grip.FontSize = size;
|
grip.FontSize = size;
|
||||||
|
MeasureLineHeight();
|
||||||
BuildContextMenus();
|
BuildContextMenus();
|
||||||
transcript.ScrollToEnd();
|
transcript.ScrollToEnd();
|
||||||
if (sizeMenu != null)
|
if (sizeMenu != null)
|
||||||
@@ -179,6 +217,7 @@ namespace QuickClaude
|
|||||||
input.FontFamily = Mono;
|
input.FontFamily = Mono;
|
||||||
status.FontFamily = Mono;
|
status.FontFamily = Mono;
|
||||||
grip.FontFamily = Mono;
|
grip.FontFamily = Mono;
|
||||||
|
MeasureLineHeight();
|
||||||
BuildContextMenus();
|
BuildContextMenus();
|
||||||
if (fontMenu != null)
|
if (fontMenu != null)
|
||||||
foreach (WF.ToolStripMenuItem item in fontMenu.DropDownItems)
|
foreach (WF.ToolStripMenuItem item in fontMenu.DropDownItems)
|
||||||
@@ -417,6 +456,12 @@ namespace QuickClaude
|
|||||||
}
|
}
|
||||||
menu.Items.Add(sizeMenu);
|
menu.Items.Add(sizeMenu);
|
||||||
|
|
||||||
|
snapItem = new WF.ToolStripMenuItem("Snap to desktop grid", null, (s, e) =>
|
||||||
|
{
|
||||||
|
settings.SnapToGrid = !settings.SnapToGrid; snapItem.Checked = settings.SnapToGrid; settings.Save();
|
||||||
|
}) { Checked = settings.SnapToGrid };
|
||||||
|
menu.Items.Add(snapItem);
|
||||||
|
|
||||||
topItem = new WF.ToolStripMenuItem("Always on top", null, (s, e) =>
|
topItem = new WF.ToolStripMenuItem("Always on top", null, (s, e) =>
|
||||||
{
|
{
|
||||||
settings.Topmost = !settings.Topmost; Topmost = settings.Topmost; topItem.Checked = settings.Topmost; settings.Save();
|
settings.Topmost = !settings.Topmost; Topmost = settings.Topmost; topItem.Checked = settings.Topmost; settings.Save();
|
||||||
@@ -481,7 +526,8 @@ namespace QuickClaude
|
|||||||
var hit = EdgeHit(hwnd, l);
|
var hit = EdgeHit(hwnd, l);
|
||||||
if (hit != 0) { handled = true; return new IntPtr(hit); }
|
if (hit != 0) { handled = true; return new IntPtr(hit); }
|
||||||
}
|
}
|
||||||
else if (msg == WM_EXITSIZEMOVE) SavePlace();
|
else if (msg == WM_ENTERSIZEMOVE) GetWindowRect(hwnd, out moveStart);
|
||||||
|
else if (msg == WM_EXITSIZEMOVE) { AfterMoveOrResize(hwnd); SavePlace(); }
|
||||||
return IntPtr.Zero;
|
return IntPtr.Zero;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -489,7 +535,89 @@ namespace QuickClaude
|
|||||||
// Resize from any edge or corner: report a ~6px border strip as the matching hit code
|
// Resize from any edge or corner: report a ~6px border strip as the matching hit code
|
||||||
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||||
[StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; }
|
[StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; }
|
||||||
const int WM_NCHITTEST = 0x0084, WM_EXITSIZEMOVE = 0x0232;
|
const int WM_NCHITTEST = 0x0084, WM_ENTERSIZEMOVE = 0x0231, WM_EXITSIZEMOVE = 0x0232;
|
||||||
|
RECT moveStart;
|
||||||
|
|
||||||
|
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
||||||
|
[DllImport("user32.dll")] static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern bool GetMonitorInfo(IntPtr mon, ref MONITORINFO info);
|
||||||
|
[DllImport("user32.dll")] static extern bool SystemParametersInfo(uint action, uint param, out int value, uint ini);
|
||||||
|
[StructLayout(LayoutKind.Sequential)] struct MONITORINFO { public int cbSize; public RECT rcMonitor, rcWork; public uint dwFlags; }
|
||||||
|
const uint SWP_NOSIZE = 0x1, SWP_NOMOVE = 0x2, SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10;
|
||||||
|
const uint SPI_ICONHORIZONTALSPACING = 0x000D, SPI_ICONVERTICALSPACING = 0x0018;
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindow(string cls, string name);
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string cls, string name);
|
||||||
|
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, int msg, IntPtr w, IntPtr l);
|
||||||
|
const int LVM_GETITEMSPACING = 0x1033;
|
||||||
|
|
||||||
|
// The desktop icon grid, asked of the desktop's own icon view so it follows the icon size
|
||||||
|
// and spacing Windows is really using (wIRC asks the same way, so both line up), falling
|
||||||
|
// back to the system icon spacing. In physical pixels.
|
||||||
|
static bool DesktopGridSpacing(out int sx, out int sy)
|
||||||
|
{
|
||||||
|
sx = sy = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var view = FindWindowEx(FindWindow("Progman", null), IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||||
|
var worker = IntPtr.Zero;
|
||||||
|
while (view == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null);
|
||||||
|
if (worker == IntPtr.Zero) break;
|
||||||
|
view = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||||
|
}
|
||||||
|
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||||||
|
if (list != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
long r = SendMessage(list, LVM_GETITEMSPACING, IntPtr.Zero, IntPtr.Zero).ToInt64();
|
||||||
|
sx = (int)(r & 0xFFFF); sy = (int)((r >> 16) & 0xFFFF);
|
||||||
|
if (sx > 8 && sy > 8) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return SystemParametersInfo(SPI_ICONHORIZONTALSPACING, 0, out sx, 0) && sx > 8 &&
|
||||||
|
SystemParametersInfo(SPI_ICONVERTICALSPACING, 0, out sy, 0) && sy > 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After a drag or resize ends: a resize trims the window to whole transcript lines, and a
|
||||||
|
// plain move snaps the top-left corner to the desktop icon grid (same spacing Windows uses
|
||||||
|
// for desktop icons, counted from the top-left of the screen's work area).
|
||||||
|
void AfterMoveOrResize(IntPtr hwnd)
|
||||||
|
{
|
||||||
|
if (!GetWindowRect(hwnd, out var r)) return;
|
||||||
|
bool resized = (r.Right - r.Left) != (moveStart.Right - moveStart.Left) ||
|
||||||
|
(r.Bottom - r.Top) != (moveStart.Bottom - moveStart.Top);
|
||||||
|
double scale = PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M22 ?? 1;
|
||||||
|
|
||||||
|
if (resized)
|
||||||
|
{
|
||||||
|
UpdateLayout();
|
||||||
|
double gap = TranscriptGap();
|
||||||
|
int gapPx = (int)Math.Floor(gap * scale);
|
||||||
|
int h = r.Bottom - r.Top;
|
||||||
|
int minPx = (int)Math.Ceiling(MinHeight * scale);
|
||||||
|
if (gapPx > 0 && h - gapPx >= minPx)
|
||||||
|
{
|
||||||
|
// Keep whichever edge didn't move: resizing from the top keeps the bottom fixed
|
||||||
|
bool fromTop = r.Top != moveStart.Top && r.Bottom == moveStart.Bottom;
|
||||||
|
int top = fromTop ? r.Top + gapPx : r.Top;
|
||||||
|
SetWindowPos(hwnd, IntPtr.Zero, r.Left, top, r.Right - r.Left, h - gapPx, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.SnapToGrid) return;
|
||||||
|
if (r.Left == moveStart.Left && r.Top == moveStart.Top) return;
|
||||||
|
if (!DesktopGridSpacing(out int sx, out int sy)) return;
|
||||||
|
var info = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
|
||||||
|
if (!GetMonitorInfo(MonitorFromWindow(hwnd, 2 /* nearest */), ref info)) return;
|
||||||
|
var w = info.rcWork;
|
||||||
|
int x = w.Left + (int)Math.Round((r.Left - w.Left) / (double)sx) * sx;
|
||||||
|
int y = w.Top + (int)Math.Round((r.Top - w.Top) / (double)sy) * sy;
|
||||||
|
if (x != r.Left || y != r.Top)
|
||||||
|
SetWindowPos(hwnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
|
const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
|
||||||
|
|
||||||
int EdgeHit(IntPtr hwnd, IntPtr l)
|
int EdgeHit(IntPtr hwnd, IntPtr l)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ namespace QuickClaude
|
|||||||
{
|
{
|
||||||
// Run as Claude Code's shell-command confirm hook (separate short-lived process)
|
// Run as Claude Code's shell-command confirm hook (separate short-lived process)
|
||||||
if (Array.IndexOf(args, "--confirm") >= 0) return ConfirmHook.Run();
|
if (Array.IndexOf(args, "--confirm") >= 0) return ConfirmHook.Run();
|
||||||
|
// Run as Claude Code's file-write guard hook (see WriteGuard)
|
||||||
|
if (Array.IndexOf(args, "--guard-write") >= 0) return WriteGuard.Run();
|
||||||
|
|
||||||
using var mutex = new Mutex(true, "QuickClaude.SingleInstance", out bool first);
|
using var mutex = new Mutex(true, "QuickClaude.SingleInstance", out bool first);
|
||||||
if (!first) return 0;
|
if (!first) return 0;
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<AssemblyName>QuickClaude</AssemblyName>
|
<AssemblyName>QuickClaude</AssemblyName>
|
||||||
<RootNamespace>QuickClaude</RootNamespace>
|
<RootNamespace>QuickClaude</RootNamespace>
|
||||||
<Version>1.3.0</Version>
|
<Version>1.5.0</Version>
|
||||||
<Product>QuickClaude</Product>
|
<Product>QuickClaude</Product>
|
||||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace QuickClaude
|
|||||||
public string TextColor { get; set; } = "Black";
|
public string TextColor { get; set; } = "Black";
|
||||||
public string Font { get; set; } = "Consolas";
|
public string Font { get; set; } = "Consolas";
|
||||||
public double FontSize { get; set; } = 10;
|
public double FontSize { get; set; } = 10;
|
||||||
|
public bool SnapToGrid { get; set; } = true;
|
||||||
public bool Topmost { get; set; } = false;
|
public bool Topmost { get; set; } = false;
|
||||||
public string WorkDir { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
public string WorkDir { get; set; } = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
public string Mode { get; set; } = "default";
|
public string Mode { get; set; } = "default";
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace QuickClaude
|
||||||
|
{
|
||||||
|
// QuickClaude.exe --guard-write: Claude Code's PreToolUse hook for Write, Edit and NotebookEdit.
|
||||||
|
// Keeps Claude from writing anywhere OpenRules lets things open without a confirm box that a
|
||||||
|
// normal (non-admin) account can write to: your Desktop and all of AppData (which holds your
|
||||||
|
// own Start Menu, AppData\Local\Programs and the Store app aliases). Without this, Claude in
|
||||||
|
// "Edit files" or "Auto" mode could drop a shortcut or program there and then open it unasked.
|
||||||
|
// Every other folder is left to the "Claude can" setting as before.
|
||||||
|
public static class WriteGuard
|
||||||
|
{
|
||||||
|
public static int Run()
|
||||||
|
{
|
||||||
|
string path = null, cwd = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stdin = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
|
||||||
|
using var doc = JsonDocument.Parse(stdin.ReadToEnd());
|
||||||
|
var root = doc.RootElement;
|
||||||
|
cwd = Str(root, "cwd");
|
||||||
|
if (root.TryGetProperty("tool_input", out var input))
|
||||||
|
path = Str(input, "file_path") ?? Str(input, "notebook_path") ?? Str(input, "path");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path)) return 0; // nothing to check: normal rules decide
|
||||||
|
|
||||||
|
if (!Blocked(path, cwd)) return 0;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
hookSpecificOutput = new
|
||||||
|
{
|
||||||
|
hookEventName = "PreToolUse",
|
||||||
|
permissionDecision = "deny",
|
||||||
|
permissionDecisionReason =
|
||||||
|
"QuickClaude doesn't let Claude write to the Desktop or AppData folders (programs and shortcuts there " +
|
||||||
|
"can open without a confirm box). Tell the user, and suggest a different folder if one would do."
|
||||||
|
}
|
||||||
|
});
|
||||||
|
using var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
|
||||||
|
stdout.Write(json);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Blocked(string path, string cwd)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var full = Path.IsPathRooted(path) || string.IsNullOrEmpty(cwd)
|
||||||
|
? Path.GetFullPath(path)
|
||||||
|
: Path.GetFullPath(Path.Combine(cwd, path));
|
||||||
|
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
var dirs = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(home, "AppData"),
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Microsoft", "Windows", "Start Menu"),
|
||||||
|
}.Where(d => !string.IsNullOrEmpty(d));
|
||||||
|
return dirs.Any(d =>
|
||||||
|
{
|
||||||
|
var root = Path.GetFullPath(d).TrimEnd('\\');
|
||||||
|
return full.Equals(root, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
full.StartsWith(root + "\\", StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { return true; } // a path we can't even parse doesn't get written
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Str(JsonElement e, string name) =>
|
||||||
|
e.ValueKind == JsonValueKind.Object && e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user