lunel-cli 0.1.57 → 0.1.59

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.
@@ -43,6 +43,8 @@ export declare class CodexProvider implements AIProvider {
43
43
  share: ShareInfo;
44
44
  }>;
45
45
  permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
46
+ questionReply(): Promise<Record<string, never>>;
47
+ questionReject(): Promise<Record<string, never>>;
46
48
  private send;
47
49
  private call;
48
50
  private handleLine;
package/dist/ai/codex.js CHANGED
@@ -209,6 +209,12 @@ export class CodexProvider {
209
209
  });
210
210
  return {};
211
211
  }
212
+ async questionReply() {
213
+ throw new Error("Codex structured user input is not supported by Lunel yet");
214
+ }
215
+ async questionReject() {
216
+ throw new Error("Codex structured user input is not supported by Lunel yet");
217
+ }
212
218
  send(req) {
213
219
  if (!this.proc?.stdin?.writable)
214
220
  return;
@@ -42,6 +42,8 @@ export declare class AiManager {
42
42
  share: import("./interface.js").ShareInfo;
43
43
  }>;
44
44
  permissionReply(backend: AiBackend, sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
45
+ questionReply(backend: AiBackend, sessionId: string, questionId: string, answers: string[][]): Promise<Record<string, never>>;
46
+ questionReject(backend: AiBackend, sessionId: string, questionId: string): Promise<Record<string, never>>;
45
47
  }
46
48
  export declare function createAiManager(): Promise<AiManager>;
47
49
  export type { AIProvider, AiEventEmitter, AiEvent, ModelSelector } from "./interface.js";
package/dist/ai/index.js CHANGED
@@ -84,6 +84,20 @@ export class AiManager {
84
84
  permissionReply(backend, sessionId, permissionId, response) {
85
85
  return this.get(backend).permissionReply(sessionId, permissionId, response);
86
86
  }
87
+ questionReply(backend, sessionId, questionId, answers) {
88
+ const provider = this.get(backend);
89
+ if (!provider.questionReply) {
90
+ throw new Error(`Backend "${backend}" does not support question replies`);
91
+ }
92
+ return provider.questionReply(sessionId, questionId, answers);
93
+ }
94
+ questionReject(backend, sessionId, questionId) {
95
+ const provider = this.get(backend);
96
+ if (!provider.questionReject) {
97
+ throw new Error(`Backend "${backend}" does not support question rejection`);
98
+ }
99
+ return provider.questionReject(sessionId, questionId);
100
+ }
87
101
  }
88
102
  export async function createAiManager() {
89
103
  const manager = new AiManager();
@@ -69,4 +69,6 @@ export interface AIProvider {
69
69
  share: ShareInfo;
70
70
  }>;
71
71
  permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
72
+ questionReply?(sessionId: string, questionId: string, answers: string[][]): Promise<Record<string, never>>;
73
+ questionReject?(sessionId: string, questionId: string): Promise<Record<string, never>>;
72
74
  }
@@ -2,9 +2,12 @@ import type { AIProvider, AiEventEmitter, ModelSelector, MessageInfo, ProviderIn
2
2
  export declare class OpenCodeProvider implements AIProvider {
3
3
  private client;
4
4
  private server;
5
+ private authHeader;
5
6
  private lastActiveSessionId;
6
7
  private shuttingDown;
7
8
  private emitter;
9
+ private knownPendingPermissionIds;
10
+ private knownPendingQuestionIds;
8
11
  init(): Promise<void>;
9
12
  destroy(): Promise<void>;
10
13
  subscribe(emitter: AiEventEmitter): () => void;
@@ -40,5 +43,17 @@ export declare class OpenCodeProvider implements AIProvider {
40
43
  share: ShareInfo;
41
44
  }>;
42
45
  permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
46
+ questionReply(sessionId: string, questionId: string, answers: string[][]): Promise<Record<string, never>>;
47
+ questionReject(sessionId: string, questionId: string): Promise<Record<string, never>>;
43
48
  private runSseLoop;
49
+ private sendPromptAsync;
50
+ private reconcileOpenCodeState;
51
+ private refreshSessionsMetadata;
52
+ private refreshPendingPermissions;
53
+ private refreshPendingQuestions;
54
+ private fetchOpenCodeJson;
55
+ private refreshSessionStatuses;
56
+ private trackPermissionEvent;
57
+ private asRecord;
58
+ private readString;
44
59
  }
@@ -26,15 +26,19 @@ function requireData(response, label) {
26
26
  export class OpenCodeProvider {
27
27
  client = null;
28
28
  server = null;
29
+ authHeader = null;
29
30
  lastActiveSessionId = null;
30
31
  shuttingDown = false;
31
32
  emitter = null;
33
+ knownPendingPermissionIds = new Set();
34
+ knownPendingQuestionIds = new Set();
32
35
  async init() {
33
36
  const opencodeUsername = "lunel";
34
37
  const opencodePassword = crypto.randomBytes(32).toString("base64url");
35
38
  const authHeader = `Basic ${Buffer.from(`${opencodeUsername}:${opencodePassword}`).toString("base64")}`;
36
39
  process.env.OPENCODE_SERVER_USERNAME = opencodeUsername;
37
40
  process.env.OPENCODE_SERVER_PASSWORD = opencodePassword;
41
+ this.authHeader = authHeader;
38
42
  console.log("Starting OpenCode...");
39
43
  this.server = await createOpencodeServer({
40
44
  hostname: "127.0.0.1",
@@ -50,6 +54,7 @@ export class OpenCodeProvider {
50
54
  }
51
55
  async destroy() {
52
56
  this.shuttingDown = true;
57
+ this.authHeader = null;
53
58
  }
54
59
  subscribe(emitter) {
55
60
  this.emitter = emitter;
@@ -146,14 +151,9 @@ export class OpenCodeProvider {
146
151
  });
147
152
  }
148
153
  // Fire-and-forget — results come back through the SSE event stream.
149
- this.client.session.prompt({
150
- path: { id: sessionId },
151
- body: {
152
- parts: [{ type: "text", text }],
153
- ...(model ? { model } : {}),
154
- ...(agent ? { agent } : {}),
155
- },
156
- }).catch((err) => {
154
+ // Prefer the async prompt endpoint so long-running turns do not get tied
155
+ // to the request lifecycle the way the basic prompt route can be.
156
+ this.sendPromptAsync(sessionId, text, model, agent).catch((err) => {
157
157
  console.error("[ai] prompt error:", err.message);
158
158
  this.emitter?.({
159
159
  type: "prompt_error",
@@ -243,6 +243,23 @@ export class OpenCodeProvider {
243
243
  });
244
244
  return {};
245
245
  }
246
+ async questionReply(sessionId, questionId, answers) {
247
+ await this.fetchOpenCodeJson(`/question/${encodeURIComponent(questionId)}/reply`, {
248
+ method: "POST",
249
+ body: { answers },
250
+ });
251
+ this.knownPendingQuestionIds.delete(questionId);
252
+ this.emitter?.({ type: "question.replied", properties: { sessionID: sessionId, requestID: questionId, answers } });
253
+ return {};
254
+ }
255
+ async questionReject(sessionId, questionId) {
256
+ await this.fetchOpenCodeJson(`/question/${encodeURIComponent(questionId)}/reject`, {
257
+ method: "POST",
258
+ });
259
+ this.knownPendingQuestionIds.delete(questionId);
260
+ this.emitter?.({ type: "question.rejected", properties: { sessionID: sessionId, requestID: questionId } });
261
+ return {};
262
+ }
246
263
  // -------------------------------------------------------------------------
247
264
  // SSE event loop (private)
248
265
  // -------------------------------------------------------------------------
@@ -270,6 +287,9 @@ export class OpenCodeProvider {
270
287
  console.log(`[sse] Active session ${this.lastActiveSessionId} still valid.`);
271
288
  }
272
289
  }
290
+ if (attempt > 0) {
291
+ await this.reconcileOpenCodeState();
292
+ }
273
293
  const events = await this.client.event.subscribe();
274
294
  if (attempt > 0) {
275
295
  console.log(`[sse] reconnected after ${attempt} attempt(s)`);
@@ -290,6 +310,7 @@ export class OpenCodeProvider {
290
310
  continue;
291
311
  }
292
312
  console.log("[sse]", base.type);
313
+ this.trackPermissionEvent(base.type, base.properties || {});
293
314
  this.emitter?.({ type: base.type, properties: base.properties || {} });
294
315
  }
295
316
  console.log("[sse] Event stream ended, reconnecting...");
@@ -312,4 +333,230 @@ export class OpenCodeProvider {
312
333
  }
313
334
  }
314
335
  }
336
+ async sendPromptAsync(sessionId, text, model, agent) {
337
+ const server = this.server;
338
+ const authHeader = this.authHeader;
339
+ if (!server || !authHeader) {
340
+ throw new Error("OpenCode server is not ready");
341
+ }
342
+ const url = new URL(`/session/${encodeURIComponent(sessionId)}/prompt_async`, server.url);
343
+ const response = await fetch(url, {
344
+ method: "POST",
345
+ headers: {
346
+ Authorization: authHeader,
347
+ "content-type": "application/json",
348
+ accept: "application/json",
349
+ },
350
+ body: JSON.stringify({
351
+ parts: [{ type: "text", text }],
352
+ ...(model ? { model } : {}),
353
+ ...(agent ? { agent } : {}),
354
+ }),
355
+ });
356
+ if (!response.ok) {
357
+ let detail = "";
358
+ try {
359
+ detail = await response.text();
360
+ }
361
+ catch {
362
+ // ignore detail read failures
363
+ }
364
+ const suffix = detail.trim().length > 0 ? `: ${detail.trim()}` : "";
365
+ throw new Error(`OpenCode prompt_async failed (${response.status})${suffix}`);
366
+ }
367
+ }
368
+ async reconcileOpenCodeState() {
369
+ await Promise.allSettled([
370
+ this.refreshSessionsMetadata(),
371
+ this.refreshPendingPermissions(),
372
+ this.refreshPendingQuestions(),
373
+ this.refreshSessionStatuses(),
374
+ ]);
375
+ }
376
+ async refreshSessionsMetadata() {
377
+ const response = await this.client.session.list();
378
+ const sessions = Array.isArray(response.data) ? response.data : [];
379
+ for (const session of sessions) {
380
+ const info = this.asRecord(session);
381
+ const id = this.readString(info.id);
382
+ if (!id)
383
+ continue;
384
+ this.emitter?.({
385
+ type: "session.updated",
386
+ properties: { info },
387
+ });
388
+ }
389
+ }
390
+ async refreshPendingPermissions() {
391
+ const permissionApi = this.client?.permission;
392
+ if (!permissionApi?.list) {
393
+ return;
394
+ }
395
+ const response = await permissionApi.list();
396
+ const data = Array.isArray(response.data) ? response.data : [];
397
+ const nextIds = new Set();
398
+ for (const entry of data) {
399
+ const permission = this.asRecord(entry);
400
+ const id = this.readString(permission.id);
401
+ if (!id)
402
+ continue;
403
+ nextIds.add(id);
404
+ if (this.knownPendingPermissionIds.has(id)) {
405
+ continue;
406
+ }
407
+ this.knownPendingPermissionIds.add(id);
408
+ this.emitter?.({
409
+ type: "permission.updated",
410
+ properties: {
411
+ id,
412
+ sessionID: this.readString(permission.sessionID) ?? this.readString(permission.sessionId),
413
+ messageID: this.readString(this.asRecord(permission.tool).messageID),
414
+ callID: this.readString(this.asRecord(permission.tool).callID),
415
+ type: this.readString(permission.permission) ?? "permission",
416
+ title: this.readString(permission.title)
417
+ ?? this.readString(permission.permission)
418
+ ?? "Permission requested",
419
+ metadata: permission.metadata && typeof permission.metadata === "object"
420
+ ? permission.metadata
421
+ : permission,
422
+ },
423
+ });
424
+ }
425
+ for (const id of Array.from(this.knownPendingPermissionIds)) {
426
+ if (nextIds.has(id))
427
+ continue;
428
+ this.knownPendingPermissionIds.delete(id);
429
+ this.emitter?.({ type: "permission.replied", properties: { permissionId: id } });
430
+ }
431
+ }
432
+ async refreshPendingQuestions() {
433
+ const data = await this.fetchOpenCodeJson("/question", {
434
+ method: "GET",
435
+ });
436
+ const questions = Array.isArray(data) ? data : [];
437
+ const nextIds = new Set();
438
+ for (const entry of questions) {
439
+ const question = this.asRecord(entry);
440
+ const id = this.readString(question.id);
441
+ const sessionID = this.readString(question.sessionID) ?? this.readString(question.sessionId);
442
+ if (!id || !sessionID)
443
+ continue;
444
+ nextIds.add(id);
445
+ if (this.knownPendingQuestionIds.has(id)) {
446
+ continue;
447
+ }
448
+ this.knownPendingQuestionIds.add(id);
449
+ this.emitter?.({
450
+ type: "question.asked",
451
+ properties: {
452
+ id,
453
+ sessionID,
454
+ questions: Array.isArray(question.questions) ? question.questions : [],
455
+ tool: typeof question.tool === "object" && question.tool !== null ? question.tool : undefined,
456
+ },
457
+ });
458
+ }
459
+ for (const id of Array.from(this.knownPendingQuestionIds)) {
460
+ if (nextIds.has(id))
461
+ continue;
462
+ this.knownPendingQuestionIds.delete(id);
463
+ }
464
+ }
465
+ async fetchOpenCodeJson(pathname, options = {}) {
466
+ const server = this.server;
467
+ const authHeader = this.authHeader;
468
+ if (!server || !authHeader) {
469
+ throw new Error("OpenCode server is not ready");
470
+ }
471
+ const url = new URL(pathname, server.url);
472
+ const response = await fetch(url, {
473
+ method: options.method ?? "GET",
474
+ headers: {
475
+ Authorization: authHeader,
476
+ accept: "application/json",
477
+ ...(options.body ? { "content-type": "application/json" } : {}),
478
+ },
479
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
480
+ });
481
+ if (!response.ok) {
482
+ let detail = "";
483
+ try {
484
+ detail = await response.text();
485
+ }
486
+ catch {
487
+ // ignore detail read failures
488
+ }
489
+ const suffix = detail.trim().length > 0 ? `: ${detail.trim()}` : "";
490
+ throw new Error(`OpenCode request failed (${response.status})${suffix}`);
491
+ }
492
+ return response.json().catch(() => null);
493
+ }
494
+ async refreshSessionStatuses() {
495
+ const server = this.server;
496
+ const authHeader = this.authHeader;
497
+ if (!server || !authHeader) {
498
+ return;
499
+ }
500
+ const url = new URL("/session/status", server.url);
501
+ const response = await fetch(url, {
502
+ headers: {
503
+ Authorization: authHeader,
504
+ accept: "application/json",
505
+ },
506
+ });
507
+ if (!response.ok) {
508
+ return;
509
+ }
510
+ const payload = await response.json().catch(() => null);
511
+ if (!payload || typeof payload !== "object") {
512
+ return;
513
+ }
514
+ for (const [sessionId, status] of Object.entries(payload)) {
515
+ this.emitter?.({
516
+ type: "session.status",
517
+ properties: {
518
+ sessionID: sessionId,
519
+ status: status,
520
+ },
521
+ });
522
+ }
523
+ }
524
+ trackPermissionEvent(type, properties) {
525
+ if (type === "permission.updated") {
526
+ const id = this.readString(properties.id);
527
+ if (id) {
528
+ this.knownPendingPermissionIds.add(id);
529
+ }
530
+ return;
531
+ }
532
+ if (type === "permission.replied") {
533
+ const id = this.readString(properties.permissionId)
534
+ ?? this.readString(properties.requestID)
535
+ ?? this.readString(properties.id);
536
+ if (id) {
537
+ this.knownPendingPermissionIds.delete(id);
538
+ }
539
+ }
540
+ if (type === "question.asked") {
541
+ const id = this.readString(properties.id);
542
+ if (id) {
543
+ this.knownPendingQuestionIds.add(id);
544
+ }
545
+ return;
546
+ }
547
+ if (type === "question.replied" || type === "question.rejected") {
548
+ const id = this.readString(properties.requestID)
549
+ ?? this.readString(properties.questionId)
550
+ ?? this.readString(properties.id);
551
+ if (id) {
552
+ this.knownPendingQuestionIds.delete(id);
553
+ }
554
+ }
555
+ }
556
+ asRecord(value) {
557
+ return value && typeof value === "object" ? value : {};
558
+ }
559
+ readString(value) {
560
+ return typeof value === "string" && value.length > 0 ? value : undefined;
561
+ }
315
562
  }
package/dist/index.js CHANGED
@@ -2410,6 +2410,12 @@ async function processMessage(message) {
2410
2410
  result = await aiManager.permissionReply(backend, payload.sessionId, payload.permissionId, permResp);
2411
2411
  break;
2412
2412
  }
2413
+ case "questionReply":
2414
+ result = await aiManager.questionReply(backend, payload.sessionId, payload.questionId, payload.answers || []);
2415
+ break;
2416
+ case "questionReject":
2417
+ result = await aiManager.questionReject(backend, payload.sessionId, payload.questionId);
2418
+ break;
2413
2419
  default:
2414
2420
  throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
2415
2421
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",