lunel-cli 0.1.43 → 0.1.45
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 +46 -0
- package/dist/ai/codex.js +271 -0
- package/dist/ai/index.d.ts +47 -0
- package/dist/ai/index.js +92 -0
- package/dist/ai/interface.d.ts +72 -0
- package/dist/ai/interface.js +3 -0
- package/dist/ai/opencode.d.ts +44 -0
- package/dist/ai/opencode.js +315 -0
- package/dist/index.js +125 -382
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AIProvider, AiEventEmitter, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
|
|
2
|
+
export declare class CodexProvider implements AIProvider {
|
|
3
|
+
private proc;
|
|
4
|
+
private shuttingDown;
|
|
5
|
+
private emitter;
|
|
6
|
+
private nextId;
|
|
7
|
+
private pending;
|
|
8
|
+
private sessions;
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
destroy(): Promise<void>;
|
|
11
|
+
subscribe(emitter: AiEventEmitter): () => void;
|
|
12
|
+
createSession(title?: string): Promise<{
|
|
13
|
+
session: SessionInfo;
|
|
14
|
+
}>;
|
|
15
|
+
listSessions(): Promise<{
|
|
16
|
+
sessions: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
getSession(id: string): Promise<{
|
|
19
|
+
session: SessionInfo;
|
|
20
|
+
}>;
|
|
21
|
+
deleteSession(id: string): Promise<Record<string, never>>;
|
|
22
|
+
getMessages(sessionId: string): Promise<{
|
|
23
|
+
messages: MessageInfo[];
|
|
24
|
+
}>;
|
|
25
|
+
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string): Promise<{
|
|
26
|
+
ack: true;
|
|
27
|
+
}>;
|
|
28
|
+
abort(sessionId: string): Promise<Record<string, never>>;
|
|
29
|
+
agents(): Promise<{
|
|
30
|
+
agents: unknown;
|
|
31
|
+
}>;
|
|
32
|
+
providers(): Promise<ProviderInfo>;
|
|
33
|
+
setAuth(providerId: string, key: string): Promise<Record<string, never>>;
|
|
34
|
+
command(sessionId: string, command: string, args: string): Promise<{
|
|
35
|
+
result: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
revert(sessionId: string, messageId: string): Promise<Record<string, never>>;
|
|
38
|
+
unrevert(sessionId: string): Promise<Record<string, never>>;
|
|
39
|
+
share(sessionId: string): Promise<{
|
|
40
|
+
share: ShareInfo;
|
|
41
|
+
}>;
|
|
42
|
+
permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
|
|
43
|
+
private send;
|
|
44
|
+
private call;
|
|
45
|
+
private handleLine;
|
|
46
|
+
}
|
package/dist/ai/codex.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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.
|
|
4
|
+
import * as crypto from "crypto";
|
|
5
|
+
import { spawn } from "child_process";
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export class CodexProvider {
|
|
25
|
+
proc = null;
|
|
26
|
+
shuttingDown = false;
|
|
27
|
+
emitter = null;
|
|
28
|
+
nextId = 1;
|
|
29
|
+
pending = new Map();
|
|
30
|
+
sessions = new Map();
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
// Lifecycle
|
|
33
|
+
// -------------------------------------------------------------------------
|
|
34
|
+
async init() {
|
|
35
|
+
console.log("Starting Codex app-server...");
|
|
36
|
+
this.proc = spawn("codex", ["app-server"], {
|
|
37
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
38
|
+
env: process.env,
|
|
39
|
+
});
|
|
40
|
+
const rl = createInterface({ input: this.proc.stdout });
|
|
41
|
+
rl.on("line", (line) => this.handleLine(line));
|
|
42
|
+
this.proc.on("error", (err) => {
|
|
43
|
+
console.error("[codex] Failed to start codex process:", err.message);
|
|
44
|
+
this.emitter?.({ type: "sse_dead", properties: { error: err.message } });
|
|
45
|
+
});
|
|
46
|
+
this.proc.on("exit", (code) => {
|
|
47
|
+
if (!this.shuttingDown) {
|
|
48
|
+
const msg = `codex app-server exited with code ${code}`;
|
|
49
|
+
console.error(`[codex] ${msg}`);
|
|
50
|
+
this.emitter?.({ type: "sse_dead", properties: { error: msg } });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// Handshake: initialize the JSON-RPC session.
|
|
54
|
+
await this.call("initialize", { clientInfo: { name: "lunel", version: "1.0" } });
|
|
55
|
+
console.log("Codex ready.\n");
|
|
56
|
+
}
|
|
57
|
+
async destroy() {
|
|
58
|
+
this.shuttingDown = true;
|
|
59
|
+
this.proc?.stdin?.end();
|
|
60
|
+
this.proc?.kill();
|
|
61
|
+
this.proc = null;
|
|
62
|
+
}
|
|
63
|
+
subscribe(emitter) {
|
|
64
|
+
this.emitter = emitter;
|
|
65
|
+
return () => {
|
|
66
|
+
this.emitter = null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Codex doesn't need session reconnect validation, so setActiveSession is omitted.
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
// Session management (emulated locally — Codex has no persistent store)
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
async createSession(title) {
|
|
74
|
+
const id = crypto.randomUUID();
|
|
75
|
+
const session = { id, title, messages: [], createdAt: Date.now() };
|
|
76
|
+
this.sessions.set(id, session);
|
|
77
|
+
// Start a new Codex thread for this session.
|
|
78
|
+
try {
|
|
79
|
+
const result = await this.call("thread/start", { title });
|
|
80
|
+
if (result?.threadId) {
|
|
81
|
+
session.threadId = result.threadId;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.warn("[codex] thread/start failed:", err.message);
|
|
86
|
+
}
|
|
87
|
+
return { session: { id, title: title ?? "", created: session.createdAt } };
|
|
88
|
+
}
|
|
89
|
+
async listSessions() {
|
|
90
|
+
return {
|
|
91
|
+
sessions: Array.from(this.sessions.values()).map((s) => ({
|
|
92
|
+
id: s.id,
|
|
93
|
+
title: s.title ?? "",
|
|
94
|
+
created: s.createdAt,
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async getSession(id) {
|
|
99
|
+
const s = this.sessions.get(id);
|
|
100
|
+
if (!s)
|
|
101
|
+
throw Object.assign(new Error(`Session ${id} not found`), { code: "ENOENT" });
|
|
102
|
+
return { session: { id: s.id, title: s.title ?? "", created: s.createdAt } };
|
|
103
|
+
}
|
|
104
|
+
async deleteSession(id) {
|
|
105
|
+
this.sessions.delete(id);
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
// -------------------------------------------------------------------------
|
|
109
|
+
// Messages
|
|
110
|
+
// -------------------------------------------------------------------------
|
|
111
|
+
async getMessages(sessionId) {
|
|
112
|
+
const s = this.sessions.get(sessionId);
|
|
113
|
+
return { messages: s?.messages ?? [] };
|
|
114
|
+
}
|
|
115
|
+
// -------------------------------------------------------------------------
|
|
116
|
+
// Interaction
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
async prompt(sessionId, text, model, agent) {
|
|
119
|
+
const session = this.sessions.get(sessionId);
|
|
120
|
+
const threadId = session?.threadId;
|
|
121
|
+
// Append the user message to local history immediately.
|
|
122
|
+
if (session) {
|
|
123
|
+
session.messages.push({
|
|
124
|
+
id: crypto.randomUUID(),
|
|
125
|
+
role: "user",
|
|
126
|
+
parts: [{ type: "text", text }],
|
|
127
|
+
time: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// Fire-and-forget — the response streams back as JSON-RPC notifications.
|
|
131
|
+
this.call("turn/start", {
|
|
132
|
+
...(threadId ? { threadId } : { sessionId }),
|
|
133
|
+
message: { role: "user", content: text },
|
|
134
|
+
...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
|
|
135
|
+
...(agent ? { agent } : {}),
|
|
136
|
+
}).catch((err) => {
|
|
137
|
+
console.error("[codex] turn/start error:", err.message);
|
|
138
|
+
this.emitter?.({ type: "prompt_error", properties: { sessionId, error: err.message } });
|
|
139
|
+
});
|
|
140
|
+
return { ack: true };
|
|
141
|
+
}
|
|
142
|
+
async abort(sessionId) {
|
|
143
|
+
const session = this.sessions.get(sessionId);
|
|
144
|
+
await this.call("turn/abort", { ...(session?.threadId ? { threadId: session.threadId } : { sessionId }) });
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
// Metadata
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
async agents() {
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.call("agent/list", {});
|
|
153
|
+
return { agents: result ?? [] };
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Codex may not support agents — return empty list gracefully.
|
|
157
|
+
return { agents: [] };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async providers() {
|
|
161
|
+
try {
|
|
162
|
+
const result = await this.call("provider/list", {});
|
|
163
|
+
return {
|
|
164
|
+
providers: result?.providers ?? [],
|
|
165
|
+
default: result?.default ?? {},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { providers: [], default: {} };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// -------------------------------------------------------------------------
|
|
173
|
+
// Auth
|
|
174
|
+
// -------------------------------------------------------------------------
|
|
175
|
+
async setAuth(providerId, key) {
|
|
176
|
+
await this.call("auth/set", { providerId, key });
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
// Session operations
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
async command(sessionId, command, args) {
|
|
183
|
+
const session = this.sessions.get(sessionId);
|
|
184
|
+
const result = await this.call("session/command", {
|
|
185
|
+
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
186
|
+
command,
|
|
187
|
+
arguments: args,
|
|
188
|
+
});
|
|
189
|
+
return { result: result ?? null };
|
|
190
|
+
}
|
|
191
|
+
async revert(sessionId, messageId) {
|
|
192
|
+
const session = this.sessions.get(sessionId);
|
|
193
|
+
await this.call("session/revert", {
|
|
194
|
+
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
195
|
+
messageId,
|
|
196
|
+
});
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
async unrevert(sessionId) {
|
|
200
|
+
const session = this.sessions.get(sessionId);
|
|
201
|
+
await this.call("session/unrevert", {
|
|
202
|
+
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
203
|
+
});
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
async share(sessionId) {
|
|
207
|
+
// Codex has no share concept — return a null stub so the app degrades gracefully.
|
|
208
|
+
return { share: { url: null } };
|
|
209
|
+
}
|
|
210
|
+
async permissionReply(sessionId, permissionId, response) {
|
|
211
|
+
const session = this.sessions.get(sessionId);
|
|
212
|
+
await this.call("session/permission", {
|
|
213
|
+
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
214
|
+
permissionId,
|
|
215
|
+
response,
|
|
216
|
+
});
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
// -------------------------------------------------------------------------
|
|
220
|
+
// JSON-RPC internals
|
|
221
|
+
// -------------------------------------------------------------------------
|
|
222
|
+
send(req) {
|
|
223
|
+
if (!this.proc?.stdin?.writable)
|
|
224
|
+
return;
|
|
225
|
+
this.proc.stdin.write(JSON.stringify(req) + "\n");
|
|
226
|
+
}
|
|
227
|
+
call(method, params) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const id = this.nextId++;
|
|
230
|
+
this.pending.set(id, { resolve, reject });
|
|
231
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
232
|
+
// Safety timeout: if Codex never responds, don't hang the caller forever.
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
if (this.pending.has(id)) {
|
|
235
|
+
this.pending.delete(id);
|
|
236
|
+
reject(new Error(`Codex RPC timeout: ${method} (id=${id})`));
|
|
237
|
+
}
|
|
238
|
+
}, 30_000);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
handleLine(line) {
|
|
242
|
+
let msg;
|
|
243
|
+
try {
|
|
244
|
+
msg = JSON.parse(line);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return; // ignore non-JSON lines (e.g. startup banners)
|
|
248
|
+
}
|
|
249
|
+
// JSON-RPC response (has numeric id + result or error)
|
|
250
|
+
if ("id" in msg && typeof msg.id === "number") {
|
|
251
|
+
const resp = msg;
|
|
252
|
+
const pending = this.pending.get(resp.id);
|
|
253
|
+
if (pending) {
|
|
254
|
+
this.pending.delete(resp.id);
|
|
255
|
+
if (resp.error) {
|
|
256
|
+
pending.reject(new Error(resp.error.message));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
pending.resolve(resp.result ?? null);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// JSON-RPC notification (no id — async event from Codex)
|
|
265
|
+
const notif = msg;
|
|
266
|
+
const params = (notif.params ?? {});
|
|
267
|
+
const mappedType = NOTIFICATION_TYPE_MAP[notif.method] ?? notif.method;
|
|
268
|
+
console.log("[codex]", notif.method);
|
|
269
|
+
this.emitter?.({ type: mappedType, properties: params });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AiEvent, ModelSelector } from "./interface.js";
|
|
2
|
+
export type AiBackend = "opencode" | "codex";
|
|
3
|
+
export declare class AiManager {
|
|
4
|
+
private _providers;
|
|
5
|
+
private _available;
|
|
6
|
+
init(): Promise<void>;
|
|
7
|
+
private tryInit;
|
|
8
|
+
availableBackends(): AiBackend[];
|
|
9
|
+
private get;
|
|
10
|
+
subscribe(emitter: (backend: AiBackend, event: AiEvent) => void): () => void;
|
|
11
|
+
destroy(): Promise<void>;
|
|
12
|
+
listAllSessions(): Promise<{
|
|
13
|
+
sessions: Array<Record<string, unknown> & {
|
|
14
|
+
backend: AiBackend;
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
17
|
+
createSession(backend: AiBackend, title?: string): Promise<{
|
|
18
|
+
session: import("./interface.js").SessionInfo;
|
|
19
|
+
}>;
|
|
20
|
+
getSession(backend: AiBackend, id: string): Promise<{
|
|
21
|
+
session: import("./interface.js").SessionInfo;
|
|
22
|
+
}>;
|
|
23
|
+
deleteSession(backend: AiBackend, id: string): Promise<Record<string, never>>;
|
|
24
|
+
getMessages(backend: AiBackend, sessionId: string): Promise<{
|
|
25
|
+
messages: import("./interface.js").MessageInfo[];
|
|
26
|
+
}>;
|
|
27
|
+
prompt(backend: AiBackend, sessionId: string, text: string, model?: ModelSelector, agent?: string): Promise<{
|
|
28
|
+
ack: true;
|
|
29
|
+
}>;
|
|
30
|
+
abort(backend: AiBackend, sessionId: string): Promise<Record<string, never>>;
|
|
31
|
+
agents(backend?: AiBackend): Promise<{
|
|
32
|
+
agents: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
providers(backend?: AiBackend): Promise<import("./interface.js").ProviderInfo>;
|
|
35
|
+
setAuth(backend: AiBackend, providerId: string, key: string): Promise<Record<string, never>>;
|
|
36
|
+
command(backend: AiBackend, sessionId: string, command: string, args: string): Promise<{
|
|
37
|
+
result: unknown;
|
|
38
|
+
}>;
|
|
39
|
+
revert(backend: AiBackend, sessionId: string, messageId: string): Promise<Record<string, never>>;
|
|
40
|
+
unrevert(backend: AiBackend, sessionId: string): Promise<Record<string, never>>;
|
|
41
|
+
share(backend: AiBackend, sessionId: string): Promise<{
|
|
42
|
+
share: import("./interface.js").ShareInfo;
|
|
43
|
+
}>;
|
|
44
|
+
permissionReply(backend: AiBackend, sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createAiManager(): Promise<AiManager>;
|
|
47
|
+
export type { AIProvider, AiEventEmitter, AiEvent, ModelSelector } from "./interface.js";
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// AI manager — runs both OpenCode and Codex simultaneously and routes calls
|
|
2
|
+
// by the `backend` field in each request. Backends that fail to init are
|
|
3
|
+
// skipped gracefully; the available list is exposed to the app.
|
|
4
|
+
export class AiManager {
|
|
5
|
+
_providers = {};
|
|
6
|
+
_available = [];
|
|
7
|
+
async init() {
|
|
8
|
+
await Promise.allSettled([
|
|
9
|
+
this.tryInit("opencode"),
|
|
10
|
+
this.tryInit("codex"),
|
|
11
|
+
]);
|
|
12
|
+
if (this._available.length === 0) {
|
|
13
|
+
throw new Error("No AI backend could be started. Ensure opencode or codex is installed.");
|
|
14
|
+
}
|
|
15
|
+
console.log(`[ai] Available backends: ${this._available.join(", ")}`);
|
|
16
|
+
}
|
|
17
|
+
async tryInit(backend) {
|
|
18
|
+
try {
|
|
19
|
+
if (backend === "opencode") {
|
|
20
|
+
const { OpenCodeProvider } = await import("./opencode.js");
|
|
21
|
+
const p = new OpenCodeProvider();
|
|
22
|
+
await p.init();
|
|
23
|
+
this._providers.opencode = p;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const { CodexProvider } = await import("./codex.js");
|
|
27
|
+
const p = new CodexProvider();
|
|
28
|
+
await p.init();
|
|
29
|
+
this._providers.codex = p;
|
|
30
|
+
}
|
|
31
|
+
this._available.push(backend);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.warn(`[ai] ${backend} backend unavailable: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
availableBackends() {
|
|
38
|
+
return [...this._available];
|
|
39
|
+
}
|
|
40
|
+
get(backend) {
|
|
41
|
+
const p = this._providers[backend];
|
|
42
|
+
if (!p) {
|
|
43
|
+
throw Object.assign(new Error(`Backend "${backend}" is not available`), { code: "EUNAVAILABLE" });
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
// Wire each provider's events to the emitter, tagged with backend name.
|
|
48
|
+
subscribe(emitter) {
|
|
49
|
+
const cleanups = this._available.map((backend) => this._providers[backend].subscribe((event) => emitter(backend, event)));
|
|
50
|
+
return () => cleanups.forEach((c) => c());
|
|
51
|
+
}
|
|
52
|
+
async destroy() {
|
|
53
|
+
await Promise.allSettled(this._available.map((b) => this._providers[b].destroy()));
|
|
54
|
+
}
|
|
55
|
+
// List sessions from all available backends, each tagged with its backend.
|
|
56
|
+
async listAllSessions() {
|
|
57
|
+
const results = await Promise.allSettled(this._available.map(async (backend) => {
|
|
58
|
+
const res = await this._providers[backend].listSessions();
|
|
59
|
+
const sessions = res.sessions ?? [];
|
|
60
|
+
return sessions.map((s) => ({ ...s, backend }));
|
|
61
|
+
}));
|
|
62
|
+
const sessions = results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
|
|
63
|
+
return { sessions };
|
|
64
|
+
}
|
|
65
|
+
// Session management — all require explicit backend
|
|
66
|
+
createSession(backend, title) { return this.get(backend).createSession(title); }
|
|
67
|
+
getSession(backend, id) { return this.get(backend).getSession(id); }
|
|
68
|
+
deleteSession(backend, id) { return this.get(backend).deleteSession(id); }
|
|
69
|
+
getMessages(backend, sessionId) { return this.get(backend).getMessages(sessionId); }
|
|
70
|
+
prompt(backend, sessionId, text, model, agent) {
|
|
71
|
+
this.get(backend).setActiveSession?.(sessionId);
|
|
72
|
+
return this.get(backend).prompt(sessionId, text, model, agent);
|
|
73
|
+
}
|
|
74
|
+
abort(backend, sessionId) { return this.get(backend).abort(sessionId); }
|
|
75
|
+
// Metadata — backend is optional, falls back to first available
|
|
76
|
+
agents(backend) { return this.get(backend ?? this._available[0]).agents(); }
|
|
77
|
+
providers(backend) { return this.get(backend ?? this._available[0]).providers(); }
|
|
78
|
+
setAuth(backend, providerId, key) { return this.get(backend).setAuth(providerId, key); }
|
|
79
|
+
// Session operations
|
|
80
|
+
command(backend, sessionId, command, args) { return this.get(backend).command(sessionId, command, args); }
|
|
81
|
+
revert(backend, sessionId, messageId) { return this.get(backend).revert(sessionId, messageId); }
|
|
82
|
+
unrevert(backend, sessionId) { return this.get(backend).unrevert(sessionId); }
|
|
83
|
+
share(backend, sessionId) { return this.get(backend).share(sessionId); }
|
|
84
|
+
permissionReply(backend, sessionId, permissionId, response) {
|
|
85
|
+
return this.get(backend).permissionReply(sessionId, permissionId, response);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function createAiManager() {
|
|
89
|
+
const manager = new AiManager();
|
|
90
|
+
await manager.init();
|
|
91
|
+
return manager;
|
|
92
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface AiEvent {
|
|
2
|
+
type: string;
|
|
3
|
+
properties: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export type AiEventEmitter = (event: AiEvent) => void;
|
|
6
|
+
export interface ModelSelector {
|
|
7
|
+
providerID: string;
|
|
8
|
+
modelID: string;
|
|
9
|
+
}
|
|
10
|
+
export interface MessageInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
role: string;
|
|
13
|
+
parts: unknown[];
|
|
14
|
+
time: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface SessionInfo {
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface ShareInfo {
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface ProviderInfo {
|
|
23
|
+
providers: unknown[];
|
|
24
|
+
default: Record<string, string>;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every AI backend (OpenCode, Codex, …) implements this interface.
|
|
29
|
+
* Method names map 1-to-1 with the "ai" namespace actions in index.ts.
|
|
30
|
+
*/
|
|
31
|
+
export interface AIProvider {
|
|
32
|
+
init(): Promise<void>;
|
|
33
|
+
destroy(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Register an event emitter. The provider calls it for every async event
|
|
36
|
+
* (SSE events, streaming tokens, errors, etc.).
|
|
37
|
+
* Returns a cleanup/unsubscribe function.
|
|
38
|
+
*/
|
|
39
|
+
subscribe(emitter: AiEventEmitter): () => void;
|
|
40
|
+
setActiveSession?(sessionId: string): void;
|
|
41
|
+
createSession(title?: string): Promise<{
|
|
42
|
+
session: SessionInfo;
|
|
43
|
+
}>;
|
|
44
|
+
listSessions(): Promise<{
|
|
45
|
+
sessions: unknown;
|
|
46
|
+
}>;
|
|
47
|
+
getSession(id: string): Promise<{
|
|
48
|
+
session: SessionInfo;
|
|
49
|
+
}>;
|
|
50
|
+
deleteSession(id: string): Promise<Record<string, never>>;
|
|
51
|
+
getMessages(sessionId: string): Promise<{
|
|
52
|
+
messages: MessageInfo[];
|
|
53
|
+
}>;
|
|
54
|
+
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string): Promise<{
|
|
55
|
+
ack: true;
|
|
56
|
+
}>;
|
|
57
|
+
abort(sessionId: string): Promise<Record<string, never>>;
|
|
58
|
+
agents(): Promise<{
|
|
59
|
+
agents: unknown;
|
|
60
|
+
}>;
|
|
61
|
+
providers(): Promise<ProviderInfo>;
|
|
62
|
+
setAuth(providerId: string, key: string): Promise<Record<string, never>>;
|
|
63
|
+
command(sessionId: string, command: string, args: string): Promise<{
|
|
64
|
+
result: unknown;
|
|
65
|
+
}>;
|
|
66
|
+
revert(sessionId: string, messageId: string): Promise<Record<string, never>>;
|
|
67
|
+
unrevert(sessionId: string): Promise<Record<string, never>>;
|
|
68
|
+
share(sessionId: string): Promise<{
|
|
69
|
+
share: ShareInfo;
|
|
70
|
+
}>;
|
|
71
|
+
permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AIProvider, AiEventEmitter, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
|
|
2
|
+
export declare class OpenCodeProvider implements AIProvider {
|
|
3
|
+
private client;
|
|
4
|
+
private server;
|
|
5
|
+
private lastActiveSessionId;
|
|
6
|
+
private shuttingDown;
|
|
7
|
+
private emitter;
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
destroy(): Promise<void>;
|
|
10
|
+
subscribe(emitter: AiEventEmitter): () => void;
|
|
11
|
+
setActiveSession(sessionId: string): void;
|
|
12
|
+
createSession(title?: string): Promise<{
|
|
13
|
+
session: SessionInfo;
|
|
14
|
+
}>;
|
|
15
|
+
listSessions(): Promise<{
|
|
16
|
+
sessions: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
getSession(id: string): Promise<{
|
|
19
|
+
session: SessionInfo;
|
|
20
|
+
}>;
|
|
21
|
+
deleteSession(id: string): Promise<Record<string, never>>;
|
|
22
|
+
getMessages(sessionId: string): Promise<{
|
|
23
|
+
messages: MessageInfo[];
|
|
24
|
+
}>;
|
|
25
|
+
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string): Promise<{
|
|
26
|
+
ack: true;
|
|
27
|
+
}>;
|
|
28
|
+
abort(sessionId: string): Promise<Record<string, never>>;
|
|
29
|
+
agents(): Promise<{
|
|
30
|
+
agents: unknown;
|
|
31
|
+
}>;
|
|
32
|
+
providers(): Promise<ProviderInfo>;
|
|
33
|
+
setAuth(providerId: string, key: string): Promise<Record<string, never>>;
|
|
34
|
+
command(sessionId: string, command: string, args: string): Promise<{
|
|
35
|
+
result: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
revert(sessionId: string, messageId: string): Promise<Record<string, never>>;
|
|
38
|
+
unrevert(sessionId: string): Promise<Record<string, never>>;
|
|
39
|
+
share(sessionId: string): Promise<{
|
|
40
|
+
share: ShareInfo;
|
|
41
|
+
}>;
|
|
42
|
+
permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
|
|
43
|
+
private runSseLoop;
|
|
44
|
+
}
|