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.
- package/dist/ai/codex.d.ts +2 -0
- package/dist/ai/codex.js +6 -0
- package/dist/ai/index.d.ts +2 -0
- package/dist/ai/index.js +14 -0
- package/dist/ai/interface.d.ts +2 -0
- package/dist/ai/opencode.d.ts +15 -0
- package/dist/ai/opencode.js +255 -8
- package/dist/index.js +6 -0
- package/package.json +1 -1
package/dist/ai/codex.d.ts
CHANGED
|
@@ -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;
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -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();
|
package/dist/ai/interface.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/ai/opencode.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/ai/opencode.js
CHANGED
|
@@ -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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
}
|