lunel-cli 0.1.114 → 0.1.116

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.
@@ -23,6 +23,9 @@ 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
  }>;
package/dist/ai/index.js CHANGED
@@ -72,6 +72,7 @@ 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
77
  prompt(backend, sessionId, text, model, agent, files, codexOptions) {
77
78
  this.get(backend).setActiveSession?.(sessionId);
@@ -10,6 +10,7 @@ export interface ModelSelector {
10
10
  export interface CodexPromptOptions {
11
11
  reasoningEffort?: "low" | "medium" | "high";
12
12
  speed?: "fast" | "balanced" | "quality";
13
+ permissionMode?: "default" | "full-access";
13
14
  }
14
15
  export interface FileAttachment {
15
16
  type: "file";
@@ -60,6 +61,9 @@ export interface AIProvider {
60
61
  deleteSession(id: string): Promise<{
61
62
  deleted: boolean;
62
63
  }>;
64
+ renameSession(id: string, title: string): Promise<{
65
+ session: SessionInfo;
66
+ }>;
63
67
  getMessages(sessionId: string): Promise<{
64
68
  messages: MessageInfo[];
65
69
  }>;
@@ -24,6 +24,9 @@ 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
  }>;
@@ -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 };
@@ -314,10 +555,15 @@ export class OpenCodeProvider {
314
555
  if (base.type !== "server.heartbeat") {
315
556
  console.log("[sse]", base.type);
316
557
  }
317
- this.trackPermissionEvent(base.type, base.properties || {});
318
- 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);
319
564
  }
320
565
  console.log("[sse] Event stream ended, reconnecting...");
566
+ attempt++;
321
567
  }
322
568
  catch (err) {
323
569
  if (this.shuttingDown)
@@ -379,6 +625,55 @@ export class OpenCodeProvider {
379
625
  this.refreshPendingQuestions(),
380
626
  this.refreshSessionStatuses(),
381
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
+ }
382
677
  }
383
678
  async refreshSessionsMetadata() {
384
679
  const response = await this.client.session.list();
@@ -414,19 +709,7 @@ export class OpenCodeProvider {
414
709
  this.knownPendingPermissionIds.add(id);
415
710
  this.emitter?.({
416
711
  type: "permission.updated",
417
- properties: {
418
- id,
419
- sessionID: this.readString(permission.sessionID) ?? this.readString(permission.sessionId),
420
- messageID: this.readString(this.asRecord(permission.tool).messageID),
421
- callID: this.readString(this.asRecord(permission.tool).callID),
422
- type: this.readString(permission.permission) ?? "permission",
423
- title: this.readString(permission.title)
424
- ?? this.readString(permission.permission)
425
- ?? "Permission requested",
426
- metadata: permission.metadata && typeof permission.metadata === "object"
427
- ? permission.metadata
428
- : permission,
429
- },
712
+ properties: normalizePermissionProperties(permission),
430
713
  });
431
714
  }
432
715
  for (const id of Array.from(this.knownPendingPermissionIds)) {
@@ -530,40 +813,40 @@ export class OpenCodeProvider {
530
813
  }
531
814
  trackPermissionEvent(type, properties) {
532
815
  if (type === "permission.updated") {
533
- const id = this.readString(properties.id);
816
+ const id = readString(properties.id);
534
817
  if (id) {
535
818
  this.knownPendingPermissionIds.add(id);
536
819
  }
537
820
  return;
538
821
  }
539
822
  if (type === "permission.replied") {
540
- const id = this.readString(properties.permissionId)
541
- ?? this.readString(properties.requestID)
542
- ?? this.readString(properties.id);
823
+ const id = readString(properties.permissionId)
824
+ ?? readString(properties.requestID)
825
+ ?? readString(properties.id);
543
826
  if (id) {
544
827
  this.knownPendingPermissionIds.delete(id);
545
828
  }
546
829
  }
547
830
  if (type === "question.asked") {
548
- const id = this.readString(properties.id);
831
+ const id = readString(properties.id);
549
832
  if (id) {
550
833
  this.knownPendingQuestionIds.add(id);
551
834
  }
552
835
  return;
553
836
  }
554
837
  if (type === "question.replied" || type === "question.rejected") {
555
- const id = this.readString(properties.requestID)
556
- ?? this.readString(properties.questionId)
557
- ?? this.readString(properties.id);
838
+ const id = readString(properties.requestID)
839
+ ?? readString(properties.questionId)
840
+ ?? readString(properties.id);
558
841
  if (id) {
559
842
  this.knownPendingQuestionIds.delete(id);
560
843
  }
561
844
  }
562
845
  }
563
- asRecord(value) {
564
- return value && typeof value === "object" ? value : {};
565
- }
566
846
  readString(value) {
567
- return typeof value === "string" && value.length > 0 ? value : undefined;
847
+ return readString(value);
848
+ }
849
+ asRecord(value) {
850
+ return asRecord(value);
568
851
  }
569
852
  }
package/dist/index.js CHANGED
@@ -31,6 +31,10 @@ const __require = createRequire(import.meta.url);
31
31
  const VERSION = __require("../package.json").version;
32
32
  const VERBOSE_AI_LOGS = process.env.LUNEL_DEBUG_AI === "1";
33
33
  const PTY_RELEASE_BASE_URL = "https://github.com/lunel-dev/lunel/releases/download/v0";
34
+ const AI_RUNTIME_INSTALL_CANDIDATES = {
35
+ opencode: ["opencode-ai", "@opencode-ai/cli", "opencode"],
36
+ codex: ["@openai/codex", "codex"],
37
+ };
34
38
  const PTY_RELEASES = {
35
39
  "linux:x64": {
36
40
  fileName: "lunel-pty-linux-x8664-0",
@@ -529,6 +533,34 @@ async function loadGitignore(dirPath) {
529
533
  }
530
534
  return ig;
531
535
  }
536
+ const KNOWN_BINARY_EXTENSIONS = new Set([
537
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".avif", ".ico", ".icns", ".heic", ".heif", ".tiff", ".tif",
538
+ ".psd", ".ai", ".eps",
539
+ ".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
540
+ ".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv", ".m4v",
541
+ ".pdf", ".zip", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".tar",
542
+ ".exe", ".dll", ".so", ".dylib", ".bin", ".class", ".o", ".obj", ".a", ".lib",
543
+ ".ttf", ".otf", ".woff", ".woff2", ".eot",
544
+ ]);
545
+ function isLikelyBinaryContent(content) {
546
+ if (content.length === 0)
547
+ return false;
548
+ const sample = content.subarray(0, Math.min(content.length, 8192));
549
+ let suspicious = 0;
550
+ for (const byte of sample) {
551
+ if (byte === 0)
552
+ return true; // Null bytes strongly indicate binary data.
553
+ if (byte < 7 || (byte > 13 && byte < 32))
554
+ suspicious += 1;
555
+ }
556
+ return suspicious / sample.length > 0.3;
557
+ }
558
+ function shouldSkipAsBinary(filePath, content) {
559
+ const ext = path.extname(filePath).toLowerCase();
560
+ if (KNOWN_BINARY_EXTENSIONS.has(ext))
561
+ return true;
562
+ return isLikelyBinaryContent(content);
563
+ }
532
564
  async function handleFsGrep(payload) {
533
565
  const reqPath = payload.path || ".";
534
566
  const pattern = payload.pattern;
@@ -553,12 +585,12 @@ async function handleFsGrep(payload) {
553
585
  if (matches.length >= maxResults)
554
586
  return;
555
587
  try {
556
- const grepResult = shell.grep(regex, filePath);
557
- if (grepResult.code !== 0 || !grepResult.stdout.trim()) {
588
+ const rawContent = await fs.readFile(filePath);
589
+ if (shouldSkipAsBinary(relativePath, rawContent)) {
558
590
  regex.lastIndex = 0;
559
591
  return;
560
592
  }
561
- const content = await fs.readFile(filePath, "utf-8");
593
+ const content = rawContent.toString("utf-8");
562
594
  const lines = content.split("\n");
563
595
  for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
564
596
  if (regex.test(lines[i])) {
@@ -927,9 +959,20 @@ async function handleGitDiscard(payload) {
927
959
  await runGit(["clean", "-fd"]);
928
960
  }
929
961
  else if (paths && paths.length > 0) {
930
- const result = await runGit(["checkout", "--", ...paths]);
931
- if (result.code !== 0) {
932
- throw Object.assign(new Error(result.stderr || "git checkout failed"), { code: "EGIT" });
962
+ for (const filePath of paths) {
963
+ const tracked = await runGit(["ls-files", "--error-unmatch", "--", filePath]);
964
+ if (tracked.code === 0) {
965
+ const result = await runGit(["checkout", "--", filePath]);
966
+ if (result.code !== 0) {
967
+ throw Object.assign(new Error(result.stderr || `git checkout failed for ${filePath}`), { code: "EGIT" });
968
+ }
969
+ }
970
+ else {
971
+ const cleanResult = await runGit(["clean", "-fd", "--", filePath]);
972
+ if (cleanResult.code !== 0) {
973
+ throw Object.assign(new Error(cleanResult.stderr || `git clean failed for ${filePath}`), { code: "EGIT" });
974
+ }
975
+ }
933
976
  }
934
977
  }
935
978
  return {};
@@ -2631,6 +2674,9 @@ async function processMessage(message) {
2631
2674
  case "deleteSession":
2632
2675
  result = await aiManager.deleteSession(backend, payload.id);
2633
2676
  break;
2677
+ case "renameSession":
2678
+ result = await aiManager.renameSession(backend, payload.id, payload.title);
2679
+ break;
2634
2680
  case "getMessages":
2635
2681
  result = await aiManager.getMessages(backend, payload.id);
2636
2682
  break;
@@ -2945,6 +2991,78 @@ function displaySavedSessionNotice() {
2945
2991
  console.log(`${red}${border}${reset}`);
2946
2992
  console.log("");
2947
2993
  }
2994
+ function isCommandAvailable(command) {
2995
+ const probe = spawnSync(command, ["--version"], {
2996
+ stdio: "ignore",
2997
+ shell: process.platform === "win32",
2998
+ });
2999
+ const err = probe.error;
3000
+ if (err && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
3001
+ return false;
3002
+ }
3003
+ return !err;
3004
+ }
3005
+ function askYesNo(question, defaultValue = false) {
3006
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
3007
+ return Promise.resolve(false);
3008
+ return new Promise((resolve) => {
3009
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3010
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
3011
+ rl.question(`${question}${suffix}`, (answer) => {
3012
+ rl.close();
3013
+ const normalized = answer.trim().toLowerCase();
3014
+ if (!normalized) {
3015
+ resolve(defaultValue);
3016
+ return;
3017
+ }
3018
+ resolve(normalized === "y" || normalized === "yes");
3019
+ });
3020
+ });
3021
+ }
3022
+ function installLatestNpmPackage(pkg) {
3023
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
3024
+ const result = spawnSync(npmCommand, ["install", "-g", `${pkg}@latest`], {
3025
+ stdio: "inherit",
3026
+ shell: process.platform === "win32",
3027
+ env: process.env,
3028
+ });
3029
+ return !result.error && result.status === 0;
3030
+ }
3031
+ async function ensureAiCliRuntimes() {
3032
+ const missingBackends = Object.keys(AI_RUNTIME_INSTALL_CANDIDATES)
3033
+ .filter((backend) => !isCommandAvailable(backend));
3034
+ if (missingBackends.length === 0)
3035
+ return;
3036
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3037
+ console.warn(`[ai] Missing runtimes: ${missingBackends.join(", ")}. Run in an interactive shell to install them.`);
3038
+ return;
3039
+ }
3040
+ const installPrompt = `Missing AI runtimes (${missingBackends.join(", ")}). Install latest versions now?`;
3041
+ const approved = await askYesNo(installPrompt, false);
3042
+ if (!approved) {
3043
+ console.warn("[ai] Skipping AI runtime installation.");
3044
+ return;
3045
+ }
3046
+ for (const backend of missingBackends) {
3047
+ if (isCommandAvailable(backend))
3048
+ continue;
3049
+ const candidates = AI_RUNTIME_INSTALL_CANDIDATES[backend];
3050
+ let installed = false;
3051
+ for (const pkg of candidates) {
3052
+ console.log(`[ai] Installing ${backend} via npm package ${pkg}@latest...`);
3053
+ if (!installLatestNpmPackage(pkg))
3054
+ continue;
3055
+ if (isCommandAvailable(backend)) {
3056
+ installed = true;
3057
+ console.log(`[ai] ${backend} installed successfully.`);
3058
+ break;
3059
+ }
3060
+ }
3061
+ if (!installed) {
3062
+ console.warn(`[ai] Failed to install ${backend}. You can install it manually and restart the CLI.`);
3063
+ }
3064
+ }
3065
+ }
2948
3066
  function gracefulShutdown() {
2949
3067
  shuttingDown = true;
2950
3068
  console.log("\nShutting down...");
@@ -3125,6 +3243,7 @@ async function main() {
3125
3243
  else {
3126
3244
  debugLog(`PTY runtime unsupported on ${os.platform()}/${os.arch()}. Skipping prefetch.\n`);
3127
3245
  }
3246
+ await ensureAiCliRuntimes();
3128
3247
  // Start AI backends in the background so missing or slow AI runtimes never
3129
3248
  // block QR/session startup for the rest of the CLI.
3130
3249
  startAiManagerInBackground();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.114",
3
+ "version": "0.1.116",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",