diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a875b2..49a203a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). Versioning foll ## [Unreleased] +### Removed + +- **`voicethere projects errors list`** — session failures are browsed via **`projects logs list --severity error`** (or `--level error`). Errors persist in agent logs with `fields.kind = session_error`, `fields.projectId`, `fields.sessionId`, and message prefix `[CODE]`. + +### Changed + +- **`projects logs list`** — documents `--severity` as alias for `--level`; E2E smokes assert session errors through this filter. + ## [0.8.0] - 2026-08-03 ### Added @@ -85,7 +93,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). Versioning foll ### Added -- **`voicethere projects logs list`** — list searchable customer agent logs for a project or `--session` (mirrors `projects errors list`). +- **`voicethere projects logs list`** — list searchable customer agent logs for a project or `--session` (filter session failures with `--severity error`). ## [0.3.15] - 2026-07-17 diff --git a/README.md b/README.md index 370a4c4..24bf27c 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ Example: [`.voicethere/config.json.example`](./.voicethere/config.json.example) | `projects conversation list [--q] [--json]` | Stored voice transcripts; `--q` searches turn text or session id | | `projects conversation get ` | Full turn timeline for one session | | `projects conversation search ` | Alias for `conversation list --q` | -| `projects errors list [--session] [--json]` | Structured session errors from dashboard API | +| `projects logs list [--session] [--level\|--severity] [--q] [--json]` | Searchable agent logs; use `--severity error` for session failures (dual-written) | | `projects voice catalog` | show STT/TTS vendors and models | | `build list` | Builds for the active project | | `build validate [file]` | Sandbox verify (default bundle from config) | diff --git a/src/cli.ts b/src/cli.ts index 90810f1..9845f98 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -31,7 +31,6 @@ import { runProjectsSubscriptionShow } from "./commands/projects/subscription/sh import { runProjectsUsageShow } from "./commands/projects/usage/show.js"; import { runProjectsSessionSettingsList } from "./commands/projects/session-settings/list.js"; import { runProjectsSessionSettingsSet } from "./commands/projects/session-settings/set.js"; -import { runProjectsErrorsList } from "./commands/projects/errors/list.js"; import { runProjectsLogsList } from "./commands/projects/logs/list.js"; import { runProjectsConversationExport, @@ -561,45 +560,24 @@ async function main(): Promise { }, ); - const errors = projects - .command("errors") - .description("List structured session errors for the active project"); - - errors - .command("list") - .description("List recent session errors (default: last 20 for project)") - .option("--project ", "Project UUID") - .option("--limit ", "Max rows when listing project errors", "20") - .option("--session ", "Filter to one orchestrator session id") - .option("--json", "Output JSON") - .action( - async (options: { - project?: string; - limit?: string; - session?: string; - json?: boolean; - }) => { - await runProjectsErrorsList({ - projectId: options.project, - limit: options.limit ? Number.parseInt(options.limit, 10) : undefined, - sessionId: options.session, - json: options.json, - }); - }, - ); - const logs = projects .command("logs") .description("List structured agent logs for the active project"); logs .command("list") - .description("List recent agent logs (default: last 20 for project)") + .description( + "List recent agent logs (default: last 20 for project). Use --severity or --level to filter; --session scopes to one conversation.", + ) .option("--project ", "Project UUID") .option("--limit ", "Max rows when listing project logs", "20") - .option("--session ", "Filter to one orchestrator session id") + .option("--session ", "Filter to one orchestrator session id (one conversation)") .option("--q ", "Search log messages") .option("--level ", "Filter by level (debug|info|warn|error)") + .option( + "--severity ", + "Filter by severity (debug|info|warn|error); alias for --level", + ) .option("--json", "Output JSON") .action( async (options: { @@ -608,6 +586,7 @@ async function main(): Promise { session?: string; q?: string; level?: string; + severity?: string; json?: boolean; }) => { await runProjectsLogsList({ @@ -616,7 +595,17 @@ async function main(): Promise { sessionId: options.session, q: options.q, level: options.level as - "debug" | "info" | "warn" | "error" | undefined, + | "debug" + | "info" + | "warn" + | "error" + | undefined, + severity: options.severity as + | "debug" + | "info" + | "warn" + | "error" + | undefined, json: options.json, }); }, diff --git a/src/commands/projects/errors/list.test.ts b/src/commands/projects/errors/list.test.ts deleted file mode 100644 index 0dff495..0000000 --- a/src/commands/projects/errors/list.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { runProjectsErrorsList } from "./list.js"; - -const listProjectSessionErrors = vi.fn(); -const listSessionErrors = vi.fn(); -const requireCredentials = vi.fn(); -const requireProjectId = vi.fn(); - -vi.mock("../../../lib/api.js", () => ({ - createApi: vi.fn(() => ({ - listProjectSessionErrors, - listSessionErrors, - })), -})); - -vi.mock("../../../lib/config.js", () => ({ - requireCredentials: (...args: unknown[]) => requireCredentials(...args), -})); - -vi.mock("../../../lib/project-config.js", () => ({ - requireProjectId: (...args: unknown[]) => requireProjectId(...args), -})); - -const sampleError = { - id: "err-1", - org_id: "org-1", - project_id: "proj-1", - session_id: null, - orchestrator_session_id: "orch-session-abc123", - source: "agent" as const, - code: "AGENT_HANDLER_FAILED", - message: "e2e crash-agent: intentional handler failure", - stack_trace: null, - context: {}, - created_at: "2026-07-07T05:00:00.000Z", -}; - -describe("runProjectsErrorsList", () => { - beforeEach(() => { - listProjectSessionErrors.mockReset(); - listSessionErrors.mockReset(); - requireCredentials.mockReset(); - requireProjectId.mockReset(); - vi.spyOn(console, "log").mockImplementation(() => {}); - - requireCredentials.mockResolvedValue({ - api_key: "vth_test", - api_base: "https://app.voicethere.dev/api/v1", - }); - requireProjectId.mockResolvedValue("proj-1"); - }); - - it("prints TSV rows for project errors", async () => { - listProjectSessionErrors.mockResolvedValue({ - project_id: "proj-1", - errors: [sampleError], - }); - - await runProjectsErrorsList({}); - - expect(listProjectSessionErrors).toHaveBeenCalledWith("proj-1", 20); - expect(console.log).toHaveBeenCalledWith( - expect.stringContaining("AGENT_HANDLER_FAILED"), - ); - expect(console.log).toHaveBeenCalledWith( - expect.stringContaining("orch-session"), - ); - }); - - it("prints TSV rows for session-scoped errors", async () => { - listSessionErrors.mockResolvedValue({ - project_id: "proj-1", - orchestrator_session_id: "orch-session-abc123", - errors: [sampleError], - }); - - await runProjectsErrorsList({ sessionId: "orch-session-abc123" }); - - expect(listSessionErrors).toHaveBeenCalledWith( - "proj-1", - "orch-session-abc123", - ); - expect(console.log).toHaveBeenCalledWith( - expect.stringContaining("AGENT_HANDLER_FAILED"), - ); - }); - - it("outputs JSON when --json is set", async () => { - listSessionErrors.mockResolvedValue({ - project_id: "proj-1", - orchestrator_session_id: "orch-session-abc123", - errors: [sampleError], - }); - - await runProjectsErrorsList({ - sessionId: "orch-session-abc123", - json: true, - }); - - expect(console.log).toHaveBeenCalledWith( - JSON.stringify( - { - project_id: "proj-1", - orchestrator_session_id: "orch-session-abc123", - errors: [sampleError], - }, - null, - 2, - ), - ); - }); - - it("uses explicit project id over linked config", async () => { - listProjectSessionErrors.mockResolvedValue({ - project_id: "proj-2", - errors: [], - }); - - await runProjectsErrorsList({ projectId: "proj-2", limit: 5 }); - - expect(requireProjectId).not.toHaveBeenCalled(); - expect(listProjectSessionErrors).toHaveBeenCalledWith("proj-2", 5); - }); -}); diff --git a/src/commands/projects/errors/list.ts b/src/commands/projects/errors/list.ts deleted file mode 100644 index df32a46..0000000 --- a/src/commands/projects/errors/list.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { logStep, logVerbose } from "../../../lib/command-log.js"; -import { createApiFromCredentials } from "../../../lib/control-plane-auth.js"; -import { requireCredentials } from "../../../lib/config.js"; -import { requireProjectId } from "../../../lib/project-config.js"; - -export interface ProjectsErrorsListOptions { - projectId?: string; - limit?: number; - sessionId?: string; - json?: boolean; -} - -export async function runProjectsErrorsList( - options: ProjectsErrorsListOptions = {}, -): Promise { - const projectId = options.projectId?.trim() || (await requireProjectId()); - const limit = options.limit ?? 20; - - logStep( - options.sessionId - ? `Listing errors for session ${options.sessionId}` - : `Listing last ${limit} project session errors`, - ); - - const credentials = await requireCredentials(); - const api = createApiFromCredentials(credentials); - - if (options.sessionId) { - const result = await api.listSessionErrors(projectId, options.sessionId); - logVerbose(`found ${result.errors.length} error(s)`); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - for (const row of result.errors) { - console.log( - `${row.created_at}\t${row.code}\t${row.source}\t${row.message}`, - ); - } - return; - } - - const result = await api.listProjectSessionErrors(projectId, limit); - logVerbose(`found ${result.errors.length} error(s)`); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - for (const row of result.errors) { - const session = row.orchestrator_session_id.slice(0, 12); - console.log( - `${row.created_at}\t${session}…\t${row.code}\t${row.source}\t${row.message}`, - ); - } -} diff --git a/src/commands/projects/logs/list.test.ts b/src/commands/projects/logs/list.test.ts index de0fe88..a455aeb 100644 --- a/src/commands/projects/logs/list.test.ts +++ b/src/commands/projects/logs/list.test.ts @@ -103,6 +103,58 @@ describe("runProjectsLogsList", () => { }); }); + it("passes --severity as level filter to the API", async () => { + listProjectLogs.mockResolvedValue({ + project_id: "proj-1", + logs: [], + }); + + await runProjectsLogsList({ + severity: "error", + }); + + expect(listProjectLogs).toHaveBeenCalledWith("proj-1", { + limit: 20, + level: "error", + }); + }); + + it("prefers --level over --severity when both are set", async () => { + listProjectLogs.mockResolvedValue({ + project_id: "proj-1", + logs: [], + }); + + await runProjectsLogsList({ + level: "warn", + severity: "error", + }); + + expect(listProjectLogs).toHaveBeenCalledWith("proj-1", { + limit: 20, + level: "warn", + }); + }); + + it("passes session-scoped logs with level filter", async () => { + listSessionLogs.mockResolvedValue({ + project_id: "proj-1", + orchestrator_session_id: "orch-session-abc123", + logs: [], + }); + + await runProjectsLogsList({ + sessionId: "orch-session-abc123", + level: "debug", + }); + + expect(listSessionLogs).toHaveBeenCalledWith( + "proj-1", + "orch-session-abc123", + { limit: 20, level: "debug" }, + ); + }); + it("outputs JSON when --json is set", async () => { listSessionLogs.mockResolvedValue({ project_id: "proj-1", diff --git a/src/commands/projects/logs/list.ts b/src/commands/projects/logs/list.ts index 0a3fc45..be4986c 100644 --- a/src/commands/projects/logs/list.ts +++ b/src/commands/projects/logs/list.ts @@ -14,6 +14,8 @@ export interface ProjectsLogsListOptions { sessionId?: string; q?: string; level?: AgentLogLevel; + /** Alias for `level` (--severity on CLI). */ + severity?: AgentLogLevel; json?: boolean; } @@ -22,8 +24,9 @@ function buildQuery(options: ProjectsLogsListOptions): ListAgentLogsQuery { if (options.q?.trim()) { query.q = options.q.trim(); } - if (options.level) { - query.level = options.level; + const level = options.level ?? options.severity; + if (level) { + query.level = level; } return query; } diff --git a/src/lib/api.ts b/src/lib/api.ts index 5651192..231748c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -439,22 +439,6 @@ export interface ProjectSessionListResponse { count: number; } -export interface ProjectSessionErrorEntry { - id: string; - orchestrator_session_id: string; - source: "agent" | "runner" | "provisioning"; - code: string; - message: string; - stack_trace: string | null; - created_at: string; -} - -export interface ProjectSessionErrorsResponse { - project_id: string; - orchestrator_session_id?: string; - errors: ProjectSessionErrorEntry[]; -} - export type AgentLogLevel = "debug" | "info" | "warn" | "error"; export interface AgentLogEntry { @@ -1019,27 +1003,6 @@ export class VoicethereApi { ); } - async listProjectSessionErrors( - projectId: string, - limit = 20, - ): Promise { - const params = new URLSearchParams({ limit: String(limit) }); - return this.request( - "GET", - `/projects/${projectId}/session-errors?${params.toString()}`, - ); - } - - async listSessionErrors( - projectId: string, - orchestratorSessionId: string, - ): Promise { - return this.request( - "GET", - `/projects/${projectId}/sessions/${encodeURIComponent(orchestratorSessionId)}/errors`, - ); - } - async listProjectLogs( projectId: string, query: ListAgentLogsQuery = {},