lunel-cli 0.1.46 → 0.1.48

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.
@@ -6,6 +6,9 @@ export declare class CodexProvider implements AIProvider {
6
6
  private nextId;
7
7
  private pending;
8
8
  private sessions;
9
+ private pendingPermissionRequestIds;
10
+ private assistantMessageIdByTurnId;
11
+ private partTextById;
9
12
  init(): Promise<void>;
10
13
  destroy(): Promise<void>;
11
14
  subscribe(emitter: AiEventEmitter): () => void;
@@ -43,4 +46,54 @@ export declare class CodexProvider implements AIProvider {
43
46
  private send;
44
47
  private call;
45
48
  private handleLine;
49
+ private handleServerRequest;
50
+ private handleNotification;
51
+ private emitMirroredUserMessage;
52
+ private handleItemStarted;
53
+ private handleStructuredItemCompleted;
54
+ private emitTextPart;
55
+ private emitStructuredToolPart;
56
+ private finishAssistantTurn;
57
+ private emitMessagePartEvent;
58
+ private ensureAssistantMessage;
59
+ private upsertLocalMessagePart;
60
+ private fetchServerThreads;
61
+ private parseThreadListEntry;
62
+ private hasNextCursor;
63
+ private ingestThreadMetadata;
64
+ private upsertSession;
65
+ private ensureLocalSession;
66
+ private resolveSessionFromPayload;
67
+ private resolveInFlightTurnId;
68
+ private decodeMessagesFromThreadRead;
69
+ private decodeItemText;
70
+ private decodeReasoningItemText;
71
+ private decodePlanItemText;
72
+ private decodeCommandExecutionItemText;
73
+ private decodeFileLikeItemText;
74
+ private normalizedCommandPhase;
75
+ private shortCommand;
76
+ private flattenTextValue;
77
+ private toSessionInfo;
78
+ private extractThreadObject;
79
+ private extractThreadTitleFromUnknown;
80
+ private extractThreadTitle;
81
+ private extractTurnId;
82
+ private extractItemId;
83
+ private extractTextPayload;
84
+ private extractThreadId;
85
+ private extractCreatedAt;
86
+ private extractUpdatedAt;
87
+ private readTimestamp;
88
+ private firstString;
89
+ private readArray;
90
+ private readString;
91
+ private asRecord;
92
+ private normalizedItemType;
93
+ private isUserRole;
94
+ private isAssistantMessageItem;
95
+ private extractIncomingItem;
96
+ private describeToolPart;
97
+ private extractToolInput;
98
+ private describeCompletedItemOutput;
46
99
  }
package/dist/ai/codex.js CHANGED
@@ -1,26 +1,10 @@
1
1
  // Codex AI provider — spawns `codex app-server` and speaks JSON-RPC 2.0
2
- // over stdin/stdout. Maps Codex's thread/turn model to the same AIProvider
3
- // interface as OpenCode so the rest of the CLI stays unchanged.
2
+ // over stdin/stdout. Maps Codex's thread/turn model onto Lunel's AIProvider
3
+ // contract using the same thread/list + thread/read flow used by Remodex.
4
4
  import * as crypto from "crypto";
5
5
  import { spawn } from "child_process";
6
6
  import { createInterface } from "readline";
7
- // ---------------------------------------------------------------------------
8
- // Codex notification method → mobile app event type mapping.
9
- // Update this map as the Codex app-server protocol evolves.
10
- // ---------------------------------------------------------------------------
11
- const NOTIFICATION_TYPE_MAP = {
12
- "turn/delta": "message.part.updated",
13
- "turn/complete": "message.completed",
14
- "turn/error": "message.error",
15
- "tool/call": "tool.invoked",
16
- "tool/result": "tool.result",
17
- "session/permission": "session.permission.needed",
18
- "thread/status/changed": "session.status.changed",
19
- "thread/tokenUsage/updated": "session.token_usage.updated",
20
- };
21
- // ---------------------------------------------------------------------------
22
- // Provider implementation
23
- // ---------------------------------------------------------------------------
7
+ const THREAD_LIST_SOURCE_KINDS = ["cli", "vscode", "appServer", "exec", "unknown"];
24
8
  export class CodexProvider {
25
9
  proc = null;
26
10
  shuttingDown = false;
@@ -28,9 +12,9 @@ export class CodexProvider {
28
12
  nextId = 1;
29
13
  pending = new Map();
30
14
  sessions = new Map();
31
- // -------------------------------------------------------------------------
32
- // Lifecycle
33
- // -------------------------------------------------------------------------
15
+ pendingPermissionRequestIds = new Map();
16
+ assistantMessageIdByTurnId = new Map();
17
+ partTextById = new Map();
34
18
  async init() {
35
19
  console.log("Starting Codex app-server...");
36
20
  this.proc = spawn("codex", ["app-server"], {
@@ -50,7 +34,6 @@ export class CodexProvider {
50
34
  this.emitter?.({ type: "sse_dead", properties: { error: msg } });
51
35
  }
52
36
  });
53
- // Handshake: initialize the JSON-RPC session.
54
37
  await this.call("initialize", { clientInfo: { name: "lunel", version: "1.0" } });
55
38
  console.log("Codex ready.\n");
56
39
  }
@@ -66,159 +49,137 @@ export class CodexProvider {
66
49
  this.emitter = null;
67
50
  };
68
51
  }
69
- // Codex doesn't need session reconnect validation, so setActiveSession is omitted.
70
- // -------------------------------------------------------------------------
71
- // Session management (emulated locally — Codex has no persistent store)
72
- // -------------------------------------------------------------------------
73
52
  async createSession(title) {
74
- const id = crypto.randomUUID();
75
- const session = { id, title, messages: [], createdAt: Date.now() };
76
- this.sessions.set(id, session);
77
- // Start a new Codex thread for this session.
78
- try {
79
- const result = await this.call("thread/start", { title });
80
- if (result?.threadId) {
81
- session.threadId = result.threadId;
82
- }
83
- }
84
- catch (err) {
85
- console.warn("[codex] thread/start failed:", err.message);
53
+ const result = await this.call("thread/start", {});
54
+ const threadId = this.extractThreadId(result);
55
+ if (!threadId) {
56
+ throw new Error("thread/start response missing threadId");
86
57
  }
87
- return { session: { id, title: title ?? "", created: session.createdAt } };
58
+ const threadTitle = this.extractThreadTitleFromUnknown(result) ?? title ?? "Conversation";
59
+ const session = this.upsertSession({
60
+ id: threadId,
61
+ title: threadTitle,
62
+ createdAt: this.extractCreatedAt(result) ?? Date.now(),
63
+ updatedAt: this.extractUpdatedAt(result) ?? Date.now(),
64
+ });
65
+ return { session: this.toSessionInfo(session) };
88
66
  }
89
67
  async listSessions() {
90
- return {
91
- sessions: Array.from(this.sessions.values()).map((s) => ({
92
- id: s.id,
93
- title: s.title ?? "",
94
- created: s.createdAt,
95
- })),
96
- };
68
+ const remoteThreads = await this.fetchServerThreads();
69
+ for (const thread of remoteThreads) {
70
+ this.upsertSession(thread);
71
+ }
72
+ const sessions = Array.from(this.sessions.values())
73
+ .sort((a, b) => a.updatedAt - b.updatedAt)
74
+ .map((session) => this.toSessionInfo(session));
75
+ return { sessions };
97
76
  }
98
77
  async getSession(id) {
99
- const s = this.sessions.get(id);
100
- if (!s)
78
+ const session = this.sessions.get(id);
79
+ if (session) {
80
+ return { session: this.toSessionInfo(session) };
81
+ }
82
+ await this.listSessions();
83
+ const next = this.sessions.get(id);
84
+ if (!next) {
101
85
  throw Object.assign(new Error(`Session ${id} not found`), { code: "ENOENT" });
102
- return { session: { id: s.id, title: s.title ?? "", created: s.createdAt } };
86
+ }
87
+ return { session: this.toSessionInfo(next) };
103
88
  }
104
89
  async deleteSession(id) {
105
90
  this.sessions.delete(id);
106
91
  return {};
107
92
  }
108
- // -------------------------------------------------------------------------
109
- // Messages
110
- // -------------------------------------------------------------------------
111
93
  async getMessages(sessionId) {
112
- const s = this.sessions.get(sessionId);
113
- return { messages: s?.messages ?? [] };
94
+ const session = this.ensureLocalSession(sessionId);
95
+ const result = await this.call("thread/read", {
96
+ threadId: sessionId,
97
+ includeTurns: true,
98
+ });
99
+ const threadObject = this.extractThreadObject(result);
100
+ if (!threadObject) {
101
+ return { messages: session.messages };
102
+ }
103
+ const historyMessages = this.decodeMessagesFromThreadRead(sessionId, threadObject);
104
+ if (historyMessages.length > 0) {
105
+ session.messages = historyMessages;
106
+ session.updatedAt = this.extractUpdatedAt(threadObject) ?? session.updatedAt;
107
+ session.createdAt = this.extractCreatedAt(threadObject) ?? session.createdAt;
108
+ const title = this.extractThreadTitle(threadObject);
109
+ if (title)
110
+ session.title = title;
111
+ }
112
+ return { messages: session.messages };
114
113
  }
115
- // -------------------------------------------------------------------------
116
- // Interaction
117
- // -------------------------------------------------------------------------
118
114
  async prompt(sessionId, text, model, agent) {
119
- const session = this.sessions.get(sessionId);
120
- const threadId = session?.threadId;
121
- // Append the user message to local history immediately.
122
- if (session) {
123
- session.messages.push({
124
- id: crypto.randomUUID(),
125
- role: "user",
126
- parts: [{ type: "text", text }],
127
- time: Date.now(),
128
- });
129
- }
130
- // Fire-and-forget — the response streams back as JSON-RPC notifications.
131
- this.call("turn/start", {
132
- ...(threadId ? { threadId } : { sessionId }),
133
- message: { role: "user", content: text },
134
- ...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
135
- ...(agent ? { agent } : {}),
136
- }).catch((err) => {
137
- console.error("[codex] turn/start error:", err.message);
138
- this.emitter?.({ type: "prompt_error", properties: { sessionId, error: err.message } });
139
- });
115
+ const session = this.ensureLocalSession(sessionId);
116
+ session.updatedAt = Date.now();
117
+ (async () => {
118
+ try {
119
+ await this.call("turn/start", {
120
+ threadId: session.id,
121
+ input: [{ type: "text", text }],
122
+ ...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
123
+ ...(agent ? { agent } : {}),
124
+ });
125
+ }
126
+ catch (err) {
127
+ const message = err.message;
128
+ console.error("[codex] turn/start error:", message);
129
+ this.emitter?.({ type: "prompt_error", properties: { sessionId, error: message } });
130
+ }
131
+ })();
140
132
  return { ack: true };
141
133
  }
142
134
  async abort(sessionId) {
143
- const session = this.sessions.get(sessionId);
144
- await this.call("turn/abort", { ...(session?.threadId ? { threadId: session.threadId } : { sessionId }) });
135
+ const session = this.ensureLocalSession(sessionId);
136
+ const turnId = session.activeTurnId ?? await this.resolveInFlightTurnId(sessionId);
137
+ if (!turnId) {
138
+ throw new Error(`Session ${sessionId} has no active interruptible Codex turn`);
139
+ }
140
+ session.activeTurnId = turnId;
141
+ await this.call("turn/interrupt", { threadId: sessionId, turnId });
145
142
  return {};
146
143
  }
147
- // -------------------------------------------------------------------------
148
- // Metadata
149
- // -------------------------------------------------------------------------
150
144
  async agents() {
151
- try {
152
- const result = await this.call("agent/list", {});
153
- return { agents: result ?? [] };
154
- }
155
- catch {
156
- // Codex may not support agents — return empty list gracefully.
157
- return { agents: [] };
158
- }
145
+ return { agents: [] };
159
146
  }
160
147
  async providers() {
161
- try {
162
- const result = await this.call("provider/list", {});
163
- return {
164
- providers: result?.providers ?? [],
165
- default: result?.default ?? {},
166
- };
167
- }
168
- catch {
169
- return { providers: [], default: {} };
170
- }
148
+ return { providers: [], default: {} };
171
149
  }
172
- // -------------------------------------------------------------------------
173
- // Auth
174
- // -------------------------------------------------------------------------
175
150
  async setAuth(providerId, key) {
176
- await this.call("auth/set", { providerId, key });
177
- return {};
151
+ throw new Error("Codex auth configuration is not supported by Lunel yet");
178
152
  }
179
- // -------------------------------------------------------------------------
180
- // Session operations
181
- // -------------------------------------------------------------------------
182
153
  async command(sessionId, command, args) {
183
- const session = this.sessions.get(sessionId);
184
- const result = await this.call("session/command", {
185
- ...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
186
- command,
187
- arguments: args,
188
- });
189
- return { result: result ?? null };
154
+ throw new Error("Codex command execution is not supported by Lunel yet");
190
155
  }
191
156
  async revert(sessionId, messageId) {
192
- const session = this.sessions.get(sessionId);
193
- await this.call("session/revert", {
194
- ...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
195
- messageId,
196
- });
197
- return {};
157
+ throw new Error("Codex revert is not supported by Lunel yet");
198
158
  }
199
159
  async unrevert(sessionId) {
200
- const session = this.sessions.get(sessionId);
201
- await this.call("session/unrevert", {
202
- ...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
203
- });
204
- return {};
160
+ throw new Error("Codex unrevert is not supported by Lunel yet");
205
161
  }
206
162
  async share(sessionId) {
207
- // Codex has no share concept — return a null stub so the app degrades gracefully.
208
163
  return { share: { url: null } };
209
164
  }
210
165
  async permissionReply(sessionId, permissionId, response) {
211
- const session = this.sessions.get(sessionId);
212
- await this.call("session/permission", {
213
- ...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
214
- permissionId,
215
- response,
166
+ const pending = this.pendingPermissionRequestIds.get(permissionId);
167
+ if (!pending) {
168
+ throw new Error(`Codex permission request ${permissionId} is no longer pending`);
169
+ }
170
+ const decision = response === "reject"
171
+ ? "decline"
172
+ : response === "always"
173
+ ? "acceptForSession"
174
+ : "accept";
175
+ this.send({ jsonrpc: "2.0", id: pending.requestId, result: decision });
176
+ this.pendingPermissionRequestIds.delete(permissionId);
177
+ this.emitter?.({
178
+ type: "permission.replied",
179
+ properties: { sessionID: sessionId, permissionId, response },
216
180
  });
217
181
  return {};
218
182
  }
219
- // -------------------------------------------------------------------------
220
- // JSON-RPC internals
221
- // -------------------------------------------------------------------------
222
183
  send(req) {
223
184
  if (!this.proc?.stdin?.writable)
224
185
  return;
@@ -227,12 +188,12 @@ export class CodexProvider {
227
188
  call(method, params) {
228
189
  return new Promise((resolve, reject) => {
229
190
  const id = this.nextId++;
230
- this.pending.set(id, { resolve, reject });
191
+ const key = String(id);
192
+ this.pending.set(key, { resolve, reject });
231
193
  this.send({ jsonrpc: "2.0", id, method, params });
232
- // Safety timeout: if Codex never responds, don't hang the caller forever.
233
194
  setTimeout(() => {
234
- if (this.pending.has(id)) {
235
- this.pending.delete(id);
195
+ if (this.pending.has(key)) {
196
+ this.pending.delete(key);
236
197
  reject(new Error(`Codex RPC timeout: ${method} (id=${id})`));
237
198
  }
238
199
  }, 30_000);
@@ -244,28 +205,848 @@ export class CodexProvider {
244
205
  msg = JSON.parse(line);
245
206
  }
246
207
  catch {
247
- return; // ignore non-JSON lines (e.g. startup banners)
208
+ return;
248
209
  }
249
- // JSON-RPC response (has numeric id + result or error)
250
- if ("id" in msg && typeof msg.id === "number") {
210
+ if ("method" in msg && typeof msg.method === "string") {
211
+ const inbound = msg;
212
+ const params = this.asRecord(inbound.params);
213
+ if (inbound.id != null) {
214
+ this.handleServerRequest(inbound.method, inbound.id, params);
215
+ }
216
+ else {
217
+ this.handleNotification(inbound.method, params);
218
+ }
219
+ return;
220
+ }
221
+ if ("id" in msg) {
251
222
  const resp = msg;
252
- const pending = this.pending.get(resp.id);
253
- if (pending) {
254
- this.pending.delete(resp.id);
255
- if (resp.error) {
256
- pending.reject(new Error(resp.error.message));
223
+ const pending = this.pending.get(String(resp.id));
224
+ if (!pending)
225
+ return;
226
+ this.pending.delete(String(resp.id));
227
+ if (resp.error) {
228
+ pending.reject(new Error(resp.error.message));
229
+ }
230
+ else {
231
+ pending.resolve(resp.result ?? null);
232
+ }
233
+ }
234
+ }
235
+ handleServerRequest(method, requestId, params) {
236
+ const session = this.resolveSessionFromPayload(params);
237
+ if (method === "item/tool/requestUserInput") {
238
+ this.send({
239
+ jsonrpc: "2.0",
240
+ id: requestId,
241
+ error: { code: -32601, message: "Structured user input is not supported by Lunel yet" },
242
+ });
243
+ return;
244
+ }
245
+ if (method === "item/commandExecution/requestApproval"
246
+ || method === "item/fileChange/requestApproval"
247
+ || method.endsWith("requestApproval")) {
248
+ const permissionId = String(requestId);
249
+ const callId = this.extractItemId(params) ?? undefined;
250
+ const messageId = session ? this.ensureAssistantMessage(session, this.extractTurnId(params) ?? `session:${session.id}`) : undefined;
251
+ this.pendingPermissionRequestIds.set(permissionId, {
252
+ sessionId: session?.id ?? "",
253
+ requestId,
254
+ messageId,
255
+ callId,
256
+ });
257
+ this.emitter?.({
258
+ type: "permission.updated",
259
+ properties: {
260
+ id: permissionId,
261
+ sessionID: session?.id,
262
+ messageID: messageId,
263
+ callID: callId,
264
+ type: method,
265
+ title: this.readString(params.reason) ?? this.readString(params.command) ?? method,
266
+ metadata: params,
267
+ },
268
+ });
269
+ return;
270
+ }
271
+ this.send({
272
+ jsonrpc: "2.0",
273
+ id: requestId,
274
+ error: { code: -32601, message: `Unsupported request method: ${method}` },
275
+ });
276
+ }
277
+ handleNotification(method, params) {
278
+ this.ingestThreadMetadata(params);
279
+ const session = this.resolveSessionFromPayload(params);
280
+ switch (method) {
281
+ case "thread/started":
282
+ case "thread/name/updated":
283
+ if (session) {
284
+ const title = this.extractThreadTitle(params);
285
+ if (title)
286
+ session.title = title;
287
+ session.updatedAt = this.extractUpdatedAt(params) ?? Date.now();
288
+ this.emitter?.({ type: "session.updated", properties: { info: this.toSessionInfo(session) } });
289
+ }
290
+ return;
291
+ case "thread/status/changed":
292
+ if (session) {
293
+ this.emitter?.({
294
+ type: "session.status",
295
+ properties: { sessionID: session.id, status: params.status ?? params },
296
+ });
297
+ }
298
+ return;
299
+ case "turn/started":
300
+ if (session) {
301
+ const turnId = this.extractTurnId(params);
302
+ if (turnId)
303
+ session.activeTurnId = turnId;
304
+ session.updatedAt = Date.now();
305
+ this.emitter?.({
306
+ type: "session.status",
307
+ properties: { sessionID: session.id, status: { type: "running" } },
308
+ });
257
309
  }
258
- else {
259
- pending.resolve(resp.result ?? null);
310
+ return;
311
+ case "turn/completed":
312
+ case "turn/failed":
313
+ if (session) {
314
+ session.activeTurnId = undefined;
315
+ session.updatedAt = Date.now();
316
+ this.finishAssistantTurn(session, params, method === "turn/failed");
317
+ this.emitter?.({ type: "session.idle", properties: { sessionID: session.id } });
318
+ if (method === "turn/failed") {
319
+ const error = this.readString(this.asRecord(params.error).message) ?? "Turn failed";
320
+ this.emitter?.({ type: "session.error", properties: { sessionID: session.id, error } });
321
+ }
260
322
  }
323
+ return;
324
+ case "error":
325
+ case "codex/event/error":
326
+ if (session) {
327
+ const error = this.readString(this.asRecord(params.error).message)
328
+ ?? this.readString(params.message)
329
+ ?? "Codex error";
330
+ this.emitter?.({ type: "session.error", properties: { sessionID: session.id, error } });
331
+ }
332
+ return;
333
+ case "codex/event/user_message":
334
+ this.emitMirroredUserMessage(session, params);
335
+ return;
336
+ case "item/started":
337
+ case "codex/event/item_started":
338
+ this.handleItemStarted(session, params);
339
+ return;
340
+ case "item/agentMessage/delta":
341
+ case "codex/event/agent_message_content_delta":
342
+ case "codex/event/agent_message_delta":
343
+ this.emitTextPart(session, params, "text", false);
344
+ return;
345
+ case "item/reasoning/summaryTextDelta":
346
+ case "item/reasoning/summaryPartAdded":
347
+ case "item/reasoning/textDelta":
348
+ this.emitTextPart(session, params, "reasoning", false);
349
+ return;
350
+ case "item/fileChange/outputDelta":
351
+ case "item/toolCall/outputDelta":
352
+ case "item/toolCall/output_delta":
353
+ case "item/tool_call/outputDelta":
354
+ case "item/tool_call/output_delta":
355
+ case "item/commandExecution/outputDelta":
356
+ case "item/command_execution/outputDelta":
357
+ case "codex/event/exec_command_output_delta":
358
+ case "codex/event/read":
359
+ case "codex/event/search":
360
+ case "codex/event/list_files":
361
+ case "turn/diff/updated":
362
+ case "codex/event/turn_diff_updated":
363
+ case "codex/event/turn_diff":
364
+ this.emitStructuredToolPart(session, method, params, false);
365
+ return;
366
+ case "item/completed":
367
+ case "codex/event/item_completed":
368
+ case "codex/event/agent_message":
369
+ case "codex/event/exec_command_end":
370
+ case "codex/event/patch_apply_end":
371
+ if (this.handleStructuredItemCompleted(session, params)) {
372
+ return;
373
+ }
374
+ this.emitTextPart(session, params, "text", true);
375
+ return;
376
+ case "serverRequest/resolved": {
377
+ const permissionId = this.readString(params.requestId) ?? this.readString(params.requestID);
378
+ if (permissionId && this.pendingPermissionRequestIds.has(permissionId)) {
379
+ this.pendingPermissionRequestIds.delete(permissionId);
380
+ this.emitter?.({ type: "permission.replied", properties: { permissionId } });
381
+ }
382
+ return;
383
+ }
384
+ default:
385
+ return;
386
+ }
387
+ }
388
+ emitMirroredUserMessage(session, params) {
389
+ if (!session)
390
+ return;
391
+ const text = this.readString(params.message) ?? this.readString(params.text);
392
+ if (!text)
393
+ return;
394
+ const turnId = this.extractTurnId(params) ?? `mirrored:${crypto.randomUUID()}`;
395
+ const messageId = `user:${turnId}`;
396
+ if (!session.messages.find((message) => message.id === messageId)) {
397
+ session.messages.push({
398
+ id: messageId,
399
+ role: "user",
400
+ parts: [{ id: `${messageId}:text`, type: "text", text }],
401
+ time: Date.now(),
402
+ });
403
+ }
404
+ this.emitter?.({
405
+ type: "message.updated",
406
+ properties: { info: { sessionID: session.id, id: messageId, role: "user" } },
407
+ });
408
+ this.emitter?.({
409
+ type: "message.part.updated",
410
+ properties: {
411
+ part: { id: `${messageId}:text`, sessionID: session.id, messageID: messageId, type: "text", text },
412
+ message: { sessionID: session.id, id: messageId, role: "user" },
413
+ },
414
+ });
415
+ }
416
+ handleItemStarted(session, params) {
417
+ if (!session)
418
+ return;
419
+ const item = this.extractIncomingItem(params);
420
+ const itemType = this.normalizedItemType(this.readString(item.type) ?? "");
421
+ if (itemType === "reasoning") {
422
+ this.emitTextPart(session, params, "reasoning", false);
423
+ return;
424
+ }
425
+ if (itemType === "toolcall"
426
+ || itemType === "commandexecution"
427
+ || itemType === "filechange"
428
+ || itemType === "diff"
429
+ || itemType === "plan"
430
+ || itemType === "contextcompaction"
431
+ || itemType === "enteredreviewmode") {
432
+ this.emitStructuredToolPart(session, itemType, params, false);
433
+ return;
434
+ }
435
+ if (this.isAssistantMessageItem(itemType, this.readString(item.role))) {
436
+ this.ensureAssistantMessage(session, this.extractTurnId(params) ?? `session:${session.id}`);
437
+ }
438
+ }
439
+ handleStructuredItemCompleted(session, params) {
440
+ if (!session)
441
+ return false;
442
+ const item = this.extractIncomingItem(params);
443
+ const itemType = this.normalizedItemType(this.readString(item.type) ?? "");
444
+ if (itemType === "toolcall"
445
+ || itemType === "commandexecution"
446
+ || itemType === "filechange"
447
+ || itemType === "diff"
448
+ || itemType === "plan"
449
+ || itemType === "contextcompaction"
450
+ || itemType === "enteredreviewmode") {
451
+ this.emitStructuredToolPart(session, itemType, params, true);
452
+ return true;
453
+ }
454
+ return false;
455
+ }
456
+ emitTextPart(session, params, partType, preferWholeText) {
457
+ if (!session)
458
+ return;
459
+ const turnId = this.extractTurnId(params) ?? session.activeTurnId ?? `session:${session.id}`;
460
+ const messageId = this.ensureAssistantMessage(session, turnId);
461
+ const partKey = `${messageId}:${partType}:${this.extractItemId(params) ?? "main"}`;
462
+ const nextChunk = this.extractTextPayload(params);
463
+ if (!nextChunk)
464
+ return;
465
+ const nextText = preferWholeText ? nextChunk : `${this.partTextById.get(partKey) ?? ""}${nextChunk}`;
466
+ this.partTextById.set(partKey, nextText);
467
+ session.updatedAt = Date.now();
468
+ this.upsertLocalMessagePart(session, messageId, {
469
+ id: partKey,
470
+ sessionID: session.id,
471
+ messageID: messageId,
472
+ type: partType,
473
+ text: nextText,
474
+ });
475
+ this.emitMessagePartEvent(session.id, messageId, "assistant", {
476
+ id: partKey,
477
+ sessionID: session.id,
478
+ messageID: messageId,
479
+ type: partType,
480
+ text: nextText,
481
+ });
482
+ }
483
+ emitStructuredToolPart(session, method, params, completed) {
484
+ if (!session)
485
+ return;
486
+ const turnId = this.extractTurnId(params) ?? session.activeTurnId ?? `session:${session.id}`;
487
+ const messageId = this.ensureAssistantMessage(session, turnId);
488
+ const item = this.extractIncomingItem(params);
489
+ const normalizedType = this.normalizedItemType(this.readString(item.type) ?? method);
490
+ const itemId = this.extractItemId(params) ?? this.readString(item.id) ?? normalizedType ?? "tool";
491
+ const partId = `${messageId}:tool:${itemId}`;
492
+ const nextText = this.extractTextPayload(params);
493
+ const prevOutput = this.partTextById.get(partId) ?? "";
494
+ const output = completed
495
+ ? nextText ?? prevOutput
496
+ : `${prevOutput}${nextText ?? ""}`;
497
+ if (output) {
498
+ this.partTextById.set(partId, output);
499
+ }
500
+ const state = completed ? "completed" : "running";
501
+ const name = this.describeToolPart(normalizedType, method, item);
502
+ const input = this.extractToolInput(item, params);
503
+ const outputValue = output || this.describeCompletedItemOutput(item, normalizedType) || undefined;
504
+ const part = {
505
+ id: partId,
506
+ sessionID: session.id,
507
+ messageID: messageId,
508
+ type: normalizedType === "plan" ? "reasoning" : "tool",
509
+ ...(normalizedType === "plan"
510
+ ? { text: outputValue ?? "Planning..." }
511
+ : { name, toolName: name, input, output: outputValue, state }),
512
+ };
513
+ this.upsertLocalMessagePart(session, messageId, part);
514
+ this.emitMessagePartEvent(session.id, messageId, "assistant", part);
515
+ }
516
+ finishAssistantTurn(session, params, failed) {
517
+ const turnId = this.extractTurnId(params);
518
+ if (!turnId)
519
+ return;
520
+ const messageId = this.assistantMessageIdByTurnId.get(turnId);
521
+ if (!messageId)
522
+ return;
523
+ const finishPartId = `${messageId}:finish`;
524
+ const part = {
525
+ id: finishPartId,
526
+ sessionID: session.id,
527
+ messageID: messageId,
528
+ type: "step-finish",
529
+ title: failed ? "Failed" : "Completed",
530
+ time: { end: Date.now() },
531
+ };
532
+ this.upsertLocalMessagePart(session, messageId, part);
533
+ this.emitMessagePartEvent(session.id, messageId, "assistant", part);
534
+ }
535
+ emitMessagePartEvent(sessionId, messageId, role, part) {
536
+ this.emitter?.({
537
+ type: "message.updated",
538
+ properties: { info: { sessionID: sessionId, id: messageId, role } },
539
+ });
540
+ this.emitter?.({
541
+ type: "message.part.updated",
542
+ properties: {
543
+ part,
544
+ message: { sessionID: sessionId, id: messageId, role },
545
+ },
546
+ });
547
+ }
548
+ ensureAssistantMessage(session, turnId) {
549
+ const existing = this.assistantMessageIdByTurnId.get(turnId);
550
+ if (existing)
551
+ return existing;
552
+ const messageId = crypto.randomUUID();
553
+ this.assistantMessageIdByTurnId.set(turnId, messageId);
554
+ session.messages.push({
555
+ id: messageId,
556
+ role: "assistant",
557
+ parts: [{
558
+ id: `${messageId}:start`,
559
+ type: "step-start",
560
+ title: "Working",
561
+ time: { start: Date.now() },
562
+ }],
563
+ time: Date.now(),
564
+ });
565
+ return messageId;
566
+ }
567
+ upsertLocalMessagePart(session, messageId, part) {
568
+ const message = session.messages.find((entry) => entry.id === messageId);
569
+ if (!message)
570
+ return;
571
+ const parts = Array.isArray(message.parts) ? [...message.parts] : [];
572
+ const idx = parts.findIndex((entry) => this.asRecord(entry).id === part.id);
573
+ if (idx >= 0) {
574
+ parts[idx] = part;
575
+ }
576
+ else {
577
+ parts.push(part);
578
+ }
579
+ message.parts = parts;
580
+ message.time = Date.now();
581
+ }
582
+ async fetchServerThreads() {
583
+ const threads = [];
584
+ let nextCursor = null;
585
+ let hasRequestedFirstPage = false;
586
+ do {
587
+ const result = await this.call("thread/list", {
588
+ sourceKinds: THREAD_LIST_SOURCE_KINDS,
589
+ cursor: nextCursor,
590
+ });
591
+ const payload = this.asRecord(result);
592
+ const page = this.readArray(payload.data) ?? this.readArray(payload.items) ?? this.readArray(payload.threads) ?? [];
593
+ for (const entry of page) {
594
+ const parsed = this.parseThreadListEntry(entry);
595
+ if (parsed)
596
+ threads.push(parsed);
261
597
  }
598
+ nextCursor = payload.nextCursor ?? payload.next_cursor ?? null;
599
+ hasRequestedFirstPage = true;
600
+ } while (hasRequestedFirstPage && this.hasNextCursor(nextCursor));
601
+ return threads;
602
+ }
603
+ parseThreadListEntry(value) {
604
+ const obj = this.asRecord(value);
605
+ const id = this.extractThreadId(obj);
606
+ if (!id)
607
+ return undefined;
608
+ return {
609
+ id,
610
+ title: this.extractThreadTitle(obj),
611
+ createdAt: this.extractCreatedAt(obj),
612
+ updatedAt: this.extractUpdatedAt(obj),
613
+ };
614
+ }
615
+ hasNextCursor(value) {
616
+ if (value == null)
617
+ return false;
618
+ if (typeof value === "string")
619
+ return value.trim().length > 0;
620
+ return true;
621
+ }
622
+ ingestThreadMetadata(payload) {
623
+ const threadId = this.extractThreadId(payload);
624
+ if (!threadId)
262
625
  return;
626
+ const title = this.extractThreadTitleFromUnknown(payload) ?? "Conversation";
627
+ this.upsertSession({
628
+ id: threadId,
629
+ title,
630
+ createdAt: this.extractCreatedAt(payload) ?? Date.now(),
631
+ updatedAt: this.extractUpdatedAt(payload) ?? Date.now(),
632
+ });
633
+ }
634
+ upsertSession(input) {
635
+ const existing = this.sessions.get(input.id);
636
+ if (existing) {
637
+ existing.title = input.title ?? existing.title;
638
+ existing.createdAt = input.createdAt ?? existing.createdAt;
639
+ existing.updatedAt = input.updatedAt ?? existing.updatedAt;
640
+ return existing;
641
+ }
642
+ const session = {
643
+ id: input.id,
644
+ title: input.title ?? "Conversation",
645
+ createdAt: input.createdAt ?? Date.now(),
646
+ updatedAt: input.updatedAt ?? Date.now(),
647
+ messages: [],
648
+ };
649
+ this.sessions.set(input.id, session);
650
+ return session;
651
+ }
652
+ ensureLocalSession(sessionId) {
653
+ const existing = this.sessions.get(sessionId);
654
+ if (existing)
655
+ return existing;
656
+ return this.upsertSession({ id: sessionId, title: "Conversation", createdAt: Date.now(), updatedAt: Date.now() });
657
+ }
658
+ resolveSessionFromPayload(payload) {
659
+ const threadId = this.extractThreadId(payload);
660
+ return threadId ? this.ensureLocalSession(threadId) : undefined;
661
+ }
662
+ async resolveInFlightTurnId(threadId) {
663
+ const result = await this.call("thread/read", { threadId, includeTurns: true });
664
+ const thread = this.extractThreadObject(result);
665
+ const turns = this.readArray(thread.turns);
666
+ for (const turn of turns.slice().reverse()) {
667
+ const turnObj = this.asRecord(turn);
668
+ const status = this.normalizedItemType(this.readString(turnObj.status) ?? this.readString(this.asRecord(turnObj.status).type) ?? "");
669
+ if (status.includes("running")
670
+ || status.includes("active")
671
+ || status.includes("processing")
672
+ || status.includes("started")
673
+ || status.includes("pending")) {
674
+ return this.readString(turnObj.id);
675
+ }
676
+ }
677
+ return undefined;
678
+ }
679
+ decodeMessagesFromThreadRead(threadId, threadObject) {
680
+ const turns = this.readArray(threadObject.turns);
681
+ const messages = [];
682
+ let orderOffset = 0;
683
+ for (const turn of turns) {
684
+ const turnObject = this.asRecord(turn);
685
+ const turnId = this.readString(turnObject.id);
686
+ const turnTime = this.extractUpdatedAt(turnObject) ?? this.extractCreatedAt(turnObject) ?? Date.now();
687
+ const items = this.readArray(turnObject.items);
688
+ for (const item of items) {
689
+ const itemObject = this.asRecord(item);
690
+ const type = this.normalizedItemType(this.readString(itemObject.type) ?? "");
691
+ const itemId = this.readString(itemObject.id) ?? crypto.randomUUID();
692
+ const timestamp = this.extractUpdatedAt(itemObject) ?? this.extractCreatedAt(itemObject) ?? (turnTime + orderOffset++);
693
+ if (type === "usermessage") {
694
+ const text = this.decodeItemText(itemObject);
695
+ if (!text)
696
+ continue;
697
+ messages.push({
698
+ id: itemId,
699
+ role: "user",
700
+ parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
701
+ time: timestamp,
702
+ });
703
+ continue;
704
+ }
705
+ if (type === "agentmessage" || type === "assistantmessage" || (type === "message" && !this.isUserRole(itemObject))) {
706
+ const text = this.decodeItemText(itemObject);
707
+ if (!text)
708
+ continue;
709
+ messages.push({
710
+ id: itemId,
711
+ role: "assistant",
712
+ parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
713
+ time: timestamp,
714
+ });
715
+ if (turnId)
716
+ this.assistantMessageIdByTurnId.set(turnId, itemId);
717
+ continue;
718
+ }
719
+ if (type === "reasoning") {
720
+ const text = this.decodeReasoningItemText(itemObject);
721
+ messages.push({
722
+ id: itemId,
723
+ role: "assistant",
724
+ parts: [{ id: `${itemId}:reasoning`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
725
+ time: timestamp,
726
+ });
727
+ continue;
728
+ }
729
+ if (type === "plan") {
730
+ const text = this.decodePlanItemText(itemObject);
731
+ messages.push({
732
+ id: itemId,
733
+ role: "assistant",
734
+ parts: [{ id: `${itemId}:plan`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
735
+ time: timestamp,
736
+ });
737
+ continue;
738
+ }
739
+ if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
740
+ const output = this.decodeCommandExecutionItemText(itemObject, type);
741
+ messages.push({
742
+ id: itemId,
743
+ role: "assistant",
744
+ parts: [{
745
+ id: `${itemId}:tool`,
746
+ type: "tool",
747
+ name: type === "commandexecution" ? "command" : "system",
748
+ toolName: type === "commandexecution" ? "command" : "system",
749
+ output,
750
+ state: "completed",
751
+ sessionID: threadId,
752
+ messageID: itemId,
753
+ }],
754
+ time: timestamp,
755
+ });
756
+ continue;
757
+ }
758
+ if (type === "filechange" || type === "toolcall" || type === "diff") {
759
+ const output = this.decodeFileLikeItemText(itemObject);
760
+ if (!output)
761
+ continue;
762
+ messages.push({
763
+ id: itemId,
764
+ role: "assistant",
765
+ parts: [{
766
+ id: `${itemId}:tool`,
767
+ type: "tool",
768
+ name: type,
769
+ toolName: type,
770
+ output,
771
+ state: "completed",
772
+ sessionID: threadId,
773
+ messageID: itemId,
774
+ }],
775
+ time: timestamp,
776
+ });
777
+ }
778
+ }
779
+ }
780
+ return messages;
781
+ }
782
+ decodeItemText(itemObject) {
783
+ const content = this.readArray(itemObject.content);
784
+ const parts = [];
785
+ for (const entry of content) {
786
+ const obj = this.asRecord(entry);
787
+ const type = this.normalizedItemType(this.readString(obj.type) ?? "");
788
+ if (type === "text" || type === "inputtext" || type === "outputtext" || type === "message") {
789
+ const text = this.readString(obj.text) ?? this.readString(this.asRecord(obj.data).text);
790
+ if (text)
791
+ parts.push(text);
792
+ }
793
+ else if (type === "skill") {
794
+ const skill = this.readString(obj.id) ?? this.readString(obj.name);
795
+ if (skill)
796
+ parts.push(`$${skill}`);
797
+ }
798
+ }
799
+ const joined = parts.join("\n").trim();
800
+ return joined || this.readString(itemObject.text) || this.readString(itemObject.message) || "";
801
+ }
802
+ decodeReasoningItemText(itemObject) {
803
+ const summary = this.flattenTextValue(itemObject.summary).trim();
804
+ const content = this.flattenTextValue(itemObject.content).trim();
805
+ return [summary, content].filter(Boolean).join("\n\n") || "Thinking...";
806
+ }
807
+ decodePlanItemText(itemObject) {
808
+ return this.decodeItemText(itemObject) || this.flattenTextValue(itemObject.summary).trim() || "Planning...";
809
+ }
810
+ decodeCommandExecutionItemText(itemObject, type) {
811
+ if (type === "enteredreviewmode") {
812
+ return `Reviewing ${this.readString(itemObject.review) ?? "changes"}...`;
813
+ }
814
+ if (type === "contextcompaction") {
815
+ return "Context compacted";
816
+ }
817
+ const status = this.readString(this.asRecord(itemObject.status).type) ?? this.readString(itemObject.status) ?? "completed";
818
+ const command = this.firstString(itemObject, ["command", "cmd", "raw_command", "rawCommand", "input", "invocation"]) ?? "command";
819
+ return `${this.normalizedCommandPhase(status)} ${this.shortCommand(command)}`;
820
+ }
821
+ decodeFileLikeItemText(itemObject) {
822
+ const direct = this.firstString(itemObject, [
823
+ "diff",
824
+ "unified_diff",
825
+ "unifiedDiff",
826
+ "patch",
827
+ "text",
828
+ "message",
829
+ "summary",
830
+ "stdout",
831
+ "stderr",
832
+ "output_text",
833
+ "outputText",
834
+ ]);
835
+ if (direct?.trim())
836
+ return direct.trim();
837
+ const changes = this.readArray(itemObject.changes);
838
+ if (changes.length === 0)
839
+ return undefined;
840
+ return changes
841
+ .map((change) => {
842
+ const obj = this.asRecord(change);
843
+ const path = this.readString(obj.path) ?? this.readString(obj.filePath) ?? this.readString(obj.file_path) ?? "file";
844
+ const kind = this.readString(obj.kind) ?? "change";
845
+ const diff = this.readString(obj.diff) ?? this.readString(obj.patch) ?? "";
846
+ return diff ? `${kind}: ${path}\n${diff}` : `${kind}: ${path}`;
847
+ })
848
+ .join("\n\n");
849
+ }
850
+ normalizedCommandPhase(rawStatus) {
851
+ const normalized = rawStatus.trim().toLowerCase();
852
+ if (normalized.includes("fail") || normalized.includes("error"))
853
+ return "failed";
854
+ if (normalized.includes("cancel") || normalized.includes("abort") || normalized.includes("interrupt"))
855
+ return "stopped";
856
+ if (normalized.includes("complete") || normalized.includes("success") || normalized.includes("done"))
857
+ return "completed";
858
+ return "running";
859
+ }
860
+ shortCommand(rawCommand, maxLength = 92) {
861
+ const normalized = rawCommand.trim().replace(/\s+/g, " ");
862
+ if (!normalized)
863
+ return "command";
864
+ if (normalized.length <= maxLength)
865
+ return normalized;
866
+ return `${normalized.slice(0, maxLength - 1)}...`;
867
+ }
868
+ flattenTextValue(value) {
869
+ if (typeof value === "string")
870
+ return value;
871
+ if (Array.isArray(value)) {
872
+ return value.map((entry) => this.flattenTextValue(entry)).filter(Boolean).join("\n");
873
+ }
874
+ if (value && typeof value === "object") {
875
+ const obj = value;
876
+ return this.readString(obj.text) ?? this.readString(obj.message) ?? this.flattenTextValue(obj.content);
877
+ }
878
+ return "";
879
+ }
880
+ toSessionInfo(session) {
881
+ return {
882
+ id: session.id,
883
+ title: session.title,
884
+ time: {
885
+ created: session.createdAt,
886
+ updated: session.updatedAt,
887
+ },
888
+ };
889
+ }
890
+ extractThreadObject(payload) {
891
+ const obj = this.asRecord(payload);
892
+ const nested = this.asRecord(obj.thread);
893
+ return Object.keys(nested).length > 0 ? nested : obj;
894
+ }
895
+ extractThreadTitleFromUnknown(payload) {
896
+ return this.extractThreadTitle(this.extractThreadObject(payload));
897
+ }
898
+ extractThreadTitle(payload) {
899
+ const thread = this.asRecord(payload.thread);
900
+ return this.readString(thread.title)
901
+ ?? this.readString(thread.name)
902
+ ?? this.readString(payload.title)
903
+ ?? this.readString(payload.name);
904
+ }
905
+ extractTurnId(payload) {
906
+ return (this.readString(payload.turnId)
907
+ ?? this.readString(payload.turn_id)
908
+ ?? this.readString(this.asRecord(payload.turn).id)
909
+ ?? this.readString(this.asRecord(payload.turn).turnId)
910
+ ?? this.readString(this.asRecord(payload.event).id)
911
+ ?? this.readString(this.asRecord(this.asRecord(payload.event).turn).id)
912
+ ?? ((payload.msg != null || payload.event != null) ? this.readString(payload.id) : undefined));
913
+ }
914
+ extractItemId(payload) {
915
+ return (this.readString(payload.itemId)
916
+ ?? this.readString(payload.item_id)
917
+ ?? this.readString(payload.messageId)
918
+ ?? this.readString(payload.message_id)
919
+ ?? this.readString(this.asRecord(payload.item).id)
920
+ ?? this.readString(this.asRecord(payload.item).itemId)
921
+ ?? this.readString(this.asRecord(payload.item).messageId)
922
+ ?? this.readString(this.asRecord(payload.event).id)
923
+ ?? this.readString(this.asRecord(this.asRecord(payload.event).item).id));
924
+ }
925
+ extractTextPayload(payload) {
926
+ return (this.readString(payload.delta)
927
+ ?? this.readString(payload.text)
928
+ ?? this.readString(payload.message)
929
+ ?? this.readString(this.asRecord(payload.item).text)
930
+ ?? this.readString(this.asRecord(payload.item).delta)
931
+ ?? this.readString(this.asRecord(payload.item).message)
932
+ ?? this.readString(this.asRecord(payload.event).text)
933
+ ?? this.readString(this.asRecord(payload.event).delta)
934
+ ?? this.readString(this.asRecord(payload.event).message));
935
+ }
936
+ extractThreadId(payload) {
937
+ if (!payload || typeof payload !== "object")
938
+ return undefined;
939
+ const obj = payload;
940
+ const direct = this.readString(obj.threadId) ?? this.readString(obj.thread_id);
941
+ if (direct)
942
+ return direct;
943
+ const thread = this.asRecord(obj.thread);
944
+ return this.readString(thread.id) ?? this.readString(thread.threadId) ?? this.readString(thread.thread_id);
945
+ }
946
+ extractCreatedAt(payload) {
947
+ const obj = this.extractThreadObject(payload);
948
+ return this.readTimestamp(obj.createdAt) ?? this.readTimestamp(obj.created_at);
949
+ }
950
+ extractUpdatedAt(payload) {
951
+ const obj = this.extractThreadObject(payload);
952
+ return this.readTimestamp(obj.updatedAt)
953
+ ?? this.readTimestamp(obj.updated_at)
954
+ ?? this.readTimestamp(obj.time);
955
+ }
956
+ readTimestamp(value) {
957
+ if (typeof value === "number" && Number.isFinite(value)) {
958
+ return value > 10_000_000_000 ? value : value * 1000;
959
+ }
960
+ if (typeof value === "string" && value.trim()) {
961
+ const asNumber = Number(value);
962
+ if (Number.isFinite(asNumber)) {
963
+ return asNumber > 10_000_000_000 ? asNumber : asNumber * 1000;
964
+ }
965
+ const parsed = Date.parse(value);
966
+ return Number.isNaN(parsed) ? undefined : parsed;
967
+ }
968
+ if (value && typeof value === "object") {
969
+ const obj = value;
970
+ return this.readTimestamp(obj.updated) ?? this.readTimestamp(obj.created) ?? this.readTimestamp(obj.start) ?? this.readTimestamp(obj.end);
971
+ }
972
+ return undefined;
973
+ }
974
+ firstString(obj, keys) {
975
+ for (const key of keys) {
976
+ const value = this.readString(obj[key]);
977
+ if (value)
978
+ return value;
979
+ }
980
+ return undefined;
981
+ }
982
+ readArray(value) {
983
+ return Array.isArray(value) ? value : [];
984
+ }
985
+ readString(value) {
986
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
987
+ }
988
+ asRecord(value) {
989
+ return value && typeof value === "object" ? value : {};
990
+ }
991
+ normalizedItemType(rawType) {
992
+ return rawType.replace(/[_-]/g, "").toLowerCase();
993
+ }
994
+ isUserRole(itemObject) {
995
+ return (this.readString(itemObject.role) ?? "").toLowerCase().includes("user");
996
+ }
997
+ isAssistantMessageItem(itemType, role) {
998
+ const normalizedRole = (role ?? "").toLowerCase();
999
+ return itemType === "agentmessage"
1000
+ || itemType === "assistantmessage"
1001
+ || itemType === "exitedreviewmode"
1002
+ || (itemType === "message" && !normalizedRole.includes("user"));
1003
+ }
1004
+ extractIncomingItem(params) {
1005
+ const direct = this.asRecord(params.item);
1006
+ if (Object.keys(direct).length > 0)
1007
+ return direct;
1008
+ const eventItem = this.asRecord(this.asRecord(params.event).item);
1009
+ if (Object.keys(eventItem).length > 0)
1010
+ return eventItem;
1011
+ return this.asRecord(params.event);
1012
+ }
1013
+ describeToolPart(normalizedType, method, item) {
1014
+ if (normalizedType === "commandexecution") {
1015
+ return "command";
1016
+ }
1017
+ if (normalizedType === "filechange" || normalizedType === "diff") {
1018
+ return "file-change";
1019
+ }
1020
+ if (normalizedType === "plan") {
1021
+ return "plan";
1022
+ }
1023
+ if (normalizedType === "enteredreviewmode") {
1024
+ return "review";
1025
+ }
1026
+ if (normalizedType === "contextcompaction") {
1027
+ return "context";
1028
+ }
1029
+ return this.readString(item.name) ?? method.replace(/^.*\//, "");
1030
+ }
1031
+ extractToolInput(item, params) {
1032
+ return item.input ?? item.command ?? item.path ?? item.args ?? params.command ?? params.path ?? undefined;
1033
+ }
1034
+ describeCompletedItemOutput(item, normalizedType) {
1035
+ if (normalizedType === "commandexecution") {
1036
+ return this.firstString(item, ["stdout", "stderr", "text", "message", "summary"]);
1037
+ }
1038
+ if (normalizedType === "plan") {
1039
+ return this.decodePlanItemText(item);
1040
+ }
1041
+ if (normalizedType === "filechange" || normalizedType === "toolcall" || normalizedType === "diff") {
1042
+ return this.decodeFileLikeItemText(item);
1043
+ }
1044
+ if (normalizedType === "enteredreviewmode") {
1045
+ return `Reviewing ${this.readString(item.review) ?? "changes"}...`;
1046
+ }
1047
+ if (normalizedType === "contextcompaction") {
1048
+ return "Context compacted";
263
1049
  }
264
- // JSON-RPC notification (no id async event from Codex)
265
- const notif = msg;
266
- const params = (notif.params ?? {});
267
- const mappedType = NOTIFICATION_TYPE_MAP[notif.method] ?? notif.method;
268
- console.log("[codex]", notif.method);
269
- this.emitter?.({ type: mappedType, properties: params });
1050
+ return this.firstString(item, ["text", "message", "summary", "output", "output_text", "outputText"]);
270
1051
  }
271
1052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",