mcp-codex-worker 1.0.18 → 1.0.20

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,274 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Timeline Writer — pure formatter for timeline.log
3
+ //
4
+ // Receives a raw event (same shape as events.jsonl entries) and returns
5
+ // a formatted one-liner or null (skip). No state — deduplication is
6
+ // handled by the caller via lastTokenCount.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export interface TimelineConfig {
10
+ maxCommandLength: number;
11
+ maxMessageLength: number;
12
+ maxPathLength: number;
13
+ maxReasoningLength: number;
14
+ maxPlanStepLength: number;
15
+ maxLineLength: number;
16
+ maxStderrLength: number;
17
+ maxErrorLength: number;
18
+ }
19
+
20
+ export const DEFAULT_TIMELINE_CONFIG: TimelineConfig = {
21
+ maxCommandLength: 120,
22
+ maxMessageLength: 200,
23
+ maxPathLength: 80,
24
+ maxReasoningLength: 150,
25
+ maxPlanStepLength: 80,
26
+ maxLineLength: 500,
27
+ maxStderrLength: 200,
28
+ maxErrorLength: 200,
29
+ };
30
+
31
+ export interface TimelineEvent {
32
+ t: string;
33
+ method: string;
34
+ params?: unknown;
35
+ code?: number | null;
36
+ signal?: string | null;
37
+ data?: string;
38
+ synthetic?: boolean;
39
+ }
40
+
41
+ export interface TimelineResult {
42
+ line: string | null;
43
+ newTokenCount?: number;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Main entry point
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export function formatTimelineLine(
51
+ event: TimelineEvent,
52
+ config: TimelineConfig = DEFAULT_TIMELINE_CONFIG,
53
+ lastTokenCount?: number,
54
+ ): TimelineResult {
55
+ const ts = localTime(event.t);
56
+ const p = asObj(event.params);
57
+
58
+ switch (event.method) {
59
+ case 'thread/status/changed': {
60
+ const status = asStr(asObj(p?.status)?.type);
61
+ if (status === 'active') return line(ts, 'STARTED', '', config);
62
+ if (status === 'idle') return line(ts, 'IDLE', '', config);
63
+ if (status === 'waitingOnUserInput') return line(ts, 'ASK', 'waiting for orchestrator', config);
64
+ return SKIP;
65
+ }
66
+
67
+ case 'turn/started': {
68
+ const turnId = asStr(asObj(p?.turn)?.id) ?? '';
69
+ return line(ts, 'TURN', truncate(turnId, 40), config);
70
+ }
71
+
72
+ case 'turn/completed': {
73
+ const turn = asObj(p?.turn);
74
+ const status = asStr(turn?.status) ?? 'unknown';
75
+ const errMsg = asStr(asObj(turn?.error)?.message);
76
+ const detail = errMsg ? `${status}: ${truncate(errMsg, 100)}` : status;
77
+ return line(ts, 'DONE', detail, config);
78
+ }
79
+
80
+ case 'turn/plan/updated': {
81
+ const steps = Array.isArray(p?.plan) ? (p.plan as Array<{ step?: string; status?: string }>) : [];
82
+ return line(ts, 'PLAN', formatPlan(steps, config), config);
83
+ }
84
+
85
+ case 'item/completed':
86
+ return formatItemCompleted(ts, asObj(p?.item), config);
87
+
88
+ case 'thread/tokenUsage/updated': {
89
+ const usage = asObj(asObj(p?.tokenUsage)?.total);
90
+ const window = typeof (asObj(p?.tokenUsage) as Record<string, unknown> | undefined)?.modelContextWindow === 'number'
91
+ ? (asObj(p?.tokenUsage) as Record<string, unknown>).modelContextWindow as number
92
+ : undefined;
93
+ if (!usage || !window) return SKIP;
94
+ const total = typeof usage.totalTokens === 'number' ? usage.totalTokens : 0;
95
+ if (total === (lastTokenCount ?? -1)) return SKIP; // deduplicate
96
+ const pct = (Math.round(total / window * 1000) / 10).toFixed(1);
97
+ return { line: capLine(`${ts} ${pad('TOKENS')} ${total} / ${window} (${pct}%)`, config), newTokenCount: total };
98
+ }
99
+
100
+ case 'item/commandExecution/requestApproval': {
101
+ const cmd = cleanCommand(asStr(p?.command) ?? '');
102
+ return line(ts, 'APPROVE', `cmd: ${truncate(cmd, 100)}`, config);
103
+ }
104
+
105
+ case 'item/fileChange/requestApproval': {
106
+ const changes = Array.isArray(p?.changes) ? (p.changes as Array<{ path?: string }>) : [];
107
+ const paths = changes.map(c => truncatePath(asStr(c?.path) ?? '', 40)).join(', ');
108
+ return line(ts, 'APPROVE', `files: ${paths} (${changes.length} files)`, config);
109
+ }
110
+
111
+ case '_process_exit':
112
+ return line(ts, 'EXIT', `code=${event.code ?? '?'} signal=${String(event.signal ?? 'null')}`, config);
113
+
114
+ case '_stderr': {
115
+ const data = stripAnsi(event.data ?? '').replace(/\n/g, ' ').trim();
116
+ if (!data) return SKIP;
117
+ return line(ts, 'STDERR', truncate(data, config.maxStderrLength), config);
118
+ }
119
+
120
+ case 'error': {
121
+ const errObj = asObj(p?.error);
122
+ const message = asStr(errObj?.message) ?? asStr(p?.message) ?? 'unknown';
123
+ const info = asStr(errObj?.codexErrorInfo) ?? asStr(p?.codexErrorInfo) ?? '';
124
+ const detail = info ? `${info} — ${truncate(message, config.maxErrorLength)}` : truncate(message, config.maxErrorLength);
125
+ return line(ts, 'ERROR', detail, config);
126
+ }
127
+
128
+ default:
129
+ return SKIP;
130
+ }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Item completed sub-formatter
135
+ // ---------------------------------------------------------------------------
136
+
137
+ function formatItemCompleted(
138
+ ts: string,
139
+ item: Record<string, unknown> | undefined,
140
+ config: TimelineConfig,
141
+ ): TimelineResult {
142
+ if (!item) return SKIP;
143
+ const itemType = asStr(item.type);
144
+
145
+ switch (itemType) {
146
+ case 'commandExecution': {
147
+ const cmd = cleanCommand(asStr(item.command) ?? '');
148
+ const dur = typeof item.durationMs === 'number' ? `${(item.durationMs / 1000).toFixed(1)}s` : '?';
149
+ const exit = item.exitCode ?? '?';
150
+ return line(ts, 'CMD', `${truncate(cmd, config.maxCommandLength)} → exit=${exit} (${dur})`, config);
151
+ }
152
+
153
+ case 'fileChange': {
154
+ const changes = Array.isArray(item.changes) ? (item.changes as Array<Record<string, unknown>>) : [];
155
+ if (changes.length === 0) return SKIP;
156
+ const lines = changes.map(c => {
157
+ const path = truncatePath(asStr(c.path) ?? 'unknown', config.maxPathLength);
158
+ const kind = asStr(c.kind) ?? 'modified';
159
+ return capLine(`${ts} ${pad('FILE')} ${path} (${kind})`, config);
160
+ });
161
+ return { line: lines.join('\n') };
162
+ }
163
+
164
+ case 'agentMessage': {
165
+ const text = (asStr(item.text) ?? '').replace(/\n/g, ' ').trim();
166
+ if (!text) return SKIP;
167
+ const max = config.maxMessageLength;
168
+ const display = text.length > max
169
+ ? `${text.slice(0, max)} [+${text.length - max} chars]`
170
+ : text;
171
+ return line(ts, 'MSG', display, config);
172
+ }
173
+
174
+ case 'reasoning': {
175
+ const summaries = Array.isArray(item.summary) ? item.summary as unknown[] : [];
176
+ if (summaries.length === 0) return SKIP;
177
+ const first = typeof summaries[0] === 'string'
178
+ ? summaries[0]
179
+ : asStr((summaries[0] as Record<string, unknown> | undefined)?.text) ?? '';
180
+ const cleaned = first.replace(/\*\*/g, '').replace(/\n/g, ' ').trim();
181
+ if (!cleaned) return SKIP;
182
+ return line(ts, 'THINK', truncate(cleaned, config.maxReasoningLength), config);
183
+ }
184
+
185
+ case 'mcpToolCall': {
186
+ const server = asStr(item.server) ?? '?';
187
+ const tool = asStr(item.tool) ?? '?';
188
+ const status = asStr(item.status) ?? '?';
189
+ return line(ts, 'MCP', `${server}/${tool} → ${status}`, config);
190
+ }
191
+
192
+ default:
193
+ return SKIP;
194
+ }
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Plan formatter
199
+ // ---------------------------------------------------------------------------
200
+
201
+ function formatPlan(steps: Array<{ step?: string; status?: string }>, config: TimelineConfig): string {
202
+ if (steps.length === 0) return '(empty plan)';
203
+
204
+ const icon = (s?: string): string => {
205
+ switch (s) {
206
+ case 'completed': return '✓';
207
+ case 'inProgress': return '→';
208
+ default: return ' ';
209
+ }
210
+ };
211
+
212
+ const formatted = steps.map(s => `[${icon(s.status)}] ${truncate(s.step ?? '', config.maxPlanStepLength)}`);
213
+ const joined = formatted.join(' · ');
214
+
215
+ if (joined.length <= 450) return joined;
216
+ const first3 = formatted.slice(0, 3).join(' · ');
217
+ return `${first3} … +${steps.length - 3} more`;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Helpers
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const SKIP: TimelineResult = { line: null };
225
+
226
+ function pad(tag: string): string {
227
+ return tag.padEnd(7);
228
+ }
229
+
230
+ function line(ts: string, tag: string, detail: string, config: TimelineConfig): TimelineResult {
231
+ return { line: capLine(`${ts} ${pad(tag)} ${detail}`.trimEnd(), config) };
232
+ }
233
+
234
+ function capLine(l: string, config: TimelineConfig): string {
235
+ if (l.length <= config.maxLineLength) return l;
236
+ return l.slice(0, config.maxLineLength - 3) + '...';
237
+ }
238
+
239
+ function localTime(iso: string): string {
240
+ const d = new Date(iso);
241
+ return [d.getHours(), d.getMinutes(), d.getSeconds()]
242
+ .map(n => String(n).padStart(2, '0'))
243
+ .join(':');
244
+ }
245
+
246
+ export function truncate(s: string, max: number): string {
247
+ if (s.length <= max) return s;
248
+ return s.slice(0, max - 1) + '…';
249
+ }
250
+
251
+ function truncatePath(p: string, max: number): string {
252
+ if (!p || p.length <= max) return p ?? '';
253
+ return '…' + p.slice(p.length - max + 1);
254
+ }
255
+
256
+ export function cleanCommand(raw: string): string {
257
+ const match = raw.match(/^\/bin\/(?:z?sh|bash)\s+-[a-z]*c\s+['"]?([\s\S]+?)['"]?$/);
258
+ let cmd = match ? match[1]! : raw;
259
+ if (cmd.startsWith('rtk ')) cmd = cmd.slice(4);
260
+ return cmd;
261
+ }
262
+
263
+ function stripAnsi(s: string): string {
264
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
265
+ }
266
+
267
+ function asObj(v: unknown): Record<string, unknown> | undefined {
268
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return undefined;
269
+ return v as Record<string, unknown>;
270
+ }
271
+
272
+ function asStr(v: unknown): string | undefined {
273
+ return typeof v === 'string' ? v : undefined;
274
+ }