chore(lint): clean up cli package warnings

This commit is contained in:
Mohamed Boudra
2026-04-23 23:12:13 +07:00
parent 31f1484150
commit dffd8c4343
18 changed files with 742 additions and 631 deletions

View File

@@ -33,6 +33,12 @@ import { resolveCliVersion } from "./version.js";
const VERSION = resolveCliVersion();
function resolveHostnamesOption(hostnames: unknown, allowedHosts: unknown): string | undefined {
if (typeof hostnames === "string") return hostnames;
if (typeof allowedHosts === "string") return allowedHosts;
return undefined;
}
export function createCli(): Command {
const program = new Command();
@@ -120,12 +126,7 @@ export function createCli(): Command {
return runDaemonRestartCommand(
{
...options,
hostnames:
typeof options.hostnames === "string"
? options.hostnames
: typeof options.allowedHosts === "string"
? options.allowedHosts
: undefined,
hostnames: resolveHostnamesOption(options.hostnames, options.allowedHosts),
},
command,
);

View File

@@ -87,16 +87,27 @@ export async function runDeleteCommand(
agents = [fetchResult.agent];
}
for (const agent of agents) {
try {
if (agent.status === "running") {
await client.cancelAgent(agent.id);
const deleteResults = await Promise.all(
agents.map(async (agent) => {
try {
if (agent.status === "running") {
await client.cancelAgent(agent.id);
}
await client.deleteAgent(agent.id);
return { ok: true as const, id: agent.id };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { ok: false as const, id: agent.id, message };
}
await client.deleteAgent(agent.id);
deletedIds.push(agent.id);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Warning: Failed to delete agent ${agent.id.slice(0, 7)}: ${message}`);
}),
);
for (const result of deleteResults) {
if (result.ok) {
deletedIds.push(result.id);
} else {
console.error(
`Warning: Failed to delete agent ${result.id.slice(0, 7)}: ${result.message}`,
);
}
}

View File

@@ -104,30 +104,28 @@ function resolveModel(snapshot: AgentSnapshotPayload): string | null {
return normalizeModelId(snapshot.runtimeInfo?.model) ?? normalizeModelId(snapshot.model);
}
function buildLastUsage(snapshot: AgentSnapshotPayload): AgentInspect["LastUsage"] {
if (!snapshot.lastUsage) return null;
return {
InputTokens: snapshot.lastUsage.inputTokens ?? 0,
OutputTokens: snapshot.lastUsage.outputTokens ?? 0,
CachedTokens: snapshot.lastUsage.cachedInputTokens ?? 0,
CostUsd: snapshot.lastUsage.totalCostUsd ?? 0,
};
}
function buildCapabilities(snapshot: AgentSnapshotPayload): AgentInspect["Capabilities"] {
if (!snapshot.capabilities) return null;
return {
Streaming: snapshot.capabilities.supportsStreaming ?? false,
Persistence: snapshot.capabilities.supportsSessionPersistence ?? false,
DynamicModes: snapshot.capabilities.supportsDynamicModes ?? false,
McpServers: snapshot.capabilities.supportsMcpServers ?? false,
};
}
/** Convert agent snapshot to inspection data */
function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
const lastUsage = snapshot.lastUsage
? {
InputTokens: snapshot.lastUsage.inputTokens ?? 0,
OutputTokens: snapshot.lastUsage.outputTokens ?? 0,
CachedTokens: snapshot.lastUsage.cachedInputTokens ?? 0,
CostUsd: snapshot.lastUsage.totalCostUsd ?? 0,
}
: null;
const capabilities = snapshot.capabilities
? {
Streaming: snapshot.capabilities.supportsStreaming ?? false,
Persistence: snapshot.capabilities.supportsSessionPersistence ?? false,
DynamicModes: snapshot.capabilities.supportsDynamicModes ?? false,
McpServers: snapshot.capabilities.supportsMcpServers ?? false,
}
: null;
// Extract worktree and parentAgentId from labels if they exist
const worktree = snapshot.labels?.["paseo.worktree"] ?? null;
const parentAgentId = snapshot.labels?.["paseo.parent-agent-id"] ?? null;
return {
Id: snapshot.id,
Name: snapshot.title ?? "-",
@@ -141,8 +139,8 @@ function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
Cwd: snapshot.cwd,
CreatedAt: snapshot.createdAt,
UpdatedAt: snapshot.updatedAt,
LastUsage: lastUsage,
Capabilities: capabilities,
LastUsage: buildLastUsage(snapshot),
Capabilities: buildCapabilities(snapshot),
AvailableModes: snapshot.availableModes
? snapshot.availableModes.map((m) => ({ id: m.id, label: m.label }))
: null,
@@ -150,8 +148,8 @@ function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
id: p.id,
tool: p.name ?? "unknown",
})),
Worktree: worktree,
ParentAgentId: parentAgentId,
Worktree: snapshot.labels?.["paseo.worktree"] ?? null,
ParentAgentId: snapshot.labels?.["paseo.parent-agent-id"] ?? null,
};
}

View File

@@ -174,6 +174,40 @@ class StructuredRunStatusError extends Error {
}
}
async function fetchStructuredOutput(
caller: (structuredPrompt: string) => Promise<string>,
prompt: string,
outputSchema: ReturnType<typeof loadOutputSchema>,
): Promise<Record<string, unknown>> {
try {
return await getStructuredAgentResponse<Record<string, unknown>>({
caller,
prompt,
schema: outputSchema,
schemaName: "RunOutput",
maxRetries: 2,
});
} catch (err) {
if (err instanceof StructuredRunStatusError) {
throw {
code: "OUTPUT_SCHEMA_FAILED",
message: err.message,
} satisfies CommandError;
}
if (err instanceof StructuredAgentResponseError) {
throw {
code: "OUTPUT_SCHEMA_FAILED",
message: "Agent response did not match the required output schema",
details:
err.validationErrors.length > 0
? err.validationErrors.join("\n")
: err.lastResponse || "No response",
} satisfies CommandError;
}
throw err;
}
}
type ConnectedDaemonClient = Awaited<ReturnType<typeof connectToDaemon>>;
export interface StructuredResponseTimelineClient {
@@ -219,6 +253,107 @@ function structuredRunSchema(output: Record<string, unknown>): OutputSchema<Agen
};
}
function validateRunOptions(prompt: string, options: AgentRunOptions, outputSchema: unknown): void {
if (!prompt || prompt.trim().length === 0) {
throw {
code: "MISSING_PROMPT",
message: "A prompt is required",
details: "Usage: paseo agent run [options] <prompt>",
} satisfies CommandError;
}
if (options.base && !options.worktree) {
throw {
code: "INVALID_OPTIONS",
message: "--base can only be used with --worktree",
details: "Usage: paseo agent run --worktree <name> --base <branch> <prompt>",
} satisfies CommandError;
}
if (outputSchema && options.detach) {
throw {
code: "INVALID_OPTIONS",
message: "--output-schema cannot be used with --detach",
details: "Structured output requires waiting for the agent to finish",
} satisfies CommandError;
}
}
function parseWaitTimeoutOption(waitTimeout: string | undefined): number {
if (!waitTimeout) return 0;
try {
const ms = parseDuration(waitTimeout);
if (ms <= 0) {
throw new Error("Timeout must be positive");
}
return ms;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw {
code: "INVALID_TIMEOUT",
message: "Invalid wait timeout value",
details: message,
} satisfies CommandError;
}
}
function loadRunImages(
imagePaths: string[] | undefined,
): Array<{ data: string; mimeType: string }> | undefined {
if (!imagePaths || imagePaths.length === 0) return undefined;
return imagePaths.map((imagePath) => {
const resolvedPath = resolve(imagePath);
try {
const imageData = readFileSync(resolvedPath);
const mimeType = lookup(resolvedPath) || "application/octet-stream";
if (!mimeType.startsWith("image/")) {
throw new Error(`File is not an image: ${imagePath} (detected type: ${mimeType})`);
}
return {
data: imageData.toString("base64"),
mimeType,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to read image ${imagePath}: ${message}`, { cause: err });
}
});
}
function parseRunLabels(labelFlags: string[] | undefined): Record<string, string> {
const labels: Record<string, string> = {};
if (!labelFlags) return labels;
for (const labelStr of labelFlags) {
const eqIndex = labelStr.indexOf("=");
if (eqIndex === -1) {
throw {
code: "INVALID_LABEL",
message: `Invalid label format: ${labelStr}`,
details: "Labels must be in key=value format",
} satisfies CommandError;
}
const key = labelStr.slice(0, eqIndex);
labels[key] = labelStr.slice(eqIndex + 1);
}
return labels;
}
async function connectToDaemonOrThrow(
hostOption: string | undefined,
host: string,
): Promise<ConnectedDaemonClient> {
try {
return await connectToDaemon({ host: hostOption });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw {
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: "Start the daemon with: paseo daemon start",
} satisfies CommandError;
}
}
export async function runRunCommand(
prompt: string,
options: AgentRunOptions,
@@ -226,70 +361,14 @@ export async function runRunCommand(
): Promise<SingleResult<AgentRunResult>> {
const host = getDaemonHost({ host: options.host as string | undefined });
const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined;
let waitTimeoutMs = 0;
// Validate prompt is provided
if (!prompt || prompt.trim().length === 0) {
const error: CommandError = {
code: "MISSING_PROMPT",
message: "A prompt is required",
details: "Usage: paseo agent run [options] <prompt>",
};
throw error;
}
// Validate --base is only used with --worktree
if (options.base && !options.worktree) {
const error: CommandError = {
code: "INVALID_OPTIONS",
message: "--base can only be used with --worktree",
details: "Usage: paseo agent run --worktree <name> --base <branch> <prompt>",
};
throw error;
}
// --output-schema always runs in attached/wait mode
if (outputSchema && options.detach) {
const error: CommandError = {
code: "INVALID_OPTIONS",
message: "--output-schema cannot be used with --detach",
details: "Structured output requires waiting for the agent to finish",
};
throw error;
}
if (options.waitTimeout) {
try {
waitTimeoutMs = parseDuration(options.waitTimeout);
if (waitTimeoutMs <= 0) {
throw new Error("Timeout must be positive");
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "INVALID_TIMEOUT",
message: "Invalid wait timeout value",
details: message,
};
throw error;
}
}
validateRunOptions(prompt, options, outputSchema);
const waitTimeoutMs = parseWaitTimeoutOption(options.waitTimeout);
const resolvedProviderModel = resolveProviderAndModel(options);
const resolvedTitle = options.title ?? options.name;
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
const client = await connectToDaemonOrThrow(options.host as string | undefined, host);
try {
// Resolve working directory
@@ -305,32 +384,8 @@ export async function runRunCommand(
throw error;
}
// Process images if provided
let images: Array<{ data: string; mimeType: string }> | undefined;
if (options.image && options.image.length > 0) {
images = options.image.map((imagePath) => {
const resolvedPath = resolve(imagePath);
try {
const imageData = readFileSync(resolvedPath);
const mimeType = lookup(resolvedPath) || "application/octet-stream";
const images = loadRunImages(options.image);
// Verify it's an image MIME type
if (!mimeType.startsWith("image/")) {
throw new Error(`File is not an image: ${imagePath} (detected type: ${mimeType})`);
}
return {
data: imageData.toString("base64"),
mimeType,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to read image ${imagePath}: ${message}`, { cause: err });
}
});
}
// Build git options if worktree is specified
const git = options.worktree
? {
createWorktree: true,
@@ -339,24 +394,7 @@ export async function runRunCommand(
}
: undefined;
// Build labels from --label flags
const labels: Record<string, string> = {};
if (options.label) {
for (const labelStr of options.label) {
const eqIndex = labelStr.indexOf("=");
if (eqIndex === -1) {
const error: CommandError = {
code: "INVALID_LABEL",
message: `Invalid label format: ${labelStr}`,
details: "Labels must be in key=value format",
};
throw error;
}
const key = labelStr.slice(0, eqIndex);
const value = labelStr.slice(eqIndex + 1);
labels[key] = value;
}
}
const labels = parseRunLabels(options.label);
if (outputSchema) {
let structuredAgent: AgentSnapshotPayload | null = null;
@@ -413,36 +451,7 @@ export async function runRunCommand(
return lastMessage;
};
let output: Record<string, unknown>;
try {
output = await getStructuredAgentResponse<Record<string, unknown>>({
caller: callStructuredTurn,
prompt,
schema: outputSchema,
schemaName: "RunOutput",
maxRetries: 2,
});
} catch (err) {
if (err instanceof StructuredRunStatusError) {
const error: CommandError = {
code: "OUTPUT_SCHEMA_FAILED",
message: err.message,
};
throw error;
}
if (err instanceof StructuredAgentResponseError) {
const error: CommandError = {
code: "OUTPUT_SCHEMA_FAILED",
message: "Agent response did not match the required output schema",
details:
err.validationErrors.length > 0
? err.validationErrors.join("\n")
: err.lastResponse || "No response",
};
throw error;
}
throw err;
}
const output = await fetchStructuredOutput(callStructuredTurn, prompt, outputSchema);
if (!structuredAgent) {
const error: CommandError = {

View File

@@ -51,51 +51,42 @@ export function addSendOptions(cmd: Command): Command {
async function readImageFiles(
imagePaths: string[],
): Promise<Array<{ data: string; mimeType: string }>> {
const images: Array<{ data: string; mimeType: string }> = [];
return Promise.all(
imagePaths.map(async (path) => {
try {
const buffer = await readFile(path);
const ext = extname(path).toLowerCase();
for (const path of imagePaths) {
try {
const buffer = await readFile(path);
const ext = extname(path).toLowerCase();
let mimeType = "image/jpeg";
switch (ext) {
case ".png":
mimeType = "image/png";
break;
case ".jpg":
case ".jpeg":
mimeType = "image/jpeg";
break;
case ".gif":
mimeType = "image/gif";
break;
case ".webp":
mimeType = "image/webp";
break;
default:
mimeType = "image/jpeg";
}
// Determine media type from extension
let mimeType = "image/jpeg";
switch (ext) {
case ".png":
mimeType = "image/png";
break;
case ".jpg":
case ".jpeg":
mimeType = "image/jpeg";
break;
case ".gif":
mimeType = "image/gif";
break;
case ".webp":
mimeType = "image/webp";
break;
default:
// Default to jpeg for unknown types
mimeType = "image/jpeg";
return { data: buffer.toString("base64"), mimeType };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw {
code: "IMAGE_READ_ERROR",
message: `Failed to read image file: ${path}`,
details: message,
} satisfies CommandError;
}
const data = buffer.toString("base64");
images.push({
data,
mimeType,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "IMAGE_READ_ERROR",
message: `Failed to read image file: ${path}`,
details: message,
};
throw error;
}
}
return images;
}),
);
}
async function resolvePromptInput(options: {
@@ -147,6 +138,28 @@ async function resolvePromptInput(options: {
}
}
type SendWaitState = Awaited<
ReturnType<Awaited<ReturnType<typeof connectToDaemon>>["waitForFinish"]>
>;
function buildSendResult(agentIdArg: string, state: SendWaitState): AgentSendResult {
const agentId = state.final?.id ?? agentIdArg;
if (state.status === "timeout") {
return { agentId, status: "timeout", message: "Timed out waiting for agent to finish" };
}
if (state.status === "permission") {
return { agentId, status: "permission", message: "Agent is waiting for permission" };
}
if (state.status === "error") {
return {
agentId,
status: "error",
message: state.error ?? "Agent finished with error",
};
}
return { agentId, status: "completed", message: "Agent completed processing the message" };
}
export async function runSendCommand(
agentIdArg: string,
prompt: string | undefined,
@@ -207,54 +220,12 @@ export async function runSendCommand(
};
}
// Wait for agent to finish
const state = await client.waitForFinish(agentIdArg, 600000); // 10 minute timeout
await client.close();
if (state.status === "timeout") {
return {
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: "timeout",
message: "Timed out waiting for agent to finish",
},
schema: agentSendSchema,
};
}
if (state.status === "permission") {
return {
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: "permission",
message: "Agent is waiting for permission",
},
schema: agentSendSchema,
};
}
if (state.status === "error") {
return {
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: "error",
message: state.error ?? "Agent finished with error",
},
schema: agentSendSchema,
};
}
return {
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: "completed",
message: "Agent completed processing the message",
},
data: buildSendResult(agentIdArg, state),
schema: agentSendSchema,
};
} catch (err) {

View File

@@ -95,16 +95,25 @@ export async function runStopCommand(
}
// Interrupt each running agent. Idle agents are a no-op.
for (const agent of agents) {
try {
if (agent.status === "running") {
const stopResults = await Promise.all(
agents.map(async (agent) => {
if (agent.status !== "running") return { ok: true as const, id: agent.id, stopped: false };
try {
await client.cancelAgent(agent.id);
stoppedIds.push(agent.id);
return { ok: true as const, id: agent.id, stopped: true };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { ok: false as const, id: agent.id, message };
}
} catch (err) {
// Continue interrupting other agents even if one fails
const message = err instanceof Error ? err.message : String(err);
console.error(`Warning: Failed to stop agent ${agent.id.slice(0, 7)}: ${message}`);
}),
);
for (const result of stopResults) {
if (!result.ok) {
console.error(`Warning: Failed to stop agent ${result.id.slice(0, 7)}: ${result.message}`);
continue;
}
if (result.stopped) {
stoppedIds.push(result.id);
}
}

View File

@@ -53,6 +53,80 @@ async function getRecentActivityTranscript(
}
}
function parseWaitTimeout(timeout: string | undefined): {
timeoutMs: number;
timeoutLabel: string | null;
} {
if (!timeout) return { timeoutMs: 0, timeoutLabel: null };
try {
const ms = parseDuration(timeout);
if (ms <= 0) {
throw new Error("Timeout must be positive");
}
const timeoutSeconds = Math.floor(ms / 1000);
return {
timeoutMs: ms,
timeoutLabel: `${timeoutSeconds} second${timeoutSeconds === 1 ? "" : "s"}`,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw {
code: "INVALID_TIMEOUT",
message: "Invalid timeout value",
details: message,
} satisfies CommandError;
}
}
type WaitFinishState = Awaited<
ReturnType<Awaited<ReturnType<typeof connectToDaemon>>["waitForFinish"]>
>;
function buildWaitResult(args: {
state: WaitFinishState;
resolvedAgentId: string;
recentActivity: string | null;
timeoutLabel: string | null;
}): AgentWaitResult {
const { state, resolvedAgentId, recentActivity, timeoutLabel } = args;
if (state.status === "timeout") {
const timeoutMessage = timeoutLabel
? `Agent did not finish within ${timeoutLabel}. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`
: `Agent wait timed out. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`;
return {
agentId: resolvedAgentId,
status: "timeout",
message: appendRecentActivity(timeoutMessage, recentActivity),
};
}
if (state.status === "permission") {
const permission = state.final?.pendingPermissions?.[0];
return {
agentId: resolvedAgentId,
status: "permission",
message: permission
? `Agent is waiting for permission: ${permission.kind}`
: "Agent is waiting for permission",
};
}
if (state.status === "error") {
return {
agentId: resolvedAgentId,
status: "error",
message: state.error ?? "Agent finished with error",
};
}
return {
agentId: resolvedAgentId,
status: "idle",
message: appendRecentActivity("Agent is idle.", recentActivity),
};
}
export function addWaitOptions(cmd: Command): Command {
return cmd
.description("Wait for an agent to become idle")
@@ -67,37 +141,15 @@ export async function runWaitCommand(
): Promise<SingleResult<AgentWaitResult>> {
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
throw {
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent wait <id>",
};
throw error;
} satisfies CommandError;
}
// Parse timeout (no limit unless explicitly provided)
let timeoutMs = 0;
let timeoutLabel: string | null = null;
if (options.timeout) {
try {
timeoutMs = parseDuration(options.timeout);
if (timeoutMs <= 0) {
throw new Error("Timeout must be positive");
}
const timeoutSeconds = Math.floor(timeoutMs / 1000);
timeoutLabel = `${timeoutSeconds} second${timeoutSeconds === 1 ? "" : "s"}`;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: "INVALID_TIMEOUT",
message: "Invalid timeout value",
details: message,
};
throw error;
}
}
const { timeoutMs, timeoutLabel } = parseWaitTimeout(options.timeout);
let client;
try {
@@ -123,56 +175,9 @@ export async function runWaitCommand(
await client.close();
if (state.status === "timeout") {
const timeoutMessage = timeoutLabel
? `Agent did not finish within ${timeoutLabel}. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`
: `Agent wait timed out. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`;
return {
type: "single",
data: {
agentId: resolvedAgentId,
status: "timeout",
message: appendRecentActivity(timeoutMessage, recentActivity),
},
schema: agentWaitSchema,
};
}
if (state.status === "permission") {
const permission = state.final?.pendingPermissions?.[0];
return {
type: "single",
data: {
agentId: resolvedAgentId,
status: "permission",
message: permission
? `Agent is waiting for permission: ${permission.kind}`
: "Agent is waiting for permission",
},
schema: agentWaitSchema,
};
}
if (state.status === "error") {
return {
type: "single",
data: {
agentId: resolvedAgentId,
status: "error",
message: state.error ?? "Agent finished with error",
},
schema: agentWaitSchema,
};
}
// Agent is idle
return {
type: "single",
data: {
agentId: resolvedAgentId,
status: "idle",
message: appendRecentActivity("Agent is idle.", recentActivity),
},
data: buildWaitResult({ state, resolvedAgentId, recentActivity, timeoutLabel }),
schema: agentWaitSchema,
};
} catch (waitErr) {

View File

@@ -7,6 +7,12 @@ import { pairCommand } from "./pair.js";
import { withOutput } from "../../output/index.js";
import { addJsonOption } from "../../utils/command-options.js";
function resolveHostnamesOption(hostnames: unknown, allowedHosts: unknown): string | undefined {
if (typeof hostnames === "string") return hostnames;
if (typeof allowedHosts === "string") return allowedHosts;
return undefined;
}
export function createDaemonCommand(): Command {
const daemon = new Command("daemon").description("Manage the Paseo daemon");
@@ -46,12 +52,7 @@ export function createDaemonCommand(): Command {
return runRestartCommand(
{
...options,
hostnames:
typeof options.hostnames === "string"
? options.hostnames
: typeof options.allowedHosts === "string"
? options.allowedHosts
: undefined,
hostnames: resolveHostnamesOption(options.hostnames, options.allowedHosts),
},
command,
);

View File

@@ -119,25 +119,30 @@ function buildChildEnv(options: DaemonStartOptions): NodeJS.ProcessEnv {
return childEnv;
}
function resolveServerRunnerFromDir(currentDir: string): string | null {
const packageJsonPath = path.join(currentDir, "package.json");
if (!existsSync(packageJsonPath)) return null;
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
if (packageJson.name !== "@getpaseo/server") return null;
const distRunner = path.join(currentDir, "dist", "scripts", "supervisor-entrypoint.js");
if (existsSync(distRunner)) {
return distRunner;
}
return path.join(currentDir, "scripts", "supervisor-entrypoint.ts");
} catch {
return null;
}
}
function resolveDaemonRunnerEntry(): string {
const serverExportPath = require.resolve("@getpaseo/server");
let currentDir = path.dirname(serverExportPath);
while (true) {
const packageJsonPath = path.join(currentDir, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
if (packageJson.name === "@getpaseo/server") {
const distRunner = path.join(currentDir, "dist", "scripts", "supervisor-entrypoint.js");
if (existsSync(distRunner)) {
return distRunner;
}
return path.join(currentDir, "scripts", "supervisor-entrypoint.ts");
}
} catch {
// Continue searching up if package.json exists but is invalid.
}
const entry = resolveServerRunnerFromDir(currentDir);
if (entry) {
return entry;
}
const parentDir = path.dirname(currentDir);
@@ -154,6 +159,22 @@ function pidFilePath(paseoHome: string): string {
return path.join(paseoHome, DAEMON_PID_FILENAME);
}
function resolveListenField(listen: unknown, sockPath: unknown): string | undefined {
if (typeof listen === "string") return listen;
if (typeof sockPath === "string") return sockPath;
return undefined;
}
function resolveStopMessage(
forced: boolean,
lifecycleRequested: boolean,
fallbackMessage: string | null | undefined,
): string {
if (forced) return "Daemon owner process was force-stopped";
if (lifecycleRequested) return "Daemon stopped gracefully";
return fallbackMessage ?? "Daemon stopped via owner PID signal";
}
function readPidFile(pidPath: string): LocalDaemonPidInfo | null {
try {
const parsed = JSON.parse(readFileSync(pidPath, "utf-8")) as Record<string, unknown>;
@@ -167,12 +188,7 @@ function readPidFile(pidPath: string): LocalDaemonPidInfo | null {
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
hostname: typeof parsed.hostname === "string" ? parsed.hostname : undefined,
uid: typeof parsed.uid === "number" ? parsed.uid : undefined,
listen:
typeof parsed.listen === "string"
? parsed.listen
: typeof parsed.sockPath === "string"
? parsed.sockPath
: undefined,
listen: resolveListenField(parsed.listen, parsed.sockPath),
desktopManaged: parsed.desktopManaged === true ? true : undefined,
};
} catch {
@@ -528,10 +544,6 @@ export async function stopLocalDaemon(
home: state.home,
pid,
forced,
message: forced
? "Daemon owner process was force-stopped"
: lifecycleRequested
? "Daemon stopped gracefully"
: (fallbackMessage ?? "Daemon stopped via owner PID signal"),
message: resolveStopMessage(forced, lifecycleRequested, fallbackMessage),
};
}

View File

@@ -208,6 +208,117 @@ function resolveOwnerLabel(uid: number | undefined, hostname: string | undefined
return `${uidPart}@${hostPart}`;
}
interface DaemonProbeResult {
connectedDaemon: DaemonStatus["connectedDaemon"];
localDaemonOverride?: DaemonStatus["localDaemon"];
daemonVersion?: string | null;
runningAgents?: number;
idleAgents?: number;
daemonNodeOverride?: string;
note?: string;
}
async function probeDaemonOverWebsocket(args: {
host: string;
state: ReturnType<typeof resolveLocalDaemonState>;
}): Promise<DaemonProbeResult> {
const { host, state } = args;
const client = await tryConnectToDaemon({ host, timeout: 1500 });
if (!client) {
if (state.running) {
return {
connectedDaemon: "unreachable",
localDaemonOverride: "unresponsive",
note: `Local daemon PID is running but websocket at ${host} is not reachable`,
};
}
return { connectedDaemon: "unreachable" };
}
const daemonVersion = client.getLastServerInfoMessage()?.version ?? null;
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
const runningAgents = agents.filter((a) => a.status === "running").length;
const idleAgents = agents.filter((a) => a.status === "idle").length;
if (!state.running) {
return {
connectedDaemon: "reachable",
daemonVersion,
runningAgents,
idleAgents,
daemonNodeOverride: "unknown (API reachable, PID unresolved)",
note: state.pidInfo
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
};
}
return {
connectedDaemon: "reachable",
daemonVersion,
runningAgents,
idleAgents,
};
} catch {
return {
connectedDaemon: "reachable",
daemonVersion,
localDaemonOverride: state.running ? "unresponsive" : undefined,
note: state.running
? `Local daemon PID is running but API requests to ${host} failed`
: `Connected daemon websocket is reachable at ${host} but fetch_agents failed`,
};
} finally {
await client.close().catch(() => {});
}
}
interface ProbeMergeState {
probe: DaemonProbeResult;
connectedDaemon: DaemonStatus["connectedDaemon"];
localDaemon: DaemonStatus["localDaemon"];
daemonNode: string;
daemonVersion: string | null;
runningAgents: number | null;
idleAgents: number | null;
note: string | undefined;
}
function applyProbeToStatus(input: ProbeMergeState): Omit<ProbeMergeState, "probe"> {
const { probe } = input;
return {
connectedDaemon: probe.connectedDaemon,
localDaemon: probe.localDaemonOverride ?? input.localDaemon,
daemonNode: probe.daemonNodeOverride ?? input.daemonNode,
daemonVersion: probe.daemonVersion !== undefined ? probe.daemonVersion : input.daemonVersion,
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
note: probe.note ? appendNote(input.note, probe.note) : input.note,
};
}
function resolveServerIdSafely(home: string): { serverId: string | null; error: string | null } {
try {
return { serverId: getOrCreateServerId(home), error: null };
} catch (error) {
return {
serverId: null,
error: `serverId unavailable: ${shortenMessage(normalizeError(error))}`,
};
}
}
async function resolveDaemonNodeLabel(
state: ReturnType<typeof resolveLocalDaemonState>,
): Promise<string> {
if (!state.running) return "-";
if (!state.pidInfo?.pid) return "unknown (no PID available)";
const fromPid = await resolveNodePathFromPid(state.pidInfo.pid);
return fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
}
export type StatusResult = ListResult<StatusRow>;
export async function runStatusCommand(
@@ -219,15 +330,7 @@ export async function runStatusCommand(
const host = resolveTcpHostFromListen(state.listen);
const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname);
let daemonNode: string;
if (!state.running) {
daemonNode = "-";
} else if (state.pidInfo?.pid) {
const fromPid = await resolveNodePathFromPid(state.pidInfo.pid);
daemonNode = fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
} else {
daemonNode = "unknown (no PID available)";
}
let daemonNode = await resolveDaemonNodeLabel(state);
const cliNode = process.execPath;
let localDaemon: DaemonStatus["localDaemon"] = state.running ? "running" : "stopped";
let connectedDaemon: DaemonStatus["connectedDaemon"] = "not_probed";
@@ -242,58 +345,28 @@ export async function runStatusCommand(
}
if (host) {
const client = await tryConnectToDaemon({ host, timeout: 1500 });
if (client) {
connectedDaemon = "reachable";
daemonVersion = client.getLastServerInfoMessage()?.version ?? null;
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
runningAgents = agents.filter((a) => a.status === "running").length;
idleAgents = agents.filter((a) => a.status === "idle").length;
if (!state.running) {
daemonNode = "unknown (API reachable, PID unresolved)";
note = appendNote(
note,
state.pidInfo
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
);
}
} catch {
if (state.running) {
localDaemon = "unresponsive";
}
note = appendNote(
note,
state.running
? `Local daemon PID is running but API requests to ${host} failed`
: `Connected daemon websocket is reachable at ${host} but fetch_agents failed`,
);
} finally {
await client.close().catch(() => {});
}
} else if (state.running) {
connectedDaemon = "unreachable";
localDaemon = "unresponsive";
note = appendNote(
const probe = await probeDaemonOverWebsocket({ host, state });
({ connectedDaemon, localDaemon, daemonNode, daemonVersion, runningAgents, idleAgents, note } =
applyProbeToStatus({
probe,
connectedDaemon,
localDaemon,
daemonNode,
daemonVersion,
runningAgents,
idleAgents,
note,
`Local daemon PID is running but websocket at ${host} is not reachable`,
);
} else {
connectedDaemon = "unreachable";
}
}));
} else {
note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
}
const cliVersion = resolveCliVersion();
let serverId: string | null = null;
try {
serverId = getOrCreateServerId(state.home);
} catch (error) {
note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`);
const serverIdResult = resolveServerIdSafely(state.home);
const serverId = serverIdResult.serverId;
if (serverIdResult.error) {
note = appendNote(note, serverIdResult.error);
}
const providers = await checkProviderBinaries();

View File

@@ -315,6 +315,102 @@ export function onboardCommand(): Command {
});
}
async function resolveAndPersistVoice(
paseoHome: string,
options: OnboardOptions,
): Promise<boolean> {
let persisted = loadPersistedConfig(paseoHome) as OnboardPersistedConfig;
const persistedVoiceSelection = resolvePersistedVoiceSelection(persisted);
const shouldPrompt = options.voice === "ask" || options.voice === undefined;
let voiceEnabled: boolean;
try {
voiceEnabled =
shouldPrompt && persistedVoiceSelection !== null
? persistedVoiceSelection
: await resolveVoiceSelection(options.voice);
} catch (error) {
if (error instanceof OnboardCancelledError) {
cancel("Onboarding cancelled.");
process.exit(0);
}
throw error;
}
if (shouldPrompt && persistedVoiceSelection !== null) {
log.message(`Using saved voice setup from config (${voiceEnabled ? "enabled" : "disabled"}).`);
}
persisted = applyVoiceSelection(persisted, voiceEnabled);
savePersistedConfig(paseoHome, persisted);
return voiceEnabled;
}
async function ensureDaemonStarted(options: OnboardOptions, richUi: boolean): Promise<void> {
const stateBeforeStart = resolveLocalDaemonState({ home: options.home });
if (stateBeforeStart.running) {
log.message(`Daemon already running (PID ${stateBeforeStart.pidInfo?.pid ?? "unknown"}).`);
return;
}
const startSpinner = richUi ? spinner() : null;
try {
if (startSpinner) {
startSpinner.start("Starting daemon...");
} else {
log.message("Starting daemon...");
}
const startup = await startLocalDaemonDetached(options);
if (startSpinner) {
startSpinner.stop(`Daemon started (PID ${startup.pid ?? "unknown"})`);
} else {
log.message(`Daemon started (PID ${startup.pid ?? "unknown"})`);
}
log.message(`Logs: ${startup.logPath}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (startSpinner) {
startSpinner.error(message);
} else {
log.error(message);
}
process.exit(1);
}
}
async function waitForDaemonReadyWithUi(args: {
home: string;
timeoutMs: number;
richUi: boolean;
}): Promise<{ listen: string; host: string | null }> {
const readySpinner = args.richUi ? spinner() : null;
try {
if (readySpinner) {
readySpinner.start("Waiting for daemon to become ready...");
} else {
log.message("Waiting for daemon to become ready...");
}
const readyState = await waitForDaemonReady({
home: args.home,
timeoutMs: args.timeoutMs,
onStatus: readySpinner ? (message) => readySpinner.message(message) : undefined,
});
if (readySpinner) {
readySpinner.stop(`Daemon ready on ${readyState.listen}`);
} else {
log.message(`Daemon ready on ${readyState.listen}`);
}
return readyState;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (readySpinner) {
readySpinner.error(message);
} else {
log.error(message);
}
process.exit(1);
}
}
export async function runOnboard(options: OnboardOptions): Promise<void> {
const richUi = process.stdin.isTTY && process.stdout.isTTY;
if (richUi) {
@@ -340,96 +436,21 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
renderNote(paseoHome, "Paseo home");
}
let persisted = loadPersistedConfig(paseoHome) as OnboardPersistedConfig;
const persistedVoiceSelection = resolvePersistedVoiceSelection(persisted);
const shouldPrompt = options.voice === "ask" || options.voice === undefined;
let voiceEnabled: boolean;
try {
voiceEnabled =
shouldPrompt && persistedVoiceSelection !== null
? persistedVoiceSelection
: await resolveVoiceSelection(options.voice);
} catch (error) {
if (error instanceof OnboardCancelledError) {
cancel("Onboarding cancelled.");
process.exit(0);
return;
}
throw error;
}
if (shouldPrompt && persistedVoiceSelection !== null) {
log.message(`Using saved voice setup from config (${voiceEnabled ? "enabled" : "disabled"}).`);
}
persisted = applyVoiceSelection(persisted, voiceEnabled);
savePersistedConfig(paseoHome, persisted);
const voiceEnabled = await resolveAndPersistVoice(paseoHome, options);
const config = loadConfig(paseoHome, { cli: toCliOverrides(options) });
const voiceStatus = voiceEnabled
? "Voice features enabled. Local speech models will be downloaded automatically if missing."
: "Voice features disabled. Local speech models will not be downloaded.";
log.message(voiceStatus);
log.message(
voiceEnabled
? "Voice features enabled. Local speech models will be downloaded automatically if missing."
: "Voice features disabled. Local speech models will not be downloaded.",
);
const stateBeforeStart = resolveLocalDaemonState({ home: options.home });
const startSpinner = richUi ? spinner() : null;
if (!stateBeforeStart.running) {
try {
if (startSpinner) {
startSpinner.start("Starting daemon...");
} else {
log.message("Starting daemon...");
}
const startup = await startLocalDaemonDetached(options);
if (startSpinner) {
startSpinner.stop(`Daemon started (PID ${startup.pid ?? "unknown"})`);
} else {
log.message(`Daemon started (PID ${startup.pid ?? "unknown"})`);
}
log.message(`Logs: ${startup.logPath}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (startSpinner) {
startSpinner.error(message);
} else {
log.error(message);
}
process.exit(1);
}
} else {
log.message(`Daemon already running (PID ${stateBeforeStart.pidInfo?.pid ?? "unknown"}).`);
}
let readyState: { listen: string; host: string | null };
const readySpinner = richUi ? spinner() : null;
try {
if (readySpinner) {
readySpinner.start("Waiting for daemon to become ready...");
} else {
log.message("Waiting for daemon to become ready...");
}
readyState = await waitForDaemonReady({
home: options.home ?? paseoHome,
timeoutMs,
onStatus: readySpinner ? (message) => readySpinner.message(message) : undefined,
});
if (readySpinner) {
readySpinner.stop(`Daemon ready on ${readyState.listen}`);
} else {
log.message(`Daemon ready on ${readyState.listen}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (readySpinner) {
readySpinner.error(message);
} else {
log.error(message);
}
process.exit(1);
return;
}
await ensureDaemonStarted(options, richUi);
await waitForDaemonReadyWithUi({
home: options.home ?? paseoHome,
timeoutMs,
richUi,
});
if (config.relayEnabled === false) {
log.warn("Relay is disabled; pairing offer is unavailable for this daemon.");

View File

@@ -125,20 +125,21 @@ export async function runAllowCommand(
}
// Allow permissions
const results: PermissionResponseItem[] = [];
for (const permission of permissionsToAllow) {
await client.respondToPermission(resolvedAgentId, permission.id, {
behavior: "allow",
...(updatedInput ? { updatedInput } : {}),
});
results.push({
requestId: permission.id.slice(0, 8),
agentId: resolvedAgentId,
agentShortId: resolvedAgentId.slice(0, 7),
name: permission.name,
result: "allowed",
});
}
const results: PermissionResponseItem[] = await Promise.all(
permissionsToAllow.map(async (permission) => {
await client.respondToPermission(resolvedAgentId, permission.id, {
behavior: "allow",
...(updatedInput ? { updatedInput } : {}),
});
return {
requestId: permission.id.slice(0, 8),
agentId: resolvedAgentId,
agentShortId: resolvedAgentId.slice(0, 7),
name: permission.name,
result: "allowed",
};
}),
);
await client.close();

View File

@@ -89,21 +89,22 @@ export async function runDenyCommand(
}
// Deny permissions
const results: PermissionResponseItem[] = [];
for (const permission of permissionsToDeny) {
await client.respondToPermission(resolvedAgentId, permission.id, {
behavior: "deny",
...(options.message ? { message: options.message } : {}),
...(options.interrupt ? { interrupt: true } : {}),
});
results.push({
requestId: permission.id.slice(0, 8),
agentId: resolvedAgentId,
agentShortId: resolvedAgentId.slice(0, 7),
name: permission.name,
result: "denied",
});
}
const results: PermissionResponseItem[] = await Promise.all(
permissionsToDeny.map(async (permission) => {
await client.respondToPermission(resolvedAgentId, permission.id, {
behavior: "deny",
...(options.message ? { message: options.message } : {}),
...(options.interrupt ? { interrupt: true } : {}),
});
return {
requestId: permission.id.slice(0, 8),
agentId: resolvedAgentId,
agentShortId: resolvedAgentId.slice(0, 7),
name: permission.name,
result: "denied",
};
}),
);
await client.close();

View File

@@ -83,6 +83,46 @@ export function formatDurationMs(durationMs: number): string {
return parts.join("");
}
function resolveScheduleTarget(args: {
targetValue: string | undefined;
hasExplicitProviderSelection: boolean;
newAgentTarget: ScheduleTarget;
}): ScheduleTarget {
const { targetValue, hasExplicitProviderSelection, newAgentTarget } = args;
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
if (!targetValue) {
if (currentAgentId && !hasExplicitProviderSelection) {
return { type: "self", agentId: currentAgentId };
}
return newAgentTarget;
}
if (targetValue === "new-agent") {
return newAgentTarget;
}
if (hasExplicitProviderSelection) {
throw {
code: "INVALID_TARGET",
message: "--provider can only be used with a new-agent target",
details: "Use --target new-agent or omit --target to create a new agent schedule",
} satisfies CommandError;
}
if (targetValue === "self") {
if (!currentAgentId) {
throw {
code: "INVALID_TARGET",
message: "--target self requires running inside a Paseo agent",
} satisfies CommandError;
}
return { type: "self", agentId: currentAgentId };
}
return { type: "agent", agentId: targetValue };
}
export function parseScheduleCreateInput(options: {
prompt: string;
every?: string;
@@ -127,45 +167,11 @@ export function parseScheduleCreateInput(options: {
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
},
};
let target: ScheduleTarget;
if (!targetValue) {
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
if (currentAgentId && !hasExplicitProviderSelection) {
target = { type: "self", agentId: currentAgentId };
} else {
target = newAgentTarget;
}
} else if (targetValue === "self") {
if (hasExplicitProviderSelection) {
throw {
code: "INVALID_TARGET",
message: "--provider can only be used with a new-agent target",
details: "Use --target new-agent or omit --target to create a new agent schedule",
} satisfies CommandError;
}
const currentAgentId = process.env.PASEO_AGENT_ID?.trim();
if (!currentAgentId) {
throw {
code: "INVALID_TARGET",
message: "--target self requires running inside a Paseo agent",
} satisfies CommandError;
}
target = { type: "self", agentId: currentAgentId };
} else if (targetValue === "new-agent") {
target = newAgentTarget;
} else {
if (hasExplicitProviderSelection) {
throw {
code: "INVALID_TARGET",
message: "--provider can only be used with a new-agent target",
details: "Use --target new-agent or omit --target to create a new agent schedule",
} satisfies CommandError;
}
target = {
type: "agent",
agentId: targetValue,
};
}
const target = resolveScheduleTarget({
targetValue,
hasExplicitProviderSelection,
newAgentTarget,
});
const maxRuns =
options.maxRuns === undefined ? undefined : parsePositiveInt(options.maxRuns, "--max-runs");

View File

@@ -85,11 +85,9 @@ function readPidSocketTarget(paseoHome: string): string | null {
listen?: unknown;
sockPath?: unknown;
};
return typeof parsed.listen === "string"
? parsed.listen
: typeof parsed.sockPath === "string"
? parsed.sockPath
: null;
if (typeof parsed.listen === "string") return parsed.listen;
if (typeof parsed.sockPath === "string") return parsed.sockPath;
return null;
} catch {
return null;
}
@@ -143,6 +141,12 @@ function resolveDaemonHostCandidates(options?: ConnectOptions): string[] {
return resolveDefaultDaemonHosts();
}
function stripIpcPrefix(trimmed: string): string {
if (trimmed.startsWith("unix://")) return trimmed.slice("unix://".length).trim();
if (trimmed.startsWith("pipe://")) return trimmed.slice("pipe://".length).trim();
return trimmed;
}
export function resolveDaemonTarget(host: string): DaemonTarget {
const trimmed = host.trim();
if (
@@ -150,11 +154,7 @@ export function resolveDaemonTarget(host: string): DaemonTarget {
trimmed.startsWith("pipe://") ||
trimmed.startsWith("\\\\.\\pipe\\")
) {
const socketPath = trimmed.startsWith("unix://")
? trimmed.slice("unix://".length).trim()
: trimmed.startsWith("pipe://")
? trimmed.slice("pipe://".length).trim()
: trimmed;
const socketPath = stripIpcPrefix(trimmed);
if (!socketPath) {
throw new Error("Invalid IPC daemon target: missing socket path");
}

View File

@@ -147,14 +147,6 @@ async function test_verify_logs(agentId: string): Promise<void> {
// Logs should have content from both tasks
assert(result.stdout.length > 0, "Logs should have content");
// The agent was asked to say specific things, check if either appears in logs
// This is a loose check since exact log format may vary
const logsLower = result.stdout.toLowerCase();
const hasInitialTask =
logsLower.includes("initial") || logsLower.includes("hello") || logsLower.includes("task");
const hasFollowUp =
logsLower.includes("follow") || logsLower.includes("complete") || logsLower.includes("task");
// At minimum, there should be log entries (we can't guarantee exact content)
assert(result.stdout.split("\n").length > 3, "Should have multiple log entries from both tasks");

View File

@@ -64,6 +64,29 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
interface PermitResult {
exitCode: number;
stdout: string;
}
function findMatchingPermission(
result: PermitResult,
agentId: string,
): { agentId?: string } | undefined {
if (result.exitCode !== 0) return undefined;
try {
const permissions = JSON.parse(result.stdout.trim());
if (!Array.isArray(permissions) || permissions.length === 0) return undefined;
return permissions.find(
(p: { agentId?: string }) =>
p.agentId?.startsWith(agentId.slice(0, 7)) ||
agentId.startsWith(p.agentId?.slice(0, 7) || ""),
);
} catch {
return undefined;
}
}
async function test_create_agent_with_permissions(): Promise<string> {
console.log("\n--- Test: Create agent in default mode (will request permissions) ---");
@@ -111,28 +134,12 @@ async function test_wait_for_permission_request(agentId: string): Promise<void>
while (Date.now() - startTime < maxWait) {
const result = await ctx.paseo(["permit", "ls", "--json"]);
if (result.exitCode === 0) {
try {
const permissions = JSON.parse(result.stdout.trim());
if (Array.isArray(permissions) && permissions.length > 0) {
// Check if any permission is for our agent
const ourPermission = permissions.find(
(p: { agentId?: string }) =>
p.agentId?.startsWith(agentId.slice(0, 7)) ||
agentId.startsWith(p.agentId?.slice(0, 7) || ""),
);
if (ourPermission) {
console.log("Permission request detected:", ourPermission);
console.log("PASS: Agent requested permission");
return;
}
}
} catch {
// JSON parse failed, continue polling
}
const ourPermission = findMatchingPermission(result, agentId);
if (ourPermission) {
console.log("Permission request detected:", ourPermission);
console.log("PASS: Agent requested permission");
return;
}
await sleep(pollInterval);
}

View File

@@ -126,24 +126,17 @@ async function terminateProcessTree(processRef: ChildProcess, timeoutMs: number)
signalProcessTree(pid, "SIGTERM");
await new Promise<void>((resolve) => {
let settled = false;
const finish = () => {
if (settled) {
return;
}
settled = true;
const done = () => resolve();
const onExit = () => {
clearTimeout(timeoutId);
resolve();
done();
};
const timeoutId = setTimeout(() => {
signalProcessTree(pid, "SIGKILL");
finish();
processRef.removeListener("exit", onExit);
done();
}, timeoutMs);
processRef.once("exit", () => {
finish();
});
processRef.once("exit", onExit);
});
}