mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 06:04:51 +00:00
coding-agent: fix macOS screenshot filenames with unicode spaces (#181)
This commit is contained in:
parent
5c0a84b2d8
commit
9a7bbb2839
9 changed files with 70 additions and 108 deletions
|
|
@ -5,8 +5,8 @@
|
||||||
import type { Attachment } from "@mariozechner/pi-agent-core";
|
import type { Attachment } from "@mariozechner/pi-agent-core";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { existsSync, readFileSync, statSync } from "fs";
|
import { existsSync, readFileSync, statSync } from "fs";
|
||||||
import { homedir } from "os";
|
|
||||||
import { extname, resolve } from "path";
|
import { extname, resolve } from "path";
|
||||||
|
import { resolveReadPath } from "../core/tools/path-utils.js";
|
||||||
|
|
||||||
/** Map of file extensions to MIME types for common image formats */
|
/** Map of file extensions to MIME types for common image formats */
|
||||||
const IMAGE_MIME_TYPES: Record<string, string> = {
|
const IMAGE_MIME_TYPES: Record<string, string> = {
|
||||||
|
|
@ -23,17 +23,6 @@ function isImageFile(filePath: string): string | null {
|
||||||
return IMAGE_MIME_TYPES[ext] || null;
|
return IMAGE_MIME_TYPES[ext] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Expand ~ to home directory */
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProcessedFiles {
|
export interface ProcessedFiles {
|
||||||
textContent: string;
|
textContent: string;
|
||||||
imageAttachments: Attachment[];
|
imageAttachments: Attachment[];
|
||||||
|
|
@ -45,9 +34,8 @@ export function processFileArguments(fileArgs: string[]): ProcessedFiles {
|
||||||
const imageAttachments: Attachment[] = [];
|
const imageAttachments: Attachment[] = [];
|
||||||
|
|
||||||
for (const fileArg of fileArgs) {
|
for (const fileArg of fileArgs) {
|
||||||
// Expand and resolve path
|
// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
|
||||||
const expandedPath = expandPath(fileArg);
|
const absolutePath = resolve(resolveReadPath(fileArg));
|
||||||
const absolutePath = resolve(expandedPath);
|
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
if (!existsSync(absolutePath)) {
|
if (!existsSync(absolutePath)) {
|
||||||
|
|
|
||||||
|
|
@ -44,17 +44,21 @@ export interface LoadHooksResult {
|
||||||
errors: Array<{ path: string; error: string }>;
|
errors: Array<{ path: string; error: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||||
* Expand path with ~ support.
|
|
||||||
*/
|
function normalizeUnicodeSpaces(str: string): string {
|
||||||
|
return str.replace(UNICODE_SPACES, " ");
|
||||||
|
}
|
||||||
|
|
||||||
function expandPath(p: string): string {
|
function expandPath(p: string): string {
|
||||||
if (p.startsWith("~/")) {
|
const normalized = normalizeUnicodeSpaces(p);
|
||||||
return path.join(os.homedir(), p.slice(2));
|
if (normalized.startsWith("~/")) {
|
||||||
|
return path.join(os.homedir(), normalized.slice(2));
|
||||||
}
|
}
|
||||||
if (p.startsWith("~")) {
|
if (normalized.startsWith("~")) {
|
||||||
return path.join(os.homedir(), p.slice(1));
|
return path.join(os.homedir(), normalized.slice(1));
|
||||||
}
|
}
|
||||||
return p;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,10 @@
|
||||||
import * as os from "node:os";
|
|
||||||
import type { AgentTool } from "@mariozechner/pi-ai";
|
import type { AgentTool } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import * as Diff from "diff";
|
import * as Diff from "diff";
|
||||||
import { constants } from "fs";
|
import { constants } from "fs";
|
||||||
import { access, readFile, writeFile } from "fs/promises";
|
import { access, readFile, writeFile } from "fs/promises";
|
||||||
import { resolve as resolvePath } from "path";
|
import { resolve as resolvePath } from "path";
|
||||||
|
import { expandPath } from "./path-utils.js";
|
||||||
/**
|
|
||||||
* Expand ~ to home directory
|
|
||||||
*/
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return os.homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return os.homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a unified diff string with line numbers and context
|
* Generate a unified diff string with line numbers and context
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,11 @@ import { Type } from "@sinclair/typebox";
|
||||||
import { spawnSync } from "child_process";
|
import { spawnSync } from "child_process";
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import { globSync } from "glob";
|
import { globSync } from "glob";
|
||||||
import { homedir } from "os";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { ensureTool } from "../../utils/tools-manager.js";
|
import { ensureTool } from "../../utils/tools-manager.js";
|
||||||
|
import { expandPath } from "./path-utils.js";
|
||||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand ~ to home directory
|
|
||||||
*/
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const findSchema = Type.Object({
|
const findSchema = Type.Object({
|
||||||
pattern: Type.String({
|
pattern: Type.String({
|
||||||
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",
|
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import type { AgentTool } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import { readFileSync, type Stats, statSync } from "fs";
|
import { readFileSync, type Stats, statSync } from "fs";
|
||||||
import { homedir } from "os";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { ensureTool } from "../../utils/tools-manager.js";
|
import { ensureTool } from "../../utils/tools-manager.js";
|
||||||
|
import { expandPath } from "./path-utils.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAX_BYTES,
|
DEFAULT_MAX_BYTES,
|
||||||
formatSize,
|
formatSize,
|
||||||
|
|
@ -15,19 +15,6 @@ import {
|
||||||
truncateLine,
|
truncateLine,
|
||||||
} from "./truncate.js";
|
} from "./truncate.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand ~ to home directory
|
|
||||||
*/
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const grepSchema = Type.Object({
|
const grepSchema = Type.Object({
|
||||||
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
||||||
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
|
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,10 @@
|
||||||
import type { AgentTool } from "@mariozechner/pi-ai";
|
import type { AgentTool } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { existsSync, readdirSync, statSync } from "fs";
|
import { existsSync, readdirSync, statSync } from "fs";
|
||||||
import { homedir } from "os";
|
|
||||||
import nodePath from "path";
|
import nodePath from "path";
|
||||||
|
import { expandPath } from "./path-utils.js";
|
||||||
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand ~ to home directory
|
|
||||||
*/
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lsSchema = Type.Object({
|
const lsSchema = Type.Object({
|
||||||
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
||||||
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
|
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
|
||||||
|
|
|
||||||
48
packages/coding-agent/src/core/tools/path-utils.ts
Normal file
48
packages/coding-agent/src/core/tools/path-utils.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { accessSync, constants } from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
|
||||||
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
||||||
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
||||||
|
|
||||||
|
function normalizeUnicodeSpaces(str: string): string {
|
||||||
|
return str.replace(UNICODE_SPACES, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryMacOSScreenshotPath(filePath: string): string {
|
||||||
|
return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileExists(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
accessSync(filePath, constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandPath(filePath: string): string {
|
||||||
|
const normalized = normalizeUnicodeSpaces(filePath);
|
||||||
|
if (normalized === "~") {
|
||||||
|
return os.homedir();
|
||||||
|
}
|
||||||
|
if (normalized.startsWith("~/")) {
|
||||||
|
return os.homedir() + normalized.slice(1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReadPath(filePath: string): string {
|
||||||
|
const expanded = expandPath(filePath);
|
||||||
|
|
||||||
|
if (fileExists(expanded)) {
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const macOSVariant = tryMacOSScreenshotPath(expanded);
|
||||||
|
if (macOSVariant !== expanded && fileExists(macOSVariant)) {
|
||||||
|
return macOSVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,11 @@
|
||||||
import * as os from "node:os";
|
|
||||||
import type { AgentTool, ImageContent, TextContent } from "@mariozechner/pi-ai";
|
import type { AgentTool, ImageContent, TextContent } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { constants } from "fs";
|
import { constants } from "fs";
|
||||||
import { access, readFile } from "fs/promises";
|
import { access, readFile } from "fs/promises";
|
||||||
import { extname, resolve as resolvePath } from "path";
|
import { extname, resolve as resolvePath } from "path";
|
||||||
|
import { resolveReadPath } from "./path-utils.js";
|
||||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand ~ to home directory
|
|
||||||
*/
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return os.homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return os.homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of file extensions to MIME types for common image formats
|
* Map of file extensions to MIME types for common image formats
|
||||||
*/
|
*/
|
||||||
|
|
@ -58,7 +45,7 @@ export const readTool: AgentTool<typeof readSchema> = {
|
||||||
{ path, offset, limit }: { path: string; offset?: number; limit?: number },
|
{ path, offset, limit }: { path: string; offset?: number; limit?: number },
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => {
|
) => {
|
||||||
const absolutePath = resolvePath(expandPath(path));
|
const absolutePath = resolvePath(resolveReadPath(path));
|
||||||
const mimeType = isImageFile(absolutePath);
|
const mimeType = isImageFile(absolutePath);
|
||||||
|
|
||||||
return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(
|
return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,8 @@
|
||||||
import * as os from "node:os";
|
|
||||||
import type { AgentTool } from "@mariozechner/pi-ai";
|
import type { AgentTool } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { mkdir, writeFile } from "fs/promises";
|
import { mkdir, writeFile } from "fs/promises";
|
||||||
import { dirname, resolve as resolvePath } from "path";
|
import { dirname, resolve as resolvePath } from "path";
|
||||||
|
import { expandPath } from "./path-utils.js";
|
||||||
/**
|
|
||||||
* Expand ~ to home directory
|
|
||||||
*/
|
|
||||||
function expandPath(filePath: string): string {
|
|
||||||
if (filePath === "~") {
|
|
||||||
return os.homedir();
|
|
||||||
}
|
|
||||||
if (filePath.startsWith("~/")) {
|
|
||||||
return os.homedir() + filePath.slice(1);
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeSchema = Type.Object({
|
const writeSchema = Type.Object({
|
||||||
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue