lunel-cli 0.1.113 → 0.1.115

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.
@@ -1,4 +1,4 @@
1
- import type { AiEvent, ModelSelector, FileAttachment } from "./interface.js";
1
+ import type { AiEvent, ModelSelector, FileAttachment, CodexPromptOptions } from "./interface.js";
2
2
  export type AiBackend = "opencode" | "codex";
3
3
  export declare class AiManager {
4
4
  private _providers;
@@ -23,10 +23,13 @@ export declare class AiManager {
23
23
  deleteSession(backend: AiBackend, id: string): Promise<{
24
24
  deleted: boolean;
25
25
  }>;
26
+ renameSession(backend: AiBackend, id: string, title: string): Promise<{
27
+ session: import("./interface.js").SessionInfo;
28
+ }>;
26
29
  getMessages(backend: AiBackend, sessionId: string): Promise<{
27
30
  messages: import("./interface.js").MessageInfo[];
28
31
  }>;
29
- prompt(backend: AiBackend, sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[]): Promise<{
32
+ prompt(backend: AiBackend, sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
30
33
  ack: true;
31
34
  }>;
32
35
  abort(backend: AiBackend, sessionId: string): Promise<Record<string, never>>;
package/dist/ai/index.js CHANGED
@@ -72,10 +72,11 @@ export class AiManager {
72
72
  createSession(backend, title) { return this.get(backend).createSession(title); }
73
73
  getSession(backend, id) { return this.get(backend).getSession(id); }
74
74
  deleteSession(backend, id) { return this.get(backend).deleteSession(id); }
75
+ renameSession(backend, id, title) { return this.get(backend).renameSession(id, title); }
75
76
  getMessages(backend, sessionId) { return this.get(backend).getMessages(sessionId); }
76
- prompt(backend, sessionId, text, model, agent, files) {
77
+ prompt(backend, sessionId, text, model, agent, files, codexOptions) {
77
78
  this.get(backend).setActiveSession?.(sessionId);
78
- return this.get(backend).prompt(sessionId, text, model, agent, files);
79
+ return this.get(backend).prompt(sessionId, text, model, agent, files, codexOptions);
79
80
  }
80
81
  abort(backend, sessionId) { return this.get(backend).abort(sessionId); }
81
82
  // Metadata — backend is optional, falls back to first available
@@ -7,6 +7,11 @@ export interface ModelSelector {
7
7
  providerID: string;
8
8
  modelID: string;
9
9
  }
10
+ export interface CodexPromptOptions {
11
+ reasoningEffort?: "low" | "medium" | "high";
12
+ speed?: "fast" | "balanced" | "quality";
13
+ permissionMode?: "default" | "full-access";
14
+ }
10
15
  export interface FileAttachment {
11
16
  type: "file";
12
17
  mime: string;
@@ -56,10 +61,13 @@ export interface AIProvider {
56
61
  deleteSession(id: string): Promise<{
57
62
  deleted: boolean;
58
63
  }>;
64
+ renameSession(id: string, title: string): Promise<{
65
+ session: SessionInfo;
66
+ }>;
59
67
  getMessages(sessionId: string): Promise<{
60
68
  messages: MessageInfo[];
61
69
  }>;
62
- prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[]): Promise<{
70
+ prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
63
71
  ack: true;
64
72
  }>;
65
73
  abort(sessionId: string): Promise<Record<string, never>>;
@@ -1,4 +1,4 @@
1
- import type { AIProvider, AiEventEmitter, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
1
+ import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
2
2
  export declare class OpenCodeProvider implements AIProvider {
3
3
  private client;
4
4
  private server;
@@ -24,10 +24,13 @@ export declare class OpenCodeProvider implements AIProvider {
24
24
  deleteSession(id: string): Promise<{
25
25
  deleted: boolean;
26
26
  }>;
27
+ renameSession(id: string, title: string): Promise<{
28
+ session: SessionInfo;
29
+ }>;
27
30
  getMessages(sessionId: string): Promise<{
28
31
  messages: MessageInfo[];
29
32
  }>;
30
- prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[]): Promise<{
33
+ prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
31
34
  ack: true;
32
35
  }>;
33
36
  abort(sessionId: string): Promise<Record<string, never>>;
@@ -50,12 +53,13 @@ export declare class OpenCodeProvider implements AIProvider {
50
53
  private runSseLoop;
51
54
  private sendPromptAsync;
52
55
  private reconcileOpenCodeState;
56
+ private refreshBusySessionMessages;
53
57
  private refreshSessionsMetadata;
54
58
  private refreshPendingPermissions;
55
59
  private refreshPendingQuestions;
56
60
  private fetchOpenCodeJson;
57
61
  private refreshSessionStatuses;
58
62
  private trackPermissionEvent;
59
- private asRecord;
60
63
  private readString;
64
+ private asRecord;
61
65
  }
@@ -23,6 +23,236 @@ function requireData(response, label) {
23
23
  }
24
24
  return response.data;
25
25
  }
26
+ function asRecord(value) {
27
+ return value && typeof value === "object" ? value : {};
28
+ }
29
+ function readString(value) {
30
+ return typeof value === "string" && value.length > 0 ? value : undefined;
31
+ }
32
+ function normalizeToolOutput(output, metadata) {
33
+ const attachments = Array.isArray(metadata.attachments) ? metadata.attachments : [];
34
+ if (attachments.length === 0)
35
+ return output;
36
+ const attachmentLines = attachments
37
+ .map((entry) => {
38
+ const file = asRecord(entry);
39
+ const filename = readString(file.filename)
40
+ ?? readString(file.path)
41
+ ?? readString(file.url)
42
+ ?? "attachment";
43
+ return `- ${filename}`;
44
+ })
45
+ .filter((line) => line.trim().length > 0);
46
+ if (attachmentLines.length === 0)
47
+ return output;
48
+ if (!output.trim()) {
49
+ return `Attachments:\n${attachmentLines.join("\n")}`;
50
+ }
51
+ return `${output}\n\nAttachments:\n${attachmentLines.join("\n")}`;
52
+ }
53
+ function buildPatchSummary(part) {
54
+ const hash = readString(part.hash);
55
+ const files = Array.isArray(part.files)
56
+ ? part.files.map((value) => String(value)).filter((value) => value.trim().length > 0)
57
+ : [];
58
+ const lines = [];
59
+ if (hash)
60
+ lines.push(`Patch hash: ${hash}`);
61
+ if (files.length > 0) {
62
+ lines.push("Files:");
63
+ for (const file of files)
64
+ lines.push(`- ${file}`);
65
+ }
66
+ return lines.join("\n");
67
+ }
68
+ function normalizeOpenCodePart(part) {
69
+ const raw = asRecord(part);
70
+ const type = readString(raw.type);
71
+ if (!type)
72
+ return raw;
73
+ if (type === "tool") {
74
+ const tool = readString(raw.tool) ?? "tool";
75
+ const state = asRecord(raw.state);
76
+ const status = readString(state.status) ?? "running";
77
+ const metadata = asRecord(state.metadata ?? raw.metadata);
78
+ const normalized = {
79
+ ...raw,
80
+ type: "tool",
81
+ toolName: tool,
82
+ name: tool,
83
+ state: status,
84
+ input: asRecord(state.input),
85
+ metadata,
86
+ };
87
+ const title = readString(state.title);
88
+ if (title)
89
+ normalized.title = title;
90
+ const rawText = readString(state.raw);
91
+ if (rawText)
92
+ normalized.raw = rawText;
93
+ const time = asRecord(state.time);
94
+ if (Object.keys(time).length > 0) {
95
+ normalized.time = time;
96
+ }
97
+ if (status === "completed") {
98
+ const output = typeof state.output === "string" ? state.output : "";
99
+ normalized.output = normalizeToolOutput(output, {
100
+ ...metadata,
101
+ attachments: state.attachments,
102
+ });
103
+ }
104
+ else if (status === "error") {
105
+ normalized.error = readString(state.error) ?? "Tool failed";
106
+ const errorMessage = readString(state.error);
107
+ if (errorMessage)
108
+ normalized.output = errorMessage;
109
+ }
110
+ const attachments = Array.isArray(state.attachments) ? state.attachments : [];
111
+ if (attachments.length > 0) {
112
+ normalized.attachments = attachments.map((entry) => normalizeOpenCodePart(entry));
113
+ }
114
+ return normalized;
115
+ }
116
+ if (type === "step-start") {
117
+ const snapshot = readString(raw.snapshot);
118
+ return {
119
+ ...raw,
120
+ type: "step-start",
121
+ title: snapshot ? `Step started · ${snapshot}` : "Step started",
122
+ };
123
+ }
124
+ if (type === "step-finish") {
125
+ const reason = readString(raw.reason);
126
+ return {
127
+ ...raw,
128
+ type: "step-finish",
129
+ title: reason ? `Step finished · ${reason}` : "Step finished",
130
+ };
131
+ }
132
+ if (type === "patch") {
133
+ return {
134
+ ...raw,
135
+ type: "file-change",
136
+ title: "File changes",
137
+ output: buildPatchSummary(raw),
138
+ };
139
+ }
140
+ if (type === "subtask") {
141
+ return {
142
+ ...raw,
143
+ type: "tool",
144
+ toolName: "subtask",
145
+ name: "subtask",
146
+ state: "completed",
147
+ input: {
148
+ prompt: readString(raw.prompt) ?? "",
149
+ description: readString(raw.description) ?? "",
150
+ agent: readString(raw.agent) ?? "",
151
+ ...(readString(raw.command) ? { command: readString(raw.command) } : {}),
152
+ },
153
+ output: readString(raw.description) ?? readString(raw.prompt) ?? "Subtask requested",
154
+ };
155
+ }
156
+ if (type === "agent") {
157
+ const name = readString(raw.name) ?? "Agent";
158
+ return {
159
+ ...raw,
160
+ type: "step-start",
161
+ title: `Agent · ${name}`,
162
+ };
163
+ }
164
+ if (type === "retry") {
165
+ const attempt = raw.attempt;
166
+ const error = asRecord(raw.error);
167
+ const message = readString(error.message) ?? "Retry requested";
168
+ return {
169
+ ...raw,
170
+ type: "tool",
171
+ toolName: "retry",
172
+ name: "retry",
173
+ state: "error",
174
+ input: {
175
+ attempt,
176
+ },
177
+ error: message,
178
+ output: message,
179
+ };
180
+ }
181
+ if (type === "compaction") {
182
+ const auto = raw.auto === true;
183
+ const overflow = raw.overflow === true;
184
+ return {
185
+ ...raw,
186
+ type: "step-start",
187
+ title: `Context compacted${auto ? " · auto" : ""}${overflow ? " · overflow" : ""}`,
188
+ };
189
+ }
190
+ if (type === "snapshot") {
191
+ return {
192
+ ...raw,
193
+ type: "step-start",
194
+ title: "Workspace snapshot",
195
+ };
196
+ }
197
+ return raw;
198
+ }
199
+ function normalizeOpenCodeMessage(message) {
200
+ return {
201
+ id: message.info.id,
202
+ role: message.info.role,
203
+ parts: (message.parts || []).map((part) => normalizeOpenCodePart(part)),
204
+ time: message.info.time,
205
+ };
206
+ }
207
+ function normalizePermissionProperties(properties) {
208
+ const tool = asRecord(properties.tool);
209
+ const metadata = properties.metadata && typeof properties.metadata === "object"
210
+ ? properties.metadata
211
+ : properties;
212
+ return {
213
+ id: readString(properties.id),
214
+ sessionID: readString(properties.sessionID) ?? readString(properties.sessionId),
215
+ messageID: readString(properties.messageID) ?? readString(tool.messageID),
216
+ callID: readString(properties.callID) ?? readString(tool.callID),
217
+ type: readString(properties.type) ?? readString(properties.permission) ?? "permission",
218
+ title: readString(properties.title)
219
+ ?? readString(properties.permission)
220
+ ?? "Permission requested",
221
+ metadata,
222
+ };
223
+ }
224
+ function normalizeOpenCodeEvent(event) {
225
+ const { type, properties } = event;
226
+ if (type === "message.part.updated") {
227
+ return {
228
+ type,
229
+ properties: {
230
+ ...properties,
231
+ part: normalizeOpenCodePart(properties.part),
232
+ },
233
+ };
234
+ }
235
+ if (type === "permission.updated" || type === "permission.asked") {
236
+ return {
237
+ type: "permission.updated",
238
+ properties: normalizePermissionProperties(properties),
239
+ };
240
+ }
241
+ if (type === "permission.replied") {
242
+ return {
243
+ type: "permission.replied",
244
+ properties: {
245
+ sessionID: readString(properties.sessionID) ?? readString(properties.sessionId),
246
+ permissionId: readString(properties.permissionID)
247
+ ?? readString(properties.requestID)
248
+ ?? readString(properties.permissionId)
249
+ ?? readString(properties.id),
250
+ response: readString(properties.response) ?? readString(properties.reply),
251
+ },
252
+ };
253
+ }
254
+ return event;
255
+ }
26
256
  export class OpenCodeProvider {
27
257
  client = null;
28
258
  server = null;
@@ -111,7 +341,23 @@ export class OpenCodeProvider {
111
341
  }
112
342
  async deleteSession(id) {
113
343
  const response = await this.client.session.delete({ path: { id } });
114
- return { deleted: Boolean(requireData(response, "session.delete")) };
344
+ const raw = response;
345
+ if (raw.error) {
346
+ const errMsg = typeof raw.error === "string"
347
+ ? raw.error
348
+ : JSON.stringify(raw.error);
349
+ throw new Error(errMsg);
350
+ }
351
+ // Treat any non-error delete response as success. Some SDK/runtime combos
352
+ // return inconsistent boolean payloads despite successful deletion.
353
+ return { deleted: true };
354
+ }
355
+ async renameSession(id, title) {
356
+ const response = await this.client.session.update({
357
+ path: { id },
358
+ body: { title },
359
+ });
360
+ return { session: requireData(response, "session.update") };
115
361
  }
116
362
  // -------------------------------------------------------------------------
117
363
  // Messages
@@ -122,12 +368,7 @@ export class OpenCodeProvider {
122
368
  try {
123
369
  const response = await this.client.session.messages({ path: { id: sessionId } });
124
370
  const raw = requireData(response, "session.messages");
125
- const messages = raw.map((m) => ({
126
- id: m.info.id,
127
- role: m.info.role,
128
- parts: m.parts || [],
129
- time: m.info.time,
130
- }));
371
+ const messages = raw.map((m) => normalizeOpenCodeMessage(m));
131
372
  if (VERBOSE_AI_LOGS)
132
373
  console.log("[ai] getMessages returned", messages.length, "messages");
133
374
  return { messages };
@@ -140,7 +381,8 @@ export class OpenCodeProvider {
140
381
  // -------------------------------------------------------------------------
141
382
  // Interaction
142
383
  // -------------------------------------------------------------------------
143
- async prompt(sessionId, text, model, agent, files = []) {
384
+ async prompt(sessionId, text, model, agent, files = [], codexOptions) {
385
+ void codexOptions;
144
386
  if (sessionId)
145
387
  this.lastActiveSessionId = sessionId;
146
388
  if (VERBOSE_AI_LOGS) {
@@ -313,10 +555,15 @@ export class OpenCodeProvider {
313
555
  if (base.type !== "server.heartbeat") {
314
556
  console.log("[sse]", base.type);
315
557
  }
316
- this.trackPermissionEvent(base.type, base.properties || {});
317
- this.emitter?.({ type: base.type, properties: base.properties || {} });
558
+ const normalizedEvent = normalizeOpenCodeEvent({
559
+ type: base.type,
560
+ properties: base.properties || {},
561
+ });
562
+ this.trackPermissionEvent(normalizedEvent.type, normalizedEvent.properties || {});
563
+ this.emitter?.(normalizedEvent);
318
564
  }
319
565
  console.log("[sse] Event stream ended, reconnecting...");
566
+ attempt++;
320
567
  }
321
568
  catch (err) {
322
569
  if (this.shuttingDown)
@@ -378,6 +625,55 @@ export class OpenCodeProvider {
378
625
  this.refreshPendingQuestions(),
379
626
  this.refreshSessionStatuses(),
380
627
  ]);
628
+ await this.refreshBusySessionMessages();
629
+ }
630
+ async refreshBusySessionMessages() {
631
+ const server = this.server;
632
+ const authHeader = this.authHeader;
633
+ if (!server || !authHeader)
634
+ return;
635
+ const statusUrl = new URL("/session/status", server.url);
636
+ const statusResp = await fetch(statusUrl, {
637
+ headers: { Authorization: authHeader, accept: "application/json" },
638
+ }).catch(() => null);
639
+ if (!statusResp?.ok)
640
+ return;
641
+ const payload = await statusResp.json().catch(() => null);
642
+ if (!payload || typeof payload !== "object")
643
+ return;
644
+ for (const [sessionId, status] of Object.entries(payload)) {
645
+ const statusObj = status;
646
+ const statusType = typeof statusObj?.type === "string" ? statusObj.type.toLowerCase() : "";
647
+ if (statusType !== "busy")
648
+ continue;
649
+ try {
650
+ const response = await this.client.session.messages({ path: { id: sessionId } });
651
+ const raw = Array.isArray(response.data) ? response.data : [];
652
+ for (const m of raw) {
653
+ const msgObj = this.asRecord(m);
654
+ const info = this.asRecord(msgObj.info);
655
+ const parts = Array.isArray(msgObj.parts) ? msgObj.parts : [];
656
+ const msgId = this.readString(info.id);
657
+ if (!msgId)
658
+ continue;
659
+ this.emitter?.({ type: "message.updated", properties: { info } });
660
+ for (const part of parts) {
661
+ const partObj = normalizeOpenCodePart(part);
662
+ this.emitter?.({
663
+ type: "message.part.updated",
664
+ properties: {
665
+ part: { ...partObj, sessionID: sessionId, messageID: msgId },
666
+ message: { sessionID: sessionId, id: msgId, role: info.role },
667
+ },
668
+ });
669
+ }
670
+ }
671
+ console.log(`[sse] Re-synced messages for busy session ${sessionId} after reconnect`);
672
+ }
673
+ catch (err) {
674
+ console.warn(`[sse] Failed to refresh messages for busy session ${sessionId}:`, err.message);
675
+ }
676
+ }
381
677
  }
382
678
  async refreshSessionsMetadata() {
383
679
  const response = await this.client.session.list();
@@ -413,19 +709,7 @@ export class OpenCodeProvider {
413
709
  this.knownPendingPermissionIds.add(id);
414
710
  this.emitter?.({
415
711
  type: "permission.updated",
416
- properties: {
417
- id,
418
- sessionID: this.readString(permission.sessionID) ?? this.readString(permission.sessionId),
419
- messageID: this.readString(this.asRecord(permission.tool).messageID),
420
- callID: this.readString(this.asRecord(permission.tool).callID),
421
- type: this.readString(permission.permission) ?? "permission",
422
- title: this.readString(permission.title)
423
- ?? this.readString(permission.permission)
424
- ?? "Permission requested",
425
- metadata: permission.metadata && typeof permission.metadata === "object"
426
- ? permission.metadata
427
- : permission,
428
- },
712
+ properties: normalizePermissionProperties(permission),
429
713
  });
430
714
  }
431
715
  for (const id of Array.from(this.knownPendingPermissionIds)) {
@@ -529,40 +813,40 @@ export class OpenCodeProvider {
529
813
  }
530
814
  trackPermissionEvent(type, properties) {
531
815
  if (type === "permission.updated") {
532
- const id = this.readString(properties.id);
816
+ const id = readString(properties.id);
533
817
  if (id) {
534
818
  this.knownPendingPermissionIds.add(id);
535
819
  }
536
820
  return;
537
821
  }
538
822
  if (type === "permission.replied") {
539
- const id = this.readString(properties.permissionId)
540
- ?? this.readString(properties.requestID)
541
- ?? this.readString(properties.id);
823
+ const id = readString(properties.permissionId)
824
+ ?? readString(properties.requestID)
825
+ ?? readString(properties.id);
542
826
  if (id) {
543
827
  this.knownPendingPermissionIds.delete(id);
544
828
  }
545
829
  }
546
830
  if (type === "question.asked") {
547
- const id = this.readString(properties.id);
831
+ const id = readString(properties.id);
548
832
  if (id) {
549
833
  this.knownPendingQuestionIds.add(id);
550
834
  }
551
835
  return;
552
836
  }
553
837
  if (type === "question.replied" || type === "question.rejected") {
554
- const id = this.readString(properties.requestID)
555
- ?? this.readString(properties.questionId)
556
- ?? this.readString(properties.id);
838
+ const id = readString(properties.requestID)
839
+ ?? readString(properties.questionId)
840
+ ?? readString(properties.id);
557
841
  if (id) {
558
842
  this.knownPendingQuestionIds.delete(id);
559
843
  }
560
844
  }
561
845
  }
562
- asRecord(value) {
563
- return value && typeof value === "object" ? value : {};
564
- }
565
846
  readString(value) {
566
- return typeof value === "string" && value.length > 0 ? value : undefined;
847
+ return readString(value);
848
+ }
849
+ asRecord(value) {
850
+ return asRecord(value);
567
851
  }
568
852
  }