@kodrunhq/opencode-autopilot 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+
3
+ export const baseEventSchema = z.object({
4
+ timestamp: z.string().max(128),
5
+ sessionId: z
6
+ .string()
7
+ .max(256)
8
+ .regex(/^[a-zA-Z0-9_-]{1,256}$/, "Invalid session ID"),
9
+ type: z.enum(["fallback", "error", "decision", "model_switch"]),
10
+ });
11
+
12
+ export const fallbackEventSchema = baseEventSchema.extend({
13
+ type: z.literal("fallback"),
14
+ failedModel: z.string().max(256),
15
+ nextModel: z.string().max(256),
16
+ reason: z.string().max(1024),
17
+ success: z.boolean(),
18
+ });
19
+
20
+ export const errorEventSchema = baseEventSchema.extend({
21
+ type: z.literal("error"),
22
+ errorType: z.enum([
23
+ "rate_limit",
24
+ "quota_exceeded",
25
+ "service_unavailable",
26
+ "missing_api_key",
27
+ "model_not_found",
28
+ "content_filter",
29
+ "context_length",
30
+ "unknown",
31
+ ]),
32
+ model: z.string().max(256),
33
+ message: z.string().max(4096),
34
+ statusCode: z.number().optional(),
35
+ });
36
+
37
+ export const decisionEventSchema = baseEventSchema.extend({
38
+ type: z.literal("decision"),
39
+ phase: z.string().max(128),
40
+ agent: z.string().max(128),
41
+ decision: z.string().max(2048),
42
+ rationale: z.string().max(2048),
43
+ });
44
+
45
+ export const modelSwitchEventSchema = baseEventSchema.extend({
46
+ type: z.literal("model_switch"),
47
+ fromModel: z.string().max(256),
48
+ toModel: z.string().max(256),
49
+ trigger: z.enum(["fallback", "config", "user"]),
50
+ });
51
+
52
+ export const sessionEventSchema = z.discriminatedUnion("type", [
53
+ fallbackEventSchema,
54
+ errorEventSchema,
55
+ decisionEventSchema,
56
+ modelSwitchEventSchema,
57
+ ]);
58
+
59
+ export const loggingConfigSchema = z.object({
60
+ enabled: z.boolean().default(true),
61
+ retentionDays: z.number().min(1).max(365).default(30),
62
+ });
63
+
64
+ export const sessionDecisionSchema = z.object({
65
+ timestamp: z.string().optional(),
66
+ phase: z.string(),
67
+ agent: z.string(),
68
+ decision: z.string(),
69
+ rationale: z.string(),
70
+ });
71
+
72
+ export const sessionLogSchema = z.object({
73
+ schemaVersion: z.number().default(1),
74
+ sessionId: z.string().regex(/^[a-zA-Z0-9_-]{1,256}$/, "Invalid session ID"),
75
+ startedAt: z.string(),
76
+ endedAt: z.string().nullable().default(null),
77
+ events: z.array(sessionEventSchema),
78
+ decisions: z.array(sessionDecisionSchema).default([]),
79
+ errorSummary: z.record(z.string(), z.number()).default({}),
80
+ });
81
+
82
+ // Pre-compute defaults for Zod v4 nested default compatibility
83
+ export const loggingDefaults = loggingConfigSchema.parse({});
@@ -0,0 +1,63 @@
1
+ import { appendFile, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { ensureDir, isEnoentError } from "../utils/fs-helpers";
4
+ import { getGlobalConfigDir } from "../utils/paths";
5
+ import { sessionEventSchema } from "./schemas";
6
+ import type { SessionEvent } from "./types";
7
+
8
+ /**
9
+ * Returns the global logs directory path.
10
+ * Logs are user-scoped, not project-scoped (D-08, D-09).
11
+ */
12
+ export function getLogsDir(): string {
13
+ return join(getGlobalConfigDir(), "logs");
14
+ }
15
+
16
+ /**
17
+ * Returns the JSONL log file path for a given session ID.
18
+ */
19
+ function getSessionLogPath(sessionId: string, logsDir?: string): string {
20
+ return join(logsDir ?? getLogsDir(), `${sessionId}.jsonl`);
21
+ }
22
+
23
+ /**
24
+ * Logs a structured event to the session's JSONL log file.
25
+ * Validates the event against the Zod schema before writing.
26
+ * Creates the logs directory if it does not exist.
27
+ *
28
+ * @param event - The session event to log (D-02, D-04-D-07)
29
+ * @param logsDir - Optional override for the logs directory (for testing)
30
+ */
31
+ export async function logEvent(event: SessionEvent, logsDir?: string): Promise<void> {
32
+ // Validate against schema -- throws on invalid events
33
+ const validated = sessionEventSchema.parse(event);
34
+
35
+ const dir = logsDir ?? getLogsDir();
36
+ await ensureDir(dir);
37
+
38
+ const logPath = getSessionLogPath(validated.sessionId, dir);
39
+ const line = `${JSON.stringify(validated)}\n`;
40
+
41
+ await appendFile(logPath, line, "utf-8");
42
+ }
43
+
44
+ /**
45
+ * Reads and parses all events from a session's JSONL log file.
46
+ * Returns an empty array if the session log does not exist.
47
+ *
48
+ * @param sessionId - The session ID to read logs for
49
+ * @param logsDir - Optional override for the logs directory (for testing)
50
+ */
51
+ export async function getSessionLog(sessionId: string, logsDir?: string): Promise<SessionEvent[]> {
52
+ const logPath = getSessionLogPath(sessionId, logsDir);
53
+
54
+ try {
55
+ const content = await readFile(logPath, "utf-8");
56
+ const lines = content.trim().split("\n").filter(Boolean);
57
+
58
+ return lines.map((line) => sessionEventSchema.parse(JSON.parse(line)));
59
+ } catch (error: unknown) {
60
+ if (isEnoentError(error)) return [];
61
+ throw error;
62
+ }
63
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Session summary generator.
3
+ *
4
+ * Transforms structured SessionLog data into human-readable markdown summaries
5
+ * for post-session analysis. All functions are pure (no I/O, no side effects).
6
+ *
7
+ * Summaries include: metadata, decisions, errors, fallbacks, model switches,
8
+ * and a strategic one-paragraph analysis (D-03, D-11, D-42, D-43).
9
+ */
10
+
11
+ import type { SessionEvent, SessionLog } from "./types";
12
+
13
+ /**
14
+ * Computes session duration in milliseconds from startedAt/endedAt.
15
+ * Returns 0 when endedAt is null (session still in progress).
16
+ */
17
+ export function computeDuration(log: SessionLog): number {
18
+ if (!log.endedAt) return 0;
19
+ return new Date(log.endedAt).getTime() - new Date(log.startedAt).getTime();
20
+ }
21
+
22
+ /**
23
+ * Formats duration in milliseconds to human-readable string.
24
+ * Uses "Xh Ym" for durations >= 1 hour, "Xm Ys" for >= 1 minute, "Xs" otherwise.
25
+ */
26
+ export function formatDuration(ms: number): string {
27
+ const totalSeconds = Math.floor(ms / 1000);
28
+ const hours = Math.floor(totalSeconds / 3600);
29
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
30
+ const seconds = totalSeconds % 60;
31
+
32
+ if (hours > 0) return `${hours}h ${minutes}m`;
33
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
34
+ return `${seconds}s`;
35
+ }
36
+
37
+ /**
38
+ * Formats cost as USD string with 4 decimal places.
39
+ */
40
+ export function formatCost(cost: number): string {
41
+ return `$${cost.toFixed(4)}`;
42
+ }
43
+
44
+ /**
45
+ * Generates a complete markdown summary from a SessionLog.
46
+ *
47
+ * Includes sections for: metadata, decisions, errors, fallbacks,
48
+ * model switches, and strategic summary. Sections are omitted when
49
+ * no relevant data exists.
50
+ */
51
+ export function generateSessionSummary(log: SessionLog): string {
52
+ const sections: string[] = [];
53
+
54
+ // Header
55
+ sections.push(renderHeader(log));
56
+
57
+ // Decisions section (conditional)
58
+ if (log.decisions.length > 0) {
59
+ sections.push(renderDecisions(log));
60
+ }
61
+
62
+ // Errors section (conditional)
63
+ const errorEvents = log.events.filter(
64
+ (e): e is SessionEvent & { type: "error" } => e.type === "error",
65
+ );
66
+ if (errorEvents.length > 0) {
67
+ sections.push(renderErrors(errorEvents, log.errorSummary));
68
+ }
69
+
70
+ // Fallbacks section (conditional)
71
+ const fallbackEvents = log.events.filter(
72
+ (e): e is SessionEvent & { type: "fallback" } => e.type === "fallback",
73
+ );
74
+ if (fallbackEvents.length > 0) {
75
+ sections.push(renderFallbacks(fallbackEvents));
76
+ }
77
+
78
+ // Model switches section (conditional)
79
+ const switchEvents = log.events.filter(
80
+ (e): e is SessionEvent & { type: "model_switch" } => e.type === "model_switch",
81
+ );
82
+ if (switchEvents.length > 0) {
83
+ sections.push(renderModelSwitches(switchEvents));
84
+ }
85
+
86
+ // Strategic summary (always present, varies by content)
87
+ sections.push(renderStrategicSummary(log));
88
+
89
+ return sections.join("\n\n");
90
+ }
91
+
92
+ // --- Internal renderers ---
93
+
94
+ function renderHeader(log: SessionLog): string {
95
+ const durationMs = computeDuration(log);
96
+ const durationStr = log.endedAt ? formatDuration(durationMs) : "In progress";
97
+
98
+ return [
99
+ `# Session Summary: ${log.sessionId}`,
100
+ "",
101
+ "| Field | Value |",
102
+ "|-------|-------|",
103
+ `| Started | ${log.startedAt} |`,
104
+ `| Ended | ${log.endedAt ?? "In progress"} |`,
105
+ `| Duration | ${durationStr} |`,
106
+ `| Events | ${log.events.length} |`,
107
+ `| Decisions | ${log.decisions.length} |`,
108
+ ].join("\n");
109
+ }
110
+
111
+ function renderDecisions(log: SessionLog): string {
112
+ const lines = ["## Decisions", ""];
113
+
114
+ for (let i = 0; i < log.decisions.length; i++) {
115
+ const d = log.decisions[i];
116
+ lines.push(`${i + 1}. **[${d.phase}] ${d.agent}**: ${d.decision}`);
117
+ lines.push(` - _Rationale:_ ${d.rationale}`);
118
+ }
119
+
120
+ return lines.join("\n");
121
+ }
122
+
123
+ function renderErrors(
124
+ events: readonly (SessionEvent & { type: "error" })[],
125
+ errorSummary: Record<string, number>,
126
+ ): string {
127
+ const lines = [
128
+ "## Errors",
129
+ "",
130
+ "| Timestamp | Type | Model | Message |",
131
+ "|-----------|------|-------|---------|",
132
+ ];
133
+
134
+ for (const e of events) {
135
+ const safeMsg = (e.message ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
136
+ const safeModel = (e.model ?? "").replace(/\|/g, "\\|");
137
+ lines.push(`| ${e.timestamp} | ${e.errorType} | ${safeModel} | ${safeMsg} |`);
138
+ }
139
+
140
+ // Error summary counts
141
+ const summaryEntries = Object.entries(errorSummary);
142
+ if (summaryEntries.length > 0) {
143
+ lines.push("");
144
+ lines.push("**Error Summary:**");
145
+ for (const [type, count] of summaryEntries) {
146
+ lines.push(`- ${type}: ${count}`);
147
+ }
148
+ }
149
+
150
+ return lines.join("\n");
151
+ }
152
+
153
+ function renderFallbacks(events: readonly (SessionEvent & { type: "fallback" })[]): string {
154
+ const lines = [
155
+ "## Fallbacks",
156
+ "",
157
+ "| Timestamp | Failed Model | Next Model | Reason | Success |",
158
+ "|-----------|-------------|------------|--------|---------|",
159
+ ];
160
+
161
+ for (const e of events) {
162
+ lines.push(
163
+ `| ${e.timestamp} | ${e.failedModel} | ${e.nextModel} | ${e.reason} | ${e.success ? "Yes" : "No"} |`,
164
+ );
165
+ }
166
+
167
+ return lines.join("\n");
168
+ }
169
+
170
+ function renderModelSwitches(events: readonly (SessionEvent & { type: "model_switch" })[]): string {
171
+ const lines = [
172
+ "## Model Switches",
173
+ "",
174
+ "| Timestamp | From | To | Trigger |",
175
+ "|-----------|------|----|---------|",
176
+ ];
177
+
178
+ for (const e of events) {
179
+ lines.push(`| ${e.timestamp} | ${e.fromModel} | ${e.toModel} | ${e.trigger} |`);
180
+ }
181
+
182
+ return lines.join("\n");
183
+ }
184
+
185
+ function renderStrategicSummary(log: SessionLog): string {
186
+ const errorCount = log.events.filter((e) => e.type === "error").length;
187
+ const fallbackCount = log.events.filter((e) => e.type === "fallback").length;
188
+ const durationMs = computeDuration(log);
189
+ const durationStr = log.endedAt ? formatDuration(durationMs) : "unknown (in progress)";
190
+
191
+ const parts: string[] = [];
192
+ parts.push(
193
+ `Session ran for ${durationStr} with ${log.events.length} events and ${log.decisions.length} autonomous decisions.`,
194
+ );
195
+
196
+ if (errorCount > 0) {
197
+ const successfulFallbacks = log.events.filter((e) => e.type === "fallback" && e.success).length;
198
+ parts.push(
199
+ `Encountered ${errorCount} errors; ${fallbackCount} fallback attempts (${successfulFallbacks} successful).`,
200
+ );
201
+ }
202
+
203
+ if (log.decisions.length > 0) {
204
+ const phases = [...new Set(log.decisions.map((d) => d.phase))];
205
+ parts.push(`Decisions spanned ${phases.length} phase(s): ${phases.join(", ")}.`);
206
+ }
207
+
208
+ return ["## Summary", "", parts.join(" ")].join("\n");
209
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Token accumulation utilities for session observability.
3
+ *
4
+ * Pure functions that accumulate token/cost data from AssistantMessage shapes.
5
+ * Returns new objects (immutable, per CLAUDE.md) -- never mutates inputs.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /**
11
+ * Aggregated token and cost data for a session.
12
+ */
13
+ export interface TokenAggregate {
14
+ readonly inputTokens: number;
15
+ readonly outputTokens: number;
16
+ readonly reasoningTokens: number;
17
+ readonly cacheReadTokens: number;
18
+ readonly cacheWriteTokens: number;
19
+ readonly totalCost: number;
20
+ readonly messageCount: number;
21
+ }
22
+
23
+ /**
24
+ * Returns a zero-initialized TokenAggregate.
25
+ */
26
+ export function createEmptyTokenAggregate(): TokenAggregate {
27
+ return Object.freeze({
28
+ inputTokens: 0,
29
+ outputTokens: 0,
30
+ reasoningTokens: 0,
31
+ cacheReadTokens: 0,
32
+ cacheWriteTokens: 0,
33
+ totalCost: 0,
34
+ messageCount: 0,
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Accumulates token/cost data into a new TokenAggregate.
40
+ * Missing fields in `incoming` default to 0.
41
+ * Returns a new frozen object (immutable).
42
+ *
43
+ * @param current - The current aggregate to add onto
44
+ * @param incoming - Partial aggregate with fields to add
45
+ */
46
+ export function accumulateTokens(
47
+ current: TokenAggregate,
48
+ incoming: Partial<TokenAggregate>,
49
+ ): TokenAggregate {
50
+ return Object.freeze({
51
+ inputTokens: current.inputTokens + (incoming.inputTokens ?? 0),
52
+ outputTokens: current.outputTokens + (incoming.outputTokens ?? 0),
53
+ reasoningTokens: current.reasoningTokens + (incoming.reasoningTokens ?? 0),
54
+ cacheReadTokens: current.cacheReadTokens + (incoming.cacheReadTokens ?? 0),
55
+ cacheWriteTokens: current.cacheWriteTokens + (incoming.cacheWriteTokens ?? 0),
56
+ totalCost: current.totalCost + (incoming.totalCost ?? 0),
57
+ messageCount: current.messageCount + (incoming.messageCount ?? 0),
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Shape of token data within an AssistantMessage from the OpenCode SDK.
63
+ */
64
+ export interface AssistantMessageTokens {
65
+ readonly tokens: {
66
+ readonly input: number;
67
+ readonly output: number;
68
+ readonly reasoning: number;
69
+ readonly cache: {
70
+ readonly read: number;
71
+ readonly write: number;
72
+ };
73
+ };
74
+ readonly cost: number;
75
+ }
76
+
77
+ /**
78
+ * Extracts token/cost data from an AssistantMessage shape and accumulates
79
+ * it into a new TokenAggregate. Increments messageCount by 1.
80
+ *
81
+ * @param current - The current aggregate to add onto
82
+ * @param msg - The AssistantMessage-shaped object with tokens and cost
83
+ */
84
+ export function accumulateTokensFromMessage(
85
+ current: TokenAggregate,
86
+ msg: AssistantMessageTokens,
87
+ ): TokenAggregate {
88
+ return accumulateTokens(current, {
89
+ inputTokens: msg.tokens.input ?? 0,
90
+ outputTokens: msg.tokens.output ?? 0,
91
+ reasoningTokens: msg.tokens.reasoning ?? 0,
92
+ cacheReadTokens: msg.tokens.cache.read ?? 0,
93
+ cacheWriteTokens: msg.tokens.cache.write ?? 0,
94
+ totalCost: msg.cost ?? 0,
95
+ messageCount: 1,
96
+ });
97
+ }
@@ -0,0 +1,24 @@
1
+ import type { z } from "zod";
2
+ import type {
3
+ baseEventSchema,
4
+ decisionEventSchema,
5
+ errorEventSchema,
6
+ fallbackEventSchema,
7
+ loggingConfigSchema,
8
+ modelSwitchEventSchema,
9
+ sessionDecisionSchema,
10
+ sessionEventSchema,
11
+ sessionLogSchema,
12
+ } from "./schemas";
13
+
14
+ export type SessionEventType = "fallback" | "error" | "decision" | "model_switch";
15
+
16
+ export type BaseEvent = z.infer<typeof baseEventSchema>;
17
+ export type FallbackEvent = z.infer<typeof fallbackEventSchema>;
18
+ export type ErrorEvent = z.infer<typeof errorEventSchema>;
19
+ export type DecisionEvent = z.infer<typeof decisionEventSchema>;
20
+ export type ModelSwitchEvent = z.infer<typeof modelSwitchEventSchema>;
21
+ export type SessionEvent = z.infer<typeof sessionEventSchema>;
22
+ export type LoggingConfig = z.infer<typeof loggingConfigSchema>;
23
+ export type SessionDecision = z.infer<typeof sessionDecisionSchema>;
24
+ export type SessionLog = z.infer<typeof sessionLogSchema>;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * oc_logs tool - Session log dashboard.
3
+ *
4
+ * Provides three modes:
5
+ * - "list": List all session logs with summary info
6
+ * - "detail": View full session log with markdown summary
7
+ * - "search": Filter events by type and time range
8
+ *
9
+ * Follows the *Core + tool() wrapper pattern per CLAUDE.md.
10
+ * Returns JSON with displayText field following oc_doctor pattern.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import { tool } from "@opencode-ai/plugin";
16
+ import { z } from "zod";
17
+ import {
18
+ listSessionLogs,
19
+ readLatestSessionLog,
20
+ readSessionLog,
21
+ searchEvents,
22
+ } from "../observability/log-reader";
23
+ import { generateSessionSummary } from "../observability/summary-generator";
24
+
25
+ /**
26
+ * Options for logsCore search/detail modes.
27
+ */
28
+ interface LogsOptions {
29
+ readonly sessionID?: string;
30
+ readonly eventType?: string;
31
+ readonly after?: string;
32
+ readonly before?: string;
33
+ }
34
+
35
+ /**
36
+ * Formats a session list as a human-readable table.
37
+ */
38
+ function formatSessionTable(
39
+ sessions: readonly {
40
+ readonly sessionId: string;
41
+ readonly startedAt: string;
42
+ readonly endedAt: string | null;
43
+ readonly eventCount: number;
44
+ readonly decisionCount: number;
45
+ readonly errorCount: number;
46
+ }[],
47
+ ): string {
48
+ if (sessions.length === 0) {
49
+ return "No session logs found.";
50
+ }
51
+
52
+ const lines = [
53
+ "Session Logs",
54
+ "",
55
+ "| Session ID | Started | Events | Decisions | Errors |",
56
+ "|------------|---------|--------|-----------|--------|",
57
+ ];
58
+
59
+ for (const s of sessions) {
60
+ const started = s.startedAt.replace("T", " ").replace(/\.\d+Z$/, "Z");
61
+ lines.push(
62
+ `| ${s.sessionId} | ${started} | ${s.eventCount} | ${s.decisionCount} | ${s.errorCount} |`,
63
+ );
64
+ }
65
+
66
+ lines.push("", `${sessions.length} session(s) total`);
67
+ return lines.join("\n");
68
+ }
69
+
70
+ /**
71
+ * Core function for the oc_logs tool.
72
+ *
73
+ * @param mode - "list", "detail", or "search"
74
+ * @param options - Optional filters (sessionID, eventType, after, before)
75
+ * @param logsDir - Optional override for logs directory (for testing)
76
+ */
77
+ export async function logsCore(
78
+ mode: "list" | "detail" | "search",
79
+ options?: LogsOptions,
80
+ logsDir?: string,
81
+ ): Promise<string> {
82
+ switch (mode) {
83
+ case "list": {
84
+ const sessions = await listSessionLogs(logsDir);
85
+
86
+ return JSON.stringify({
87
+ action: "logs_list",
88
+ sessions,
89
+ displayText: formatSessionTable(sessions),
90
+ });
91
+ }
92
+
93
+ case "detail": {
94
+ const log = options?.sessionID
95
+ ? await readSessionLog(options.sessionID, logsDir)
96
+ : await readLatestSessionLog(logsDir);
97
+
98
+ if (!log) {
99
+ const target = options?.sessionID
100
+ ? `Session "${options.sessionID}" not found.`
101
+ : "No session logs found.";
102
+ return JSON.stringify({
103
+ action: "error",
104
+ message: target,
105
+ });
106
+ }
107
+
108
+ const summary = generateSessionSummary(log);
109
+
110
+ return JSON.stringify({
111
+ action: "logs_detail",
112
+ sessionLog: log,
113
+ summary,
114
+ displayText: summary,
115
+ });
116
+ }
117
+
118
+ case "search": {
119
+ const log = options?.sessionID
120
+ ? await readSessionLog(options.sessionID, logsDir)
121
+ : await readLatestSessionLog(logsDir);
122
+
123
+ if (!log) {
124
+ const target = options?.sessionID
125
+ ? `Session "${options.sessionID}" not found.`
126
+ : "No session logs found.";
127
+ return JSON.stringify({
128
+ action: "error",
129
+ message: target,
130
+ });
131
+ }
132
+
133
+ const filtered = searchEvents(log.events, {
134
+ type: options?.eventType,
135
+ after: options?.after,
136
+ before: options?.before,
137
+ });
138
+
139
+ const displayLines = [
140
+ `Search Results (${filtered.length} event(s))`,
141
+ "",
142
+ ...filtered.map((e) => `[${e.timestamp}] ${e.type}: ${JSON.stringify(e)}`),
143
+ ];
144
+
145
+ return JSON.stringify({
146
+ action: "logs_search",
147
+ sessionId: log.sessionId,
148
+ events: filtered,
149
+ displayText: displayLines.join("\n"),
150
+ });
151
+ }
152
+ }
153
+ }
154
+
155
+ // --- Tool wrapper ---
156
+
157
+ export const ocLogs = tool({
158
+ description:
159
+ "View session logs. Modes: 'list' shows all sessions, 'detail' shows full log with " +
160
+ "summary, 'search' filters events by type/time. Use to inspect session history and errors.",
161
+ args: {
162
+ mode: z.enum(["list", "detail", "search"]).describe("View mode: list, detail, or search"),
163
+ sessionID: z
164
+ .string()
165
+ .regex(/^[a-zA-Z0-9_-]{1,256}$/)
166
+ .optional()
167
+ .describe("Session ID to view (uses latest if omitted)"),
168
+ eventType: z.string().optional().describe("Filter events by type (for search mode)"),
169
+ after: z.string().optional().describe("Only events after this ISO timestamp (for search mode)"),
170
+ before: z
171
+ .string()
172
+ .optional()
173
+ .describe("Only events before this ISO timestamp (for search mode)"),
174
+ },
175
+ async execute({ mode, sessionID, eventType, after, before }) {
176
+ return logsCore(mode, { sessionID, eventType, after, before });
177
+ },
178
+ });