jukto-cli 0.1.0

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.
@@ -0,0 +1,57 @@
1
+ import type { AiEvent, ModelSelector, FileAttachment, CodexPromptOptions } 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<{
24
+ deleted: boolean;
25
+ }>;
26
+ renameSession(backend: AiBackend, id: string, title: string): Promise<{
27
+ session: import("./interface.js").SessionInfo;
28
+ }>;
29
+ getMessages(backend: AiBackend, sessionId: string): Promise<{
30
+ messages: import("./interface.js").MessageInfo[];
31
+ }>;
32
+ statuses(backend: AiBackend): Promise<{
33
+ statuses: Record<string, unknown>;
34
+ }>;
35
+ prompt(backend: AiBackend, sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
36
+ ack: true;
37
+ }>;
38
+ abort(backend: AiBackend, sessionId: string): Promise<Record<string, never>>;
39
+ agents(backend?: AiBackend): Promise<{
40
+ agents: unknown;
41
+ }>;
42
+ providers(backend?: AiBackend): Promise<import("./interface.js").ProviderInfo>;
43
+ setAuth(backend: AiBackend, providerId: string, key: string): Promise<Record<string, never>>;
44
+ command(backend: AiBackend, sessionId: string, command: string, args: string): Promise<{
45
+ result: unknown;
46
+ }>;
47
+ revert(backend: AiBackend, sessionId: string, messageId: string): Promise<Record<string, never>>;
48
+ unrevert(backend: AiBackend, sessionId: string): Promise<Record<string, never>>;
49
+ share(backend: AiBackend, sessionId: string): Promise<{
50
+ share: import("./interface.js").ShareInfo;
51
+ }>;
52
+ permissionReply(backend: AiBackend, sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
53
+ questionReply(backend: AiBackend, sessionId: string, questionId: string, answers: string[][]): Promise<Record<string, never>>;
54
+ questionReject(backend: AiBackend, sessionId: string, questionId: string): Promise<Record<string, never>>;
55
+ }
56
+ export declare function createAiManager(): Promise<AiManager>;
57
+ export type { AIProvider, AiEventEmitter, AiEvent, ModelSelector } from "./interface.js";
@@ -0,0 +1,119 @@
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
+ const DEBUG_MODE = process.env.JUKTO_DEBUG === "1" || process.env.JUKTO_DEBUG_AI === "1";
5
+ export class AiManager {
6
+ _providers = {};
7
+ _available = [];
8
+ async init() {
9
+ await Promise.allSettled([
10
+ this.tryInit("opencode"),
11
+ this.tryInit("codex"),
12
+ ]);
13
+ if (this._available.length === 0) {
14
+ console.warn("[ai] No AI backends available. CLI will continue without AI features.");
15
+ return;
16
+ }
17
+ if (DEBUG_MODE) {
18
+ console.log(`[ai] Available backends: ${this._available.join(", ")}`);
19
+ }
20
+ }
21
+ async tryInit(backend) {
22
+ try {
23
+ if (backend === "opencode") {
24
+ const { OpenCodeProvider } = await import("./opencode.js");
25
+ const p = new OpenCodeProvider();
26
+ await p.init();
27
+ this._providers.opencode = p;
28
+ }
29
+ else {
30
+ const { CodexProvider } = await import("./codex.js");
31
+ const p = new CodexProvider();
32
+ await p.init();
33
+ this._providers.codex = p;
34
+ }
35
+ this._available.push(backend);
36
+ }
37
+ catch (err) {
38
+ if (DEBUG_MODE) {
39
+ console.warn(`[ai] ${backend} backend unavailable: ${err.message}`);
40
+ }
41
+ }
42
+ }
43
+ availableBackends() {
44
+ return [...this._available];
45
+ }
46
+ get(backend) {
47
+ const p = this._providers[backend];
48
+ if (!p) {
49
+ throw Object.assign(new Error(`Backend "${backend}" is not available`), { code: "EUNAVAILABLE" });
50
+ }
51
+ return p;
52
+ }
53
+ // Wire each provider's events to the emitter, tagged with backend name.
54
+ subscribe(emitter) {
55
+ const cleanups = this._available.map((backend) => this._providers[backend].subscribe((event) => emitter(backend, event)));
56
+ return () => cleanups.forEach((c) => c());
57
+ }
58
+ async destroy() {
59
+ await Promise.allSettled(this._available.map((b) => this._providers[b].destroy()));
60
+ }
61
+ // List sessions from all available backends, each tagged with its backend.
62
+ async listAllSessions() {
63
+ const results = await Promise.allSettled(this._available.map(async (backend) => {
64
+ const res = await this._providers[backend].listSessions();
65
+ const sessions = res.sessions ?? [];
66
+ return sessions.map((s) => ({ ...s, backend }));
67
+ }));
68
+ const sessions = results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
69
+ return { sessions };
70
+ }
71
+ // Session management — all require explicit backend
72
+ createSession(backend, title) { return this.get(backend).createSession(title); }
73
+ getSession(backend, id) { return this.get(backend).getSession(id); }
74
+ deleteSession(backend, id) { return this.get(backend).deleteSession(id); }
75
+ renameSession(backend, id, title) { return this.get(backend).renameSession(id, title); }
76
+ getMessages(backend, sessionId) { return this.get(backend).getMessages(sessionId); }
77
+ async statuses(backend) {
78
+ const provider = this.get(backend);
79
+ if (!provider.statuses)
80
+ return { statuses: {} };
81
+ return provider.statuses();
82
+ }
83
+ prompt(backend, sessionId, text, model, agent, files, codexOptions) {
84
+ this.get(backend).setActiveSession?.(sessionId);
85
+ return this.get(backend).prompt(sessionId, text, model, agent, files, codexOptions);
86
+ }
87
+ abort(backend, sessionId) { return this.get(backend).abort(sessionId); }
88
+ // Metadata — backend is optional, falls back to first available
89
+ agents(backend) { return this.get(backend ?? this._available[0]).agents(); }
90
+ providers(backend) { return this.get(backend ?? this._available[0]).providers(); }
91
+ setAuth(backend, providerId, key) { return this.get(backend).setAuth(providerId, key); }
92
+ // Session operations
93
+ command(backend, sessionId, command, args) { return this.get(backend).command(sessionId, command, args); }
94
+ revert(backend, sessionId, messageId) { return this.get(backend).revert(sessionId, messageId); }
95
+ unrevert(backend, sessionId) { return this.get(backend).unrevert(sessionId); }
96
+ share(backend, sessionId) { return this.get(backend).share(sessionId); }
97
+ permissionReply(backend, sessionId, permissionId, response) {
98
+ return this.get(backend).permissionReply(sessionId, permissionId, response);
99
+ }
100
+ questionReply(backend, sessionId, questionId, answers) {
101
+ const provider = this.get(backend);
102
+ if (!provider.questionReply) {
103
+ throw new Error(`Backend "${backend}" does not support question replies`);
104
+ }
105
+ return provider.questionReply(sessionId, questionId, answers);
106
+ }
107
+ questionReject(backend, sessionId, questionId) {
108
+ const provider = this.get(backend);
109
+ if (!provider.questionReject) {
110
+ throw new Error(`Backend "${backend}" does not support question rejection`);
111
+ }
112
+ return provider.questionReject(sessionId, questionId);
113
+ }
114
+ }
115
+ export async function createAiManager() {
116
+ const manager = new AiManager();
117
+ await manager.init();
118
+ return manager;
119
+ }
@@ -0,0 +1,93 @@
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 CodexPromptOptions {
11
+ reasoningEffort?: string;
12
+ speed?: string;
13
+ permissionMode?: "default" | "full-access";
14
+ }
15
+ export interface FileAttachment {
16
+ type: "file";
17
+ mime: string;
18
+ filename?: string;
19
+ url: string;
20
+ }
21
+ export interface MessageInfo {
22
+ id: string;
23
+ role: string;
24
+ parts: unknown[];
25
+ time: unknown;
26
+ }
27
+ export interface SessionInfo {
28
+ [key: string]: unknown;
29
+ }
30
+ export interface ShareInfo {
31
+ [key: string]: unknown;
32
+ }
33
+ export interface ProviderInfo {
34
+ providers: unknown[];
35
+ default: Record<string, string>;
36
+ [key: string]: unknown;
37
+ }
38
+ /**
39
+ * Every AI backend (OpenCode, Codex, …) implements this interface.
40
+ * Method names map 1-to-1 with the "ai" namespace actions in index.ts.
41
+ */
42
+ export interface AIProvider {
43
+ init(): Promise<void>;
44
+ destroy(): Promise<void>;
45
+ /**
46
+ * Register an event emitter. The provider calls it for every async event
47
+ * (SSE events, streaming tokens, errors, etc.).
48
+ * Returns a cleanup/unsubscribe function.
49
+ */
50
+ subscribe(emitter: AiEventEmitter): () => void;
51
+ setActiveSession?(sessionId: string): void;
52
+ createSession(title?: string): Promise<{
53
+ session: SessionInfo;
54
+ }>;
55
+ listSessions(): Promise<{
56
+ sessions: unknown;
57
+ }>;
58
+ getSession(id: string): Promise<{
59
+ session: SessionInfo;
60
+ }>;
61
+ deleteSession(id: string): Promise<{
62
+ deleted: boolean;
63
+ }>;
64
+ renameSession(id: string, title: string): Promise<{
65
+ session: SessionInfo;
66
+ }>;
67
+ getMessages(sessionId: string): Promise<{
68
+ messages: MessageInfo[];
69
+ }>;
70
+ statuses?(): Promise<{
71
+ statuses: Record<string, unknown>;
72
+ }>;
73
+ prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
74
+ ack: true;
75
+ }>;
76
+ abort(sessionId: string): Promise<Record<string, never>>;
77
+ agents(): Promise<{
78
+ agents: unknown;
79
+ }>;
80
+ providers(): Promise<ProviderInfo>;
81
+ setAuth(providerId: string, key: string): Promise<Record<string, never>>;
82
+ command(sessionId: string, command: string, args: string): Promise<{
83
+ result: unknown;
84
+ }>;
85
+ revert(sessionId: string, messageId: string): Promise<Record<string, never>>;
86
+ unrevert(sessionId: string): Promise<Record<string, never>>;
87
+ share(sessionId: string): Promise<{
88
+ share: ShareInfo;
89
+ }>;
90
+ permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
91
+ questionReply?(sessionId: string, questionId: string, answers: string[][]): Promise<Record<string, never>>;
92
+ questionReject?(sessionId: string, questionId: string): Promise<Record<string, never>>;
93
+ }
@@ -0,0 +1,3 @@
1
+ // Shared types for AI provider abstraction.
2
+ // No runtime imports — pure TypeScript types only.
3
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
2
+ export declare class OpenCodeProvider implements AIProvider {
3
+ private client;
4
+ private server;
5
+ private authHeader;
6
+ private lastActiveSessionId;
7
+ private shuttingDown;
8
+ private emitter;
9
+ private knownPendingPermissionIds;
10
+ private knownPendingQuestionIds;
11
+ private debugLog;
12
+ private debugWarn;
13
+ private debugError;
14
+ init(): Promise<void>;
15
+ destroy(): Promise<void>;
16
+ subscribe(emitter: AiEventEmitter): () => void;
17
+ setActiveSession(sessionId: string): void;
18
+ createSession(title?: string): Promise<{
19
+ session: SessionInfo;
20
+ }>;
21
+ listSessions(): Promise<{
22
+ sessions: unknown;
23
+ }>;
24
+ getSession(id: string): Promise<{
25
+ session: SessionInfo;
26
+ }>;
27
+ deleteSession(id: string): Promise<{
28
+ deleted: boolean;
29
+ }>;
30
+ renameSession(id: string, title: string): Promise<{
31
+ session: SessionInfo;
32
+ }>;
33
+ getMessages(sessionId: string): Promise<{
34
+ messages: MessageInfo[];
35
+ }>;
36
+ statuses(): Promise<{
37
+ statuses: Record<string, unknown>;
38
+ }>;
39
+ prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
40
+ ack: true;
41
+ }>;
42
+ abort(sessionId: string): Promise<Record<string, never>>;
43
+ agents(): Promise<{
44
+ agents: unknown;
45
+ }>;
46
+ providers(): Promise<ProviderInfo>;
47
+ setAuth(providerId: string, key: string): Promise<Record<string, never>>;
48
+ command(sessionId: string, command: string, args: string): Promise<{
49
+ result: unknown;
50
+ }>;
51
+ revert(sessionId: string, messageId: string): Promise<Record<string, never>>;
52
+ unrevert(sessionId: string): Promise<Record<string, never>>;
53
+ share(sessionId: string): Promise<{
54
+ share: ShareInfo;
55
+ }>;
56
+ permissionReply(sessionId: string, permissionId: string, response: "once" | "always" | "reject"): Promise<Record<string, never>>;
57
+ questionReply(sessionId: string, questionId: string, answers: string[][]): Promise<Record<string, never>>;
58
+ questionReject(sessionId: string, questionId: string): Promise<Record<string, never>>;
59
+ private runSseLoop;
60
+ private sendPromptAsync;
61
+ private reconcileOpenCodeState;
62
+ private refreshBusySessionMessages;
63
+ private refreshSessionsMetadata;
64
+ private refreshPendingPermissions;
65
+ private refreshPendingQuestions;
66
+ private fetchOpenCodeJson;
67
+ private refreshSessionStatuses;
68
+ private fetchSessionStatuses;
69
+ private trackPermissionEvent;
70
+ private readString;
71
+ private asRecord;
72
+ }