Unify cwd validation and improve MCP tool log visibility
This commit is contained in:
@@ -328,5 +328,37 @@ describe("curateAgentActivity", () => {
|
|||||||
|
|
||||||
expect(result).toBe("[Grep] TODO");
|
expect(result).toBe("[Grep] TODO");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows speak tool text input", () => {
|
||||||
|
const timeline: AgentTimelineItem[] = [
|
||||||
|
{
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "s1",
|
||||||
|
name: "speak",
|
||||||
|
input: { text: "hello from voice" },
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = curateAgentActivity(timeline);
|
||||||
|
expect(result).toBe('[speak] {"text":"hello from voice"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows MCP tool input JSON", () => {
|
||||||
|
const timeline: AgentTimelineItem[] = [
|
||||||
|
{
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "m1",
|
||||||
|
name: "paseo__create_agent",
|
||||||
|
input: { cwd: "/tmp/repo", initialPrompt: "do the thing" },
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = curateAgentActivity(timeline);
|
||||||
|
expect(result).toBe(
|
||||||
|
'[paseo__create_agent] {"cwd":"/tmp/repo","initialPrompt":"do the thing"}'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
|||||||
import { extractPrincipalParam } from "../../utils/tool-call-parsers.js";
|
import { extractPrincipalParam } from "../../utils/tool-call-parsers.js";
|
||||||
|
|
||||||
const DEFAULT_MAX_ITEMS = 40;
|
const DEFAULT_MAX_ITEMS = 40;
|
||||||
|
const MAX_TOOL_INPUT_CHARS = 400;
|
||||||
|
|
||||||
function appendText(buffer: string, text: string): string {
|
function appendText(buffer: string, text: string): string {
|
||||||
const normalized = text.trim();
|
const normalized = text.trim();
|
||||||
@@ -25,6 +26,34 @@ function flushBuffers(lines: string[], buffers: { message: string; thought: stri
|
|||||||
buffers.thought = "";
|
buffers.thought = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLikelyMcpToolCall(name: string): boolean {
|
||||||
|
const normalized = name.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized.includes("mcp") ||
|
||||||
|
normalized.includes("paseo") ||
|
||||||
|
normalized.includes("__") ||
|
||||||
|
normalized === "speak"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatToolInputJson(input: unknown): string | null {
|
||||||
|
if (input === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const encoded = JSON.stringify(input);
|
||||||
|
if (!encoded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (encoded.length <= MAX_TOOL_INPUT_CHARS) {
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
return `${encoded.slice(0, MAX_TOOL_INPUT_CHARS)}...`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collapse timeline items:
|
* Collapse timeline items:
|
||||||
* - Dedupe tool calls by callId (pending/completed -> single)
|
* - Dedupe tool calls by callId (pending/completed -> single)
|
||||||
@@ -127,6 +156,11 @@ export function curateAgentActivity(
|
|||||||
break;
|
break;
|
||||||
case "tool_call": {
|
case "tool_call": {
|
||||||
flushBuffers(lines, buffers);
|
flushBuffers(lines, buffers);
|
||||||
|
const inputJson = formatToolInputJson(item.input);
|
||||||
|
if (isLikelyMcpToolCall(item.name) && inputJson) {
|
||||||
|
lines.push(`[${item.name}] ${inputJson}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
const principal = extractPrincipalParam(item.input);
|
const principal = extractPrincipalParam(item.input);
|
||||||
if (principal) {
|
if (principal) {
|
||||||
lines.push(`[${item.name}] ${principal}`);
|
lines.push(`[${item.name}] ${principal}`);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||||
import { createAgentMcpServer } from "./mcp-server.js";
|
import { createAgentMcpServer } from "./mcp-server.js";
|
||||||
@@ -48,6 +51,7 @@ function createTestDeps(): TestDeps {
|
|||||||
|
|
||||||
describe("create_agent MCP tool", () => {
|
describe("create_agent MCP tool", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
const existingCwd = process.cwd();
|
||||||
|
|
||||||
it("requires a concise title no longer than 60 characters", async () => {
|
it("requires a concise title no longer than 60 characters", async () => {
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
@@ -56,7 +60,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
|
|
||||||
const missingTitle = await tool.inputSchema.safeParseAsync({
|
const missingTitle = await tool.inputSchema.safeParseAsync({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
initialMode: "default",
|
initialMode: "default",
|
||||||
initialPrompt: "test",
|
initialPrompt: "test",
|
||||||
});
|
});
|
||||||
@@ -64,7 +68,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
expect(missingTitle.error.issues[0].path).toEqual(["title"]);
|
expect(missingTitle.error.issues[0].path).toEqual(["title"]);
|
||||||
|
|
||||||
const tooLong = await tool.inputSchema.safeParseAsync({
|
const tooLong = await tool.inputSchema.safeParseAsync({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
initialMode: "default",
|
initialMode: "default",
|
||||||
title: "x".repeat(61),
|
title: "x".repeat(61),
|
||||||
initialPrompt: "test",
|
initialPrompt: "test",
|
||||||
@@ -73,7 +77,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
expect(tooLong.error.issues[0].path).toEqual(["title"]);
|
expect(tooLong.error.issues[0].path).toEqual(["title"]);
|
||||||
|
|
||||||
const ok = await tool.inputSchema.safeParseAsync({
|
const ok = await tool.inputSchema.safeParseAsync({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
initialMode: "default",
|
initialMode: "default",
|
||||||
title: "Short title",
|
title: "Short title",
|
||||||
initialPrompt: "test",
|
initialPrompt: "test",
|
||||||
@@ -86,7 +90,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||||
const tool = (server as any)._registeredTools["create_agent"];
|
const tool = (server as any)._registeredTools["create_agent"];
|
||||||
const parsed = await tool.inputSchema.safeParseAsync({
|
const parsed = await tool.inputSchema.safeParseAsync({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
initialMode: "default",
|
initialMode: "default",
|
||||||
title: "Short title",
|
title: "Short title",
|
||||||
});
|
});
|
||||||
@@ -94,6 +98,20 @@ describe("create_agent MCP tool", () => {
|
|||||||
expect(parsed.error.issues.some((issue: { path: string[] }) => issue.path[0] === "initialPrompt")).toBe(true);
|
expect(parsed.error.issues.some((issue: { path: string[] }) => issue.path[0] === "initialPrompt")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails immediately when cwd does not exist", async () => {
|
||||||
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
|
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||||
|
const tool = (server as any)._registeredTools["create_agent"];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tool.callback({
|
||||||
|
cwd: "/path/that/does/not/exist",
|
||||||
|
title: "Short title",
|
||||||
|
initialPrompt: "Do work",
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Working directory does not exist");
|
||||||
|
});
|
||||||
|
|
||||||
it("passes caller-provided titles directly into createAgent", async () => {
|
it("passes caller-provided titles directly into createAgent", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
spies.agentManager.createAgent.mockResolvedValue({
|
spies.agentManager.createAgent.mockResolvedValue({
|
||||||
@@ -108,14 +126,14 @@ describe("create_agent MCP tool", () => {
|
|||||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||||
const tool = (server as any)._registeredTools["create_agent"];
|
const tool = (server as any)._registeredTools["create_agent"];
|
||||||
await tool.callback({
|
await tool.callback({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
title: " Fix auth bug ",
|
title: " Fix auth bug ",
|
||||||
initialPrompt: "Do work",
|
initialPrompt: "Do work",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
title: "Fix auth bug",
|
title: "Fix auth bug",
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -137,7 +155,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||||
const tool = (server as any)._registeredTools["create_agent"];
|
const tool = (server as any)._registeredTools["create_agent"];
|
||||||
await tool.callback({
|
await tool.callback({
|
||||||
cwd: "/tmp/repo",
|
cwd: existingCwd,
|
||||||
title: " Fix auth ",
|
title: " Fix auth ",
|
||||||
initialPrompt: "Do work",
|
initialPrompt: "Do work",
|
||||||
});
|
});
|
||||||
@@ -153,15 +171,18 @@ describe("create_agent MCP tool", () => {
|
|||||||
|
|
||||||
it("allows caller agents to override cwd and applies caller context labels", async () => {
|
it("allows caller agents to override cwd and applies caller context labels", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
|
const baseDir = await mkdtemp(join(tmpdir(), "paseo-mcp-test-"));
|
||||||
|
const subdir = join(baseDir, "subdir");
|
||||||
|
await mkdir(subdir, { recursive: true });
|
||||||
spies.agentManager.getAgent.mockReturnValue({
|
spies.agentManager.getAgent.mockReturnValue({
|
||||||
id: "voice-agent",
|
id: "voice-agent",
|
||||||
cwd: "/tmp/voice",
|
cwd: baseDir,
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
currentModeId: "full-access",
|
currentModeId: "full-access",
|
||||||
} as ManagedAgent);
|
} as ManagedAgent);
|
||||||
spies.agentManager.createAgent.mockResolvedValue({
|
spies.agentManager.createAgent.mockResolvedValue({
|
||||||
id: "child-agent",
|
id: "child-agent",
|
||||||
cwd: "/tmp/voice/subdir",
|
cwd: subdir,
|
||||||
lifecycle: "idle",
|
lifecycle: "idle",
|
||||||
currentModeId: null,
|
currentModeId: null,
|
||||||
availableModes: [],
|
availableModes: [],
|
||||||
@@ -189,11 +210,12 @@ describe("create_agent MCP tool", () => {
|
|||||||
|
|
||||||
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cwd: "/tmp/voice/subdir",
|
cwd: subdir,
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
{ labels: { ui: "true" } }
|
{ labels: { ui: "true" } }
|
||||||
);
|
);
|
||||||
|
await rm(baseDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { AgentStorage } from "./agent-storage.js";
|
|||||||
import { createWorktree } from "../../utils/worktree.js";
|
import { createWorktree } from "../../utils/worktree.js";
|
||||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||||
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
||||||
|
import { validateWorkingDirectoryExists } from "./working-directory-validation.js";
|
||||||
|
|
||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
@@ -496,6 +497,7 @@ export async function createAgentMcpServer(
|
|||||||
} = topLevelArgs;
|
} = topLevelArgs;
|
||||||
|
|
||||||
resolvedCwd = expandPath(cwd);
|
resolvedCwd = expandPath(cwd);
|
||||||
|
await validateWorkingDirectoryExists(resolvedCwd);
|
||||||
|
|
||||||
if (worktreeName) {
|
if (worktreeName) {
|
||||||
if (!baseBranch) {
|
if (!baseBranch) {
|
||||||
@@ -513,6 +515,7 @@ export async function createAgentMcpServer(
|
|||||||
|
|
||||||
resolvedMode = initialMode;
|
resolvedMode = initialMode;
|
||||||
}
|
}
|
||||||
|
await validateWorkingDirectoryExists(resolvedCwd);
|
||||||
|
|
||||||
const provider: AgentProvider = agentType ?? "claude";
|
const provider: AgentProvider = agentType ?? "claude";
|
||||||
const normalizedTitle = title?.trim() ?? null;
|
const normalizedTitle = title?.trim() ?? null;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { stat } from "node:fs/promises";
|
||||||
|
|
||||||
|
export async function validateWorkingDirectoryExists(
|
||||||
|
cwd: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const cwdStats = await stat(cwd);
|
||||||
|
if (!cwdStats.isDirectory()) {
|
||||||
|
throw new Error(`Working directory is not a directory: ${cwd}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
||||||
|
}
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to access working directory: ${cwd}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ import { AgentManager } from "./agent/agent-manager.js";
|
|||||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
import type { ManagedAgent } from "./agent/agent-manager.js";
|
||||||
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
||||||
import { toAgentPayload } from "./agent/agent-projections.js";
|
import { toAgentPayload } from "./agent/agent-projections.js";
|
||||||
|
import { validateWorkingDirectoryExists } from "./agent/working-directory-validation.js";
|
||||||
import {
|
import {
|
||||||
StructuredAgentResponseError,
|
StructuredAgentResponseError,
|
||||||
generateStructuredAgentResponse,
|
generateStructuredAgentResponse,
|
||||||
@@ -298,6 +299,7 @@ export class Session {
|
|||||||
// Voice mode state
|
// Voice mode state
|
||||||
private isVoiceMode = false;
|
private isVoiceMode = false;
|
||||||
private speechInProgress = false;
|
private speechInProgress = false;
|
||||||
|
// OpenRouter voice-only conversation storage identifier.
|
||||||
private voiceConversationId: string | null = null;
|
private voiceConversationId: string | null = null;
|
||||||
|
|
||||||
private readonly dictationStreamManager: DictationStreamManager;
|
private readonly dictationStreamManager: DictationStreamManager;
|
||||||
@@ -1400,6 +1402,16 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.isVoiceMode = true;
|
this.isVoiceMode = true;
|
||||||
|
if (this.voiceLlmProvider !== "openrouter") {
|
||||||
|
this.voiceConversationId = null;
|
||||||
|
this.voiceAssistantAgentId = voiceConversationId;
|
||||||
|
this.sessionLogger.info(
|
||||||
|
{ voiceAssistantAgentId: this.voiceAssistantAgentId },
|
||||||
|
"Voice conversation enabled (agent-backed)"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.voiceConversationId = voiceConversationId;
|
this.voiceConversationId = voiceConversationId;
|
||||||
|
|
||||||
const loaded = await this.voiceConversationStore.load(
|
const loaded = await this.voiceConversationStore.load(
|
||||||
@@ -1416,6 +1428,14 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.isVoiceMode = false;
|
this.isVoiceMode = false;
|
||||||
|
if (this.voiceLlmProvider !== "openrouter") {
|
||||||
|
this.sessionLogger.info(
|
||||||
|
{ voiceAssistantAgentId: this.voiceAssistantAgentId },
|
||||||
|
"Voice conversation disabled (agent-backed)"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const idToPersist = this.voiceConversationId;
|
const idToPersist = this.voiceConversationId;
|
||||||
if (idToPersist) {
|
if (idToPersist) {
|
||||||
try {
|
try {
|
||||||
@@ -1548,21 +1568,7 @@ export class Session {
|
|||||||
try {
|
try {
|
||||||
// Validate that the working directory exists
|
// Validate that the working directory exists
|
||||||
const resolvedCwd = expandTilde(config.cwd);
|
const resolvedCwd = expandTilde(config.cwd);
|
||||||
try {
|
await validateWorkingDirectoryExists(resolvedCwd);
|
||||||
const stats = await stat(resolvedCwd);
|
|
||||||
if (!stats.isDirectory()) {
|
|
||||||
throw new Error(
|
|
||||||
`Working directory is not a directory: ${config.cwd}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (statError: any) {
|
|
||||||
if (statError.code === "ENOENT") {
|
|
||||||
throw new Error(
|
|
||||||
`Working directory does not exist: ${config.cwd}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw statError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(
|
const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(
|
||||||
config,
|
config,
|
||||||
@@ -4653,11 +4659,20 @@ export class Session {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
return existing.id;
|
return existing.id;
|
||||||
}
|
}
|
||||||
this.voiceAssistantAgentId = null;
|
try {
|
||||||
|
const hydrated = await this.ensureAgentLoaded(this.voiceAssistantAgentId);
|
||||||
|
this.voiceAssistantAgentId = hydrated.id;
|
||||||
|
return hydrated.id;
|
||||||
|
} catch (error) {
|
||||||
|
this.sessionLogger.debug(
|
||||||
|
{ err: error, voiceAssistantAgentId: this.voiceAssistantAgentId },
|
||||||
|
"Voice assistant agent not found in active/persisted state; creating new session"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = this.resolveVoiceAgentProvider();
|
const provider = this.resolveVoiceAgentProvider();
|
||||||
const voiceAgentId = `voice-${uuidv4()}`;
|
const voiceAgentId = this.voiceAssistantAgentId ?? uuidv4();
|
||||||
const cwd = join(this.paseoHome, "voice-agent-workspace");
|
const cwd = join(this.paseoHome, "voice-agent-workspace");
|
||||||
await mkdir(cwd, { recursive: true });
|
await mkdir(cwd, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const PrincipalParamSchema = z.union([
|
|||||||
z.object({ pattern: z.string() }).transform((d) => ({ type: "text" as const, value: d.pattern })),
|
z.object({ pattern: z.string() }).transform((d) => ({ type: "text" as const, value: d.pattern })),
|
||||||
z.object({ query: z.string() }).transform((d) => ({ type: "text" as const, value: d.query })),
|
z.object({ query: z.string() }).transform((d) => ({ type: "text" as const, value: d.query })),
|
||||||
z.object({ url: z.string() }).transform((d) => ({ type: "text" as const, value: d.url })),
|
z.object({ url: z.string() }).transform((d) => ({ type: "text" as const, value: d.url })),
|
||||||
|
z.object({ text: z.string() }).transform((d) => ({ type: "text" as const, value: d.text })),
|
||||||
// Files array (Codex apply_patch)
|
// Files array (Codex apply_patch)
|
||||||
z.object({ files: z.array(FileEntrySchema).nonempty() }).transform((d) => ({ type: "path" as const, value: d.files[0].path })),
|
z.object({ files: z.array(FileEntrySchema).nonempty() }).transform((d) => ({ type: "path" as const, value: d.files[0].path })),
|
||||||
// TodoWrite - show in_progress item or count
|
// TodoWrite - show in_progress item or count
|
||||||
|
|||||||
Reference in New Issue
Block a user