lunel-cli 0.1.47 → 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,7 +6,9 @@ export declare class CodexProvider implements AIProvider {
6
6
  private nextId;
7
7
  private pending;
8
8
  private sessions;
9
- private sessionIdByThreadId;
9
+ private pendingPermissionRequestIds;
10
+ private assistantMessageIdByTurnId;
11
+ private partTextById;
10
12
  init(): Promise<void>;
11
13
  destroy(): Promise<void>;
12
14
  subscribe(emitter: AiEventEmitter): () => void;
@@ -44,8 +46,54 @@ export declare class CodexProvider implements AIProvider {
44
46
  private send;
45
47
  private call;
46
48
  private handleLine;
47
- private ensureThread;
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;
48
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;
49
84
  private extractThreadId;
85
+ private extractCreatedAt;
86
+ private extractUpdatedAt;
87
+ private readTimestamp;
88
+ private firstString;
89
+ private readArray;
50
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;
51
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,10 +12,9 @@ export class CodexProvider {
28
12
  nextId = 1;
29
13
  pending = new Map();
30
14
  sessions = new Map();
31
- sessionIdByThreadId = new Map();
32
- // -------------------------------------------------------------------------
33
- // Lifecycle
34
- // -------------------------------------------------------------------------
15
+ pendingPermissionRequestIds = new Map();
16
+ assistantMessageIdByTurnId = new Map();
17
+ partTextById = new Map();
35
18
  async init() {
36
19
  console.log("Starting Codex app-server...");
37
20
  this.proc = spawn("codex", ["app-server"], {
@@ -51,7 +34,6 @@ export class CodexProvider {
51
34
  this.emitter?.({ type: "sse_dead", properties: { error: msg } });
52
35
  }
53
36
  });
54
- // Handshake: initialize the JSON-RPC session.
55
37
  await this.call("initialize", { clientInfo: { name: "lunel", version: "1.0" } });
56
38
  console.log("Codex ready.\n");
57
39
  }
@@ -67,77 +49,76 @@ export class CodexProvider {
67
49
  this.emitter = null;
68
50
  };
69
51
  }
70
- // Codex doesn't need session reconnect validation, so setActiveSession is omitted.
71
- // -------------------------------------------------------------------------
72
- // Session management (emulated locally — Codex has no persistent store)
73
- // -------------------------------------------------------------------------
74
52
  async createSession(title) {
75
- const id = crypto.randomUUID();
76
- const session = { id, title, messages: [], createdAt: Date.now() };
77
- this.sessions.set(id, session);
78
- // Start a new Codex thread for this session.
79
- try {
80
- await this.ensureThread(session);
81
- }
82
- catch (err) {
83
- 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");
84
57
  }
85
- 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) };
86
66
  }
87
67
  async listSessions() {
88
- return {
89
- sessions: Array.from(this.sessions.values()).map((s) => ({
90
- id: s.id,
91
- title: s.title ?? "",
92
- created: s.createdAt,
93
- })),
94
- };
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 };
95
76
  }
96
77
  async getSession(id) {
97
- const s = this.sessions.get(id);
98
- 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) {
99
85
  throw Object.assign(new Error(`Session ${id} not found`), { code: "ENOENT" });
100
- return { session: { id: s.id, title: s.title ?? "", created: s.createdAt } };
86
+ }
87
+ return { session: this.toSessionInfo(next) };
101
88
  }
102
89
  async deleteSession(id) {
103
- const session = this.sessions.get(id);
104
- if (session?.threadId) {
105
- this.sessionIdByThreadId.delete(session.threadId);
106
- }
107
90
  this.sessions.delete(id);
108
91
  return {};
109
92
  }
110
- // -------------------------------------------------------------------------
111
- // Messages
112
- // -------------------------------------------------------------------------
113
93
  async getMessages(sessionId) {
114
- const s = this.sessions.get(sessionId);
115
- 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 };
116
113
  }
117
- // -------------------------------------------------------------------------
118
- // Interaction
119
- // -------------------------------------------------------------------------
120
114
  async prompt(sessionId, text, model, agent) {
121
- const session = this.sessions.get(sessionId);
122
- // Append the user message to local history immediately.
123
- if (session) {
124
- session.messages.push({
125
- id: crypto.randomUUID(),
126
- role: "user",
127
- parts: [{ type: "text", text }],
128
- time: Date.now(),
129
- });
130
- }
131
- // Fire-and-forget — the response streams back as JSON-RPC notifications.
115
+ const session = this.ensureLocalSession(sessionId);
116
+ session.updatedAt = Date.now();
132
117
  (async () => {
133
118
  try {
134
- if (!session) {
135
- throw new Error(`Session ${sessionId} not found`);
136
- }
137
- const threadId = await this.ensureThread(session);
138
119
  await this.call("turn/start", {
139
- threadId,
140
- input: [{ type: "message", role: "user", content: [{ type: "input_text", text }] }],
120
+ threadId: session.id,
121
+ input: [{ type: "text", text }],
141
122
  ...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
142
123
  ...(agent ? { agent } : {}),
143
124
  });
@@ -151,100 +132,54 @@ export class CodexProvider {
151
132
  return { ack: true };
152
133
  }
153
134
  async abort(sessionId) {
154
- const session = this.sessions.get(sessionId);
155
- if (!session?.threadId) {
156
- throw new Error(`Session ${sessionId} has no active Codex thread`);
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`);
157
139
  }
158
- await this.call("turn/abort", { threadId: session.threadId });
140
+ session.activeTurnId = turnId;
141
+ await this.call("turn/interrupt", { threadId: sessionId, turnId });
159
142
  return {};
160
143
  }
161
- // -------------------------------------------------------------------------
162
- // Metadata
163
- // -------------------------------------------------------------------------
164
144
  async agents() {
165
- try {
166
- const result = await this.call("agent/list", {});
167
- return { agents: result ?? [] };
168
- }
169
- catch {
170
- // Codex may not support agents — return empty list gracefully.
171
- return { agents: [] };
172
- }
145
+ return { agents: [] };
173
146
  }
174
147
  async providers() {
175
- try {
176
- const result = await this.call("provider/list", {});
177
- return {
178
- providers: result?.providers ?? [],
179
- default: result?.default ?? {},
180
- };
181
- }
182
- catch {
183
- return { providers: [], default: {} };
184
- }
148
+ return { providers: [], default: {} };
185
149
  }
186
- // -------------------------------------------------------------------------
187
- // Auth
188
- // -------------------------------------------------------------------------
189
150
  async setAuth(providerId, key) {
190
- await this.call("auth/set", { providerId, key });
191
- return {};
151
+ throw new Error("Codex auth configuration is not supported by Lunel yet");
192
152
  }
193
- // -------------------------------------------------------------------------
194
- // Session operations
195
- // -------------------------------------------------------------------------
196
153
  async command(sessionId, command, args) {
197
- const session = this.sessions.get(sessionId);
198
- if (!session?.threadId) {
199
- throw new Error(`Session ${sessionId} has no active Codex thread`);
200
- }
201
- const result = await this.call("session/command", {
202
- threadId: session.threadId,
203
- command,
204
- arguments: args,
205
- });
206
- return { result: result ?? null };
154
+ throw new Error("Codex command execution is not supported by Lunel yet");
207
155
  }
208
156
  async revert(sessionId, messageId) {
209
- const session = this.sessions.get(sessionId);
210
- if (!session?.threadId) {
211
- throw new Error(`Session ${sessionId} has no active Codex thread`);
212
- }
213
- await this.call("session/revert", {
214
- threadId: session.threadId,
215
- messageId,
216
- });
217
- return {};
157
+ throw new Error("Codex revert is not supported by Lunel yet");
218
158
  }
219
159
  async unrevert(sessionId) {
220
- const session = this.sessions.get(sessionId);
221
- if (!session?.threadId) {
222
- throw new Error(`Session ${sessionId} has no active Codex thread`);
223
- }
224
- await this.call("session/unrevert", {
225
- threadId: session.threadId,
226
- });
227
- return {};
160
+ throw new Error("Codex unrevert is not supported by Lunel yet");
228
161
  }
229
162
  async share(sessionId) {
230
- // Codex has no share concept — return a null stub so the app degrades gracefully.
231
163
  return { share: { url: null } };
232
164
  }
233
165
  async permissionReply(sessionId, permissionId, response) {
234
- const session = this.sessions.get(sessionId);
235
- if (!session?.threadId) {
236
- throw new Error(`Session ${sessionId} has no active Codex thread`);
237
- }
238
- await this.call("session/permission", {
239
- threadId: session.threadId,
240
- permissionId,
241
- 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 },
242
180
  });
243
181
  return {};
244
182
  }
245
- // -------------------------------------------------------------------------
246
- // JSON-RPC internals
247
- // -------------------------------------------------------------------------
248
183
  send(req) {
249
184
  if (!this.proc?.stdin?.writable)
250
185
  return;
@@ -253,12 +188,12 @@ export class CodexProvider {
253
188
  call(method, params) {
254
189
  return new Promise((resolve, reject) => {
255
190
  const id = this.nextId++;
256
- this.pending.set(id, { resolve, reject });
191
+ const key = String(id);
192
+ this.pending.set(key, { resolve, reject });
257
193
  this.send({ jsonrpc: "2.0", id, method, params });
258
- // Safety timeout: if Codex never responds, don't hang the caller forever.
259
194
  setTimeout(() => {
260
- if (this.pending.has(id)) {
261
- this.pending.delete(id);
195
+ if (this.pending.has(key)) {
196
+ this.pending.delete(key);
262
197
  reject(new Error(`Codex RPC timeout: ${method} (id=${id})`));
263
198
  }
264
199
  }, 30_000);
@@ -270,83 +205,848 @@ export class CodexProvider {
270
205
  msg = JSON.parse(line);
271
206
  }
272
207
  catch {
273
- return; // ignore non-JSON lines (e.g. startup banners)
208
+ return;
209
+ }
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;
274
220
  }
275
- // JSON-RPC response (has numeric id + result or error)
276
- if ("id" in msg && typeof msg.id === "number") {
221
+ if ("id" in msg) {
277
222
  const resp = msg;
278
- const pending = this.pending.get(resp.id);
279
- if (pending) {
280
- this.pending.delete(resp.id);
281
- if (resp.error) {
282
- 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
+ });
309
+ }
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
+ }
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;
283
373
  }
284
- else {
285
- pending.resolve(resp.result ?? null);
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 } });
286
381
  }
382
+ return;
287
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)
288
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
+ });
289
403
  }
290
- // JSON-RPC notification (no id — async event from Codex)
291
- const notif = msg;
292
- const params = (notif.params ?? {});
293
- const mappedType = NOTIFICATION_TYPE_MAP[notif.method] ?? notif.method;
294
- this.ingestThreadMetadata(params);
295
- console.log("[codex]", notif.method);
296
- this.emitter?.({ type: mappedType, properties: params });
297
- }
298
- async ensureThread(session) {
299
- if (session.threadId)
300
- return session.threadId;
301
- const result = await this.call("thread/start", {
302
- ...(session.title ? { title: session.title } : {}),
404
+ this.emitter?.({
405
+ type: "message.updated",
406
+ properties: { info: { sessionID: session.id, id: messageId, role: "user" } },
303
407
  });
304
- const threadId = this.extractThreadId(result);
305
- if (!threadId) {
306
- throw new Error("thread/start response missing threadId");
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;
307
575
  }
308
- session.threadId = threadId;
309
- this.sessionIdByThreadId.set(threadId, session.id);
310
- return threadId;
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);
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;
311
621
  }
312
622
  ingestThreadMetadata(payload) {
313
623
  const threadId = this.extractThreadId(payload);
314
624
  if (!threadId)
315
625
  return;
316
- const sessionId = this.sessionIdByThreadId.get(threadId);
317
- if (sessionId) {
318
- const session = this.sessions.get(sessionId);
319
- if (session) {
320
- session.threadId = threadId;
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);
321
675
  }
322
- return;
323
676
  }
324
- for (const session of this.sessions.values()) {
325
- if (!session.threadId) {
326
- session.threadId = threadId;
327
- this.sessionIdByThreadId.set(threadId, session.id);
328
- return;
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}`);
329
797
  }
330
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));
331
935
  }
332
936
  extractThreadId(payload) {
333
937
  if (!payload || typeof payload !== "object")
334
938
  return undefined;
335
939
  const obj = payload;
336
- const direct = this.readString(obj.threadId) ??
337
- this.readString(obj.thread_id);
940
+ const direct = this.readString(obj.threadId) ?? this.readString(obj.thread_id);
338
941
  if (direct)
339
942
  return direct;
340
- const thread = obj.thread;
341
- if (thread && typeof thread === "object") {
342
- const threadObj = thread;
343
- return (this.readString(threadObj.id) ??
344
- this.readString(threadObj.threadId) ??
345
- this.readString(threadObj.thread_id));
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);
346
971
  }
347
972
  return undefined;
348
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
+ }
349
985
  readString(value) {
350
986
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
351
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";
1049
+ }
1050
+ return this.firstString(item, ["text", "message", "summary", "output", "output_text", "outputText"]);
1051
+ }
352
1052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",