@axhub/genie 0.1.6 → 0.1.8

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.
Files changed (37) hide show
  1. package/dist/api-docs.html +351 -909
  2. package/dist/assets/index-CVjMty4a.js +902 -0
  3. package/dist/assets/index-eo5scY_Z.css +32 -0
  4. package/dist/index.html +5 -5
  5. package/dist/manifest.json +2 -2
  6. package/package.json +8 -2
  7. package/server/channels/core/ChannelManager.js +399 -0
  8. package/server/channels/core/PluginManager.js +59 -0
  9. package/server/channels/index.js +3 -0
  10. package/server/channels/plugins/BasePlugin.js +46 -0
  11. package/server/channels/plugins/dingtalk/DingTalkAdapter.js +156 -0
  12. package/server/channels/plugins/dingtalk/DingTalkPlugin.js +592 -0
  13. package/server/channels/plugins/dingtalk/index.js +2 -0
  14. package/server/channels/plugins/lark/LarkAdapter.js +100 -0
  15. package/server/channels/plugins/lark/LarkCards.js +43 -0
  16. package/server/channels/plugins/lark/LarkPlugin.js +260 -0
  17. package/server/channels/runtime/AgentRuntimeAdapter.js +179 -0
  18. package/server/channels/runtime/DingTalkStreamWriter.js +105 -0
  19. package/server/channels/runtime/LarkStreamWriter.js +99 -0
  20. package/server/channels/store/ChannelStore.js +236 -0
  21. package/server/database/db.js +109 -1
  22. package/server/database/init.sql +47 -1
  23. package/server/gemini-cli.js +280 -0
  24. package/server/index.js +230 -11
  25. package/server/openai-codex.js +104 -8
  26. package/server/opencode-cli.js +673 -0
  27. package/server/projects.js +645 -5
  28. package/server/routes/agent.js +40 -12
  29. package/server/routes/channels.js +221 -0
  30. package/server/routes/cli-auth.js +317 -0
  31. package/server/routes/commands.js +29 -3
  32. package/server/routes/git.js +15 -5
  33. package/server/routes/opencode.js +72 -0
  34. package/shared/modelConstants.js +62 -17
  35. package/dist/assets/index-CtRxrKDm.css +0 -32
  36. package/dist/assets/index-OENtErNy.js +0 -1249
  37. package/server/database/auth.db +0 -0
@@ -0,0 +1,236 @@
1
+ import crypto from 'crypto';
2
+
3
+ import { db } from '../../database/db.js';
4
+
5
+ const DEFAULT_CONFIGS = {
6
+ lark: {
7
+ appId: '',
8
+ appSecret: '',
9
+ encryptKey: '',
10
+ verificationToken: '',
11
+ defaultBackend: 'codex',
12
+ defaultModel: '',
13
+ defaultProjectPath: '',
14
+ },
15
+ dingtalk: {
16
+ clientId: '',
17
+ clientSecret: '',
18
+ defaultBackend: 'codex',
19
+ defaultModel: '',
20
+ defaultProjectPath: '',
21
+ },
22
+ };
23
+
24
+ const PLATFORM_TO_PLUGIN_ID = {
25
+ lark: 'lark_default',
26
+ dingtalk: 'dingtalk_default',
27
+ };
28
+
29
+ const PLUGIN_ID_TO_PLATFORM = {
30
+ lark_default: 'lark',
31
+ dingtalk_default: 'dingtalk',
32
+ };
33
+
34
+ function now() {
35
+ return new Date().toISOString();
36
+ }
37
+
38
+ function normalizePlatform(platform) {
39
+ const normalized = String(platform || '').trim().toLowerCase();
40
+ return normalized === 'dingtalk' ? 'dingtalk' : 'lark';
41
+ }
42
+
43
+ function resolvePlatformByPluginId(pluginId) {
44
+ return PLUGIN_ID_TO_PLATFORM[pluginId] || 'lark';
45
+ }
46
+
47
+ function resolvePluginIdByPlatform(platform) {
48
+ return PLATFORM_TO_PLUGIN_ID[normalizePlatform(platform)] || PLATFORM_TO_PLUGIN_ID.lark;
49
+ }
50
+
51
+ function getDefaultConfig(platform) {
52
+ return { ...(DEFAULT_CONFIGS[normalizePlatform(platform)] || DEFAULT_CONFIGS.lark) };
53
+ }
54
+
55
+ function parseConfig(value, platform) {
56
+ const defaults = getDefaultConfig(platform);
57
+ if (!value) {
58
+ return defaults;
59
+ }
60
+
61
+ try {
62
+ const parsed = JSON.parse(value);
63
+ return {
64
+ ...defaults,
65
+ ...(parsed || {}),
66
+ };
67
+ } catch {
68
+ return defaults;
69
+ }
70
+ }
71
+
72
+ export class ChannelStore {
73
+ normalizePlatform(platform) {
74
+ return normalizePlatform(platform);
75
+ }
76
+
77
+ getPluginId(platform) {
78
+ return resolvePluginIdByPlatform(platform);
79
+ }
80
+
81
+ getPluginConfig(pluginId = 'lark_default') {
82
+ const row = db.prepare('SELECT * FROM channel_plugins WHERE id = ?').get(pluginId);
83
+ const platform = resolvePlatformByPluginId(pluginId);
84
+
85
+ if (!row) {
86
+ return {
87
+ id: pluginId,
88
+ type: platform,
89
+ enabled: 0,
90
+ status: 'stopped',
91
+ config: getDefaultConfig(platform),
92
+ };
93
+ }
94
+
95
+ return {
96
+ id: row.id,
97
+ type: row.type || platform,
98
+ enabled: row.enabled,
99
+ status: row.status,
100
+ config: parseConfig(row.config_json, row.type || platform),
101
+ updatedAt: row.updated_at,
102
+ };
103
+ }
104
+
105
+ upsertPluginConfig(pluginId = 'lark_default', patch = {}) {
106
+ const existing = this.getPluginConfig(pluginId);
107
+ const type = resolvePlatformByPluginId(pluginId);
108
+ const nextConfig = {
109
+ ...existing.config,
110
+ ...patch,
111
+ };
112
+
113
+ const updatedAt = now();
114
+
115
+ db.prepare(`
116
+ INSERT INTO channel_plugins (id, type, enabled, status, config_json, updated_at)
117
+ VALUES (?, ?, ?, ?, ?, ?)
118
+ ON CONFLICT(id) DO UPDATE SET
119
+ type = excluded.type,
120
+ enabled = excluded.enabled,
121
+ status = excluded.status,
122
+ config_json = excluded.config_json,
123
+ updated_at = excluded.updated_at
124
+ `).run(
125
+ pluginId,
126
+ type,
127
+ existing.enabled || 0,
128
+ existing.status || 'stopped',
129
+ JSON.stringify(nextConfig),
130
+ updatedAt
131
+ );
132
+
133
+ return this.getPluginConfig(pluginId);
134
+ }
135
+
136
+ updatePluginRuntimeStatus(pluginId = 'lark_default', enabled, status) {
137
+ const existing = this.getPluginConfig(pluginId);
138
+ const type = resolvePlatformByPluginId(pluginId);
139
+ const updatedAt = now();
140
+
141
+ db.prepare(`
142
+ INSERT INTO channel_plugins (id, type, enabled, status, config_json, updated_at)
143
+ VALUES (?, ?, ?, ?, ?, ?)
144
+ ON CONFLICT(id) DO UPDATE SET
145
+ enabled = excluded.enabled,
146
+ status = excluded.status,
147
+ updated_at = excluded.updated_at
148
+ `).run(
149
+ pluginId,
150
+ type,
151
+ enabled ? 1 : 0,
152
+ status,
153
+ JSON.stringify(existing.config),
154
+ updatedAt
155
+ );
156
+
157
+ return this.getPluginConfig(pluginId);
158
+ }
159
+
160
+ listAllowedUsers(platform = 'lark') {
161
+ const normalizedPlatform = normalizePlatform(platform);
162
+ return db.prepare(`
163
+ SELECT id, platform_type AS platformType, user_id AS userId, display_name AS displayName, note, is_active AS isActive, created_at AS createdAt, updated_at AS updatedAt
164
+ FROM channel_allowed_users
165
+ WHERE platform_type = ?
166
+ ORDER BY created_at DESC
167
+ `).all(normalizedPlatform);
168
+ }
169
+
170
+ addAllowedUser(platform = 'lark', { userId, displayName = null, note = null }) {
171
+ const normalizedPlatform = normalizePlatform(platform);
172
+ const trimmedUserId = String(userId || '').trim();
173
+ if (!trimmedUserId) {
174
+ throw new Error('userId is required');
175
+ }
176
+
177
+ const current = now();
178
+
179
+ db.prepare(`
180
+ INSERT INTO channel_allowed_users (platform_type, user_id, display_name, note, is_active, created_at, updated_at)
181
+ VALUES (?, ?, ?, ?, 1, ?, ?)
182
+ `).run(normalizedPlatform, trimmedUserId, displayName ? String(displayName).trim() : null, note ? String(note).trim() : null, current, current);
183
+
184
+ return db.prepare(`
185
+ SELECT id, platform_type AS platformType, user_id AS userId, display_name AS displayName, note, is_active AS isActive, created_at AS createdAt, updated_at AS updatedAt
186
+ FROM channel_allowed_users
187
+ WHERE platform_type = ? AND user_id = ?
188
+ `).get(normalizedPlatform, trimmedUserId);
189
+ }
190
+
191
+ removeAllowedUser(platform = 'lark', id) {
192
+ const normalizedPlatform = normalizePlatform(platform);
193
+ const result = db.prepare('DELETE FROM channel_allowed_users WHERE platform_type = ? AND id = ?').run(normalizedPlatform, id);
194
+ return result.changes > 0;
195
+ }
196
+
197
+ toggleAllowedUser(platform = 'lark', id, isActive) {
198
+ const normalizedPlatform = normalizePlatform(platform);
199
+ const result = db.prepare('UPDATE channel_allowed_users SET is_active = ?, updated_at = ? WHERE platform_type = ? AND id = ?').run(isActive ? 1 : 0, now(), normalizedPlatform, id);
200
+ return result.changes > 0;
201
+ }
202
+
203
+ isAllowedUser(platform = 'lark', userId) {
204
+ const normalizedPlatform = normalizePlatform(platform);
205
+ if (!userId) return false;
206
+ const row = db.prepare('SELECT id FROM channel_allowed_users WHERE platform_type = ? AND user_id = ? AND is_active = 1').get(normalizedPlatform, String(userId));
207
+ return !!row;
208
+ }
209
+
210
+ getChannelSession({ platform = 'lark', userId, chatId, backend }) {
211
+ const normalizedPlatform = normalizePlatform(platform);
212
+ return db.prepare(`
213
+ SELECT id, platform_type AS platformType, user_id AS userId, chat_id AS chatId, backend, provider_session_id AS providerSessionId, project_path AS projectPath, updated_at AS updatedAt
214
+ FROM channel_sessions
215
+ WHERE platform_type = ? AND user_id = ? AND chat_id = ? AND backend = ?
216
+ LIMIT 1
217
+ `).get(normalizedPlatform, userId, chatId, backend) || null;
218
+ }
219
+
220
+ upsertChannelSession({ platform = 'lark', userId, chatId, backend, providerSessionId, projectPath }) {
221
+ const normalizedPlatform = normalizePlatform(platform);
222
+ const id = crypto.randomUUID();
223
+ const updatedAt = now();
224
+
225
+ db.prepare(`
226
+ INSERT INTO channel_sessions (id, platform_type, user_id, chat_id, backend, provider_session_id, project_path, updated_at)
227
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
228
+ ON CONFLICT(platform_type, user_id, chat_id, backend) DO UPDATE SET
229
+ provider_session_id = excluded.provider_session_id,
230
+ project_path = excluded.project_path,
231
+ updated_at = excluded.updated_at
232
+ `).run(id, normalizedPlatform, userId, chatId, backend, providerSessionId || null, projectPath || null, updatedAt);
233
+
234
+ return this.getChannelSession({ platform: normalizedPlatform, userId, chatId, backend });
235
+ }
236
+ }
@@ -55,6 +55,111 @@ if (process.env.DATABASE_PATH) {
55
55
  console.log(c.dim('═'.repeat(60)));
56
56
  console.log('');
57
57
 
58
+ const tableExists = (tableName) => {
59
+ const row = db
60
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
61
+ .get(tableName);
62
+ return !!row;
63
+ };
64
+
65
+ const getTableColumns = (tableName) => {
66
+ if (!tableExists(tableName)) {
67
+ return [];
68
+ }
69
+ return db.prepare(`PRAGMA table_info(${tableName})`).all().map((col) => col.name);
70
+ };
71
+
72
+ const hasIndex = (indexName) => {
73
+ const row = db
74
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?")
75
+ .get(indexName);
76
+ return !!row;
77
+ };
78
+
79
+ const ensureChannelAllowedUsersSchema = () => {
80
+ if (!tableExists('channel_allowed_users')) {
81
+ return;
82
+ }
83
+
84
+ const columns = getTableColumns('channel_allowed_users');
85
+ const tableSqlRow = db
86
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'channel_allowed_users'")
87
+ .get();
88
+ const tableSql = String(tableSqlRow?.sql || '');
89
+ const hasLegacyUserIdUnique = /user_id\s+TEXT\s+UNIQUE/i.test(tableSql);
90
+ const needsRebuild = !columns.includes('platform_type') || hasLegacyUserIdUnique;
91
+
92
+ if (needsRebuild) {
93
+ console.log('Running migration: Rebuilding channel_allowed_users with platform isolation');
94
+ const beforeCount = db.prepare('SELECT COUNT(*) AS count FROM channel_allowed_users').get()?.count || 0;
95
+
96
+ db.transaction(() => {
97
+ db.exec('ALTER TABLE channel_allowed_users RENAME TO channel_allowed_users_old');
98
+ db.exec(`
99
+ CREATE TABLE channel_allowed_users (
100
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
101
+ platform_type TEXT NOT NULL DEFAULT 'lark',
102
+ user_id TEXT NOT NULL,
103
+ display_name TEXT,
104
+ note TEXT,
105
+ is_active BOOLEAN DEFAULT 1,
106
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
107
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
108
+ )
109
+ `);
110
+
111
+ const oldColumns = getTableColumns('channel_allowed_users_old');
112
+ const hasOldPlatformType = oldColumns.includes('platform_type');
113
+
114
+ if (hasOldPlatformType) {
115
+ db.exec(`
116
+ INSERT INTO channel_allowed_users (id, platform_type, user_id, display_name, note, is_active, created_at, updated_at)
117
+ SELECT id, COALESCE(platform_type, 'lark'), user_id, display_name, note, is_active, created_at, updated_at
118
+ FROM channel_allowed_users_old
119
+ `);
120
+ } else {
121
+ db.exec(`
122
+ INSERT INTO channel_allowed_users (id, platform_type, user_id, display_name, note, is_active, created_at, updated_at)
123
+ SELECT id, 'lark', user_id, display_name, note, is_active, created_at, updated_at
124
+ FROM channel_allowed_users_old
125
+ `);
126
+ }
127
+
128
+ db.exec('DROP TABLE channel_allowed_users_old');
129
+ })();
130
+
131
+ const afterCount = db.prepare('SELECT COUNT(*) AS count FROM channel_allowed_users').get()?.count || 0;
132
+ console.log(`channel_allowed_users rows migrated: ${beforeCount} -> ${afterCount}`);
133
+ }
134
+
135
+ db.exec("UPDATE channel_allowed_users SET platform_type = 'lark' WHERE platform_type IS NULL OR TRIM(platform_type) = ''");
136
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_user ON channel_allowed_users(platform_type, user_id)');
137
+ db.exec('CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_user_id ON channel_allowed_users(user_id)');
138
+ db.exec('CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_type ON channel_allowed_users(platform_type)');
139
+ db.exec('CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_active ON channel_allowed_users(is_active)');
140
+ };
141
+
142
+ const ensureChannelSessionsSchema = () => {
143
+ if (!tableExists('channel_sessions')) {
144
+ return;
145
+ }
146
+
147
+ const columns = getTableColumns('channel_sessions');
148
+ if (!columns.includes('platform_type')) {
149
+ console.log('Running migration: Adding platform_type to channel_sessions');
150
+ db.exec("ALTER TABLE channel_sessions ADD COLUMN platform_type TEXT NOT NULL DEFAULT 'lark'");
151
+ }
152
+
153
+ db.exec("UPDATE channel_sessions SET platform_type = 'lark' WHERE platform_type IS NULL OR TRIM(platform_type) = ''");
154
+
155
+ if (hasIndex('idx_channel_sessions_unique')) {
156
+ db.exec('DROP INDEX IF EXISTS idx_channel_sessions_unique');
157
+ }
158
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_sessions_unique ON channel_sessions(platform_type, user_id, chat_id, backend)');
159
+ db.exec('CREATE INDEX IF NOT EXISTS idx_channel_sessions_platform_type ON channel_sessions(platform_type)');
160
+ db.exec('CREATE INDEX IF NOT EXISTS idx_channel_sessions_user_id ON channel_sessions(user_id)');
161
+ };
162
+
58
163
  const runMigrations = () => {
59
164
  try {
60
165
  const tableInfo = db.prepare("PRAGMA table_info(users)").all();
@@ -75,6 +180,9 @@ const runMigrations = () => {
75
180
  db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
76
181
  }
77
182
 
183
+ ensureChannelAllowedUsersSchema();
184
+ ensureChannelSessionsSchema();
185
+
78
186
  console.log('Database migrations completed successfully');
79
187
  } catch (error) {
80
188
  console.error('Error running migrations:', error.message);
@@ -358,4 +466,4 @@ export {
358
466
  apiKeysDb,
359
467
  credentialsDb,
360
468
  githubTokensDb // Backward compatibility
361
- };
469
+ };
@@ -49,4 +49,50 @@ CREATE TABLE IF NOT EXISTS user_credentials (
49
49
 
50
50
  CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
51
51
  CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
52
- CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
52
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
53
+
54
+ -- Channel plugins table (Lark and future channels)
55
+ CREATE TABLE IF NOT EXISTS channel_plugins (
56
+ id TEXT PRIMARY KEY,
57
+ type TEXT NOT NULL,
58
+ enabled BOOLEAN DEFAULT 0,
59
+ status TEXT DEFAULT 'stopped',
60
+ config_json TEXT NOT NULL,
61
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
62
+ );
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_channel_plugins_type ON channel_plugins(type);
65
+ CREATE INDEX IF NOT EXISTS idx_channel_plugins_enabled ON channel_plugins(enabled);
66
+
67
+ -- Allowed channel users (whitelist by platform user ID)
68
+ CREATE TABLE IF NOT EXISTS channel_allowed_users (
69
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
70
+ platform_type TEXT NOT NULL DEFAULT 'lark',
71
+ user_id TEXT NOT NULL,
72
+ display_name TEXT,
73
+ note TEXT,
74
+ is_active BOOLEAN DEFAULT 1,
75
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
76
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
77
+ );
78
+
79
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_user ON channel_allowed_users(platform_type, user_id);
80
+ CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_user_id ON channel_allowed_users(user_id);
81
+ CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_platform_type ON channel_allowed_users(platform_type);
82
+ CREATE INDEX IF NOT EXISTS idx_channel_allowed_users_active ON channel_allowed_users(is_active);
83
+
84
+ -- Channel session bindings
85
+ CREATE TABLE IF NOT EXISTS channel_sessions (
86
+ id TEXT PRIMARY KEY,
87
+ platform_type TEXT NOT NULL DEFAULT 'lark',
88
+ user_id TEXT NOT NULL,
89
+ chat_id TEXT NOT NULL,
90
+ backend TEXT NOT NULL,
91
+ provider_session_id TEXT,
92
+ project_path TEXT,
93
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
94
+ );
95
+
96
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_sessions_unique ON channel_sessions(platform_type, user_id, chat_id, backend);
97
+ CREATE INDEX IF NOT EXISTS idx_channel_sessions_platform_type ON channel_sessions(platform_type);
98
+ CREATE INDEX IF NOT EXISTS idx_channel_sessions_user_id ON channel_sessions(user_id);
@@ -0,0 +1,280 @@
1
+ import { spawn } from 'child_process';
2
+ import crossSpawn from 'cross-spawn';
3
+
4
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
5
+
6
+ const activeGeminiProcesses = new Map();
7
+
8
+ function collectTextChunks(payload) {
9
+ if (!payload) return [];
10
+ if (typeof payload === 'string') return payload.trim() ? [payload] : [];
11
+
12
+ const chunks = [];
13
+
14
+ if (Array.isArray(payload)) {
15
+ payload.forEach(item => {
16
+ chunks.push(...collectTextChunks(item));
17
+ });
18
+ return chunks;
19
+ }
20
+
21
+ if (typeof payload !== 'object') {
22
+ return chunks;
23
+ }
24
+
25
+ const directKeys = ['text', 'response', 'content', 'message', 'delta'];
26
+ for (const key of directKeys) {
27
+ if (payload[key] !== undefined) {
28
+ chunks.push(...collectTextChunks(payload[key]));
29
+ }
30
+ }
31
+
32
+ if (Array.isArray(payload.parts)) {
33
+ payload.parts.forEach(part => {
34
+ chunks.push(...collectTextChunks(part));
35
+ });
36
+ }
37
+
38
+ if (payload.data && typeof payload.data === 'object') {
39
+ chunks.push(...collectTextChunks(payload.data));
40
+ }
41
+
42
+ return chunks;
43
+ }
44
+
45
+ function getEventRole(event) {
46
+ return (
47
+ event?.role ||
48
+ event?.author ||
49
+ event?.sender ||
50
+ event?.message?.role ||
51
+ event?.data?.role ||
52
+ event?.content?.role ||
53
+ null
54
+ );
55
+ }
56
+
57
+ function extractAssistantTextChunks(event, command) {
58
+ const role = String(getEventRole(event) || '').toLowerCase();
59
+ if (role === 'user') return [];
60
+
61
+ const eventType = String(event?.type || event?.event || event?.kind || '').toLowerCase();
62
+ const hasAssistantPayload = !!(event?.response || event?.candidates || event?.delta || event?.text || event?.content || event?.message);
63
+ if (eventType.includes('prompt') && !hasAssistantPayload) {
64
+ return [];
65
+ }
66
+
67
+ const normalizedPrompt = String(command || '').trim();
68
+ return collectTextChunks(event).filter((text) => {
69
+ const trimmed = String(text || '').trim();
70
+ if (!trimmed) return false;
71
+ if (normalizedPrompt && trimmed === normalizedPrompt) return false;
72
+ return true;
73
+ });
74
+ }
75
+
76
+ function parseGeminiJsonLine(line) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed) return null;
79
+
80
+ const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed;
81
+ if (!payload) return null;
82
+
83
+ return JSON.parse(payload);
84
+ }
85
+
86
+ function emitTextChunks(ws, sessionId, chunks) {
87
+ chunks.forEach((text) => {
88
+ if (!text) return;
89
+ ws.send({
90
+ type: 'claude-response',
91
+ data: {
92
+ type: 'content_block_delta',
93
+ delta: {
94
+ type: 'text_delta',
95
+ text
96
+ }
97
+ },
98
+ provider: 'gemini',
99
+ sessionId
100
+ });
101
+ });
102
+ }
103
+
104
+ async function queryGemini(command, options = {}, ws) {
105
+ return new Promise((resolve, reject) => {
106
+ const { sessionId, cwd, projectPath, resume, model, permissionMode } = options;
107
+ let capturedSessionId = sessionId;
108
+ let sentSessionCreated = false;
109
+
110
+ const args = ['-y', '@google/gemini-cli'];
111
+
112
+ if (sessionId && (resume || !command || !command.trim())) {
113
+ args.push('--resume', sessionId);
114
+ }
115
+
116
+ if (command && command.trim()) {
117
+ args.push('--prompt', command);
118
+ args.push('--output-format', 'stream-json');
119
+ }
120
+
121
+ if (model) {
122
+ args.push('--model', model);
123
+ }
124
+
125
+ if (permissionMode === 'bypassPermissions' || permissionMode === 'acceptEdits') {
126
+ args.push('--yolo');
127
+ }
128
+
129
+ const workingDir = cwd || projectPath || process.cwd();
130
+
131
+ const geminiProcess = spawnFunction('npx', args, {
132
+ cwd: workingDir,
133
+ stdio: ['pipe', 'pipe', 'pipe'],
134
+ env: {
135
+ ...process.env,
136
+ GEMINI_NONINTERACTIVE: '1'
137
+ }
138
+ });
139
+
140
+ const processKey = capturedSessionId || Date.now().toString();
141
+ let processRegistryKey = processKey;
142
+ activeGeminiProcesses.set(processRegistryKey, geminiProcess);
143
+
144
+ const finalizeSessionId = () => capturedSessionId || sessionId || null;
145
+
146
+ const handleJsonEvent = (event) => {
147
+ const incomingSessionId = event?.session_id || event?.sessionId || event?.data?.session_id || event?.data?.sessionId;
148
+ if (incomingSessionId && !capturedSessionId) {
149
+ capturedSessionId = incomingSessionId;
150
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
151
+ ws.setSessionId(capturedSessionId);
152
+ }
153
+
154
+ if (processRegistryKey !== capturedSessionId) {
155
+ activeGeminiProcesses.delete(processRegistryKey);
156
+ activeGeminiProcesses.set(capturedSessionId, geminiProcess);
157
+ processRegistryKey = capturedSessionId;
158
+ }
159
+
160
+ if (!sessionId && !sentSessionCreated) {
161
+ sentSessionCreated = true;
162
+ ws.send({
163
+ type: 'session-created',
164
+ sessionId: capturedSessionId,
165
+ provider: 'gemini'
166
+ });
167
+ }
168
+ }
169
+
170
+ const type = event?.type || event?.event || event?.kind;
171
+ if (type === 'result' || type === 'done' || type === 'complete') {
172
+ ws.send({
173
+ type: 'claude-response',
174
+ data: { type: 'content_block_stop' },
175
+ provider: 'gemini',
176
+ sessionId: finalizeSessionId()
177
+ });
178
+
179
+ ws.send({
180
+ type: 'gemini-result',
181
+ data: event,
182
+ sessionId: finalizeSessionId()
183
+ });
184
+ return;
185
+ }
186
+
187
+ const textChunks = extractAssistantTextChunks(event, command);
188
+ emitTextChunks(ws, finalizeSessionId(), textChunks);
189
+ };
190
+
191
+ let stdoutBuffer = '';
192
+ geminiProcess.stdout.on('data', (data) => {
193
+ stdoutBuffer += data.toString();
194
+ const lines = stdoutBuffer.split('\n');
195
+ stdoutBuffer = lines.pop() || '';
196
+
197
+ for (const line of lines) {
198
+ try {
199
+ const event = parseGeminiJsonLine(line);
200
+ if (!event) continue;
201
+ handleJsonEvent(event);
202
+ } catch {}
203
+ }
204
+ });
205
+
206
+ geminiProcess.stderr.on('data', (data) => {
207
+ ws.send({
208
+ type: 'claude-error',
209
+ error: data.toString(),
210
+ sessionId: finalizeSessionId()
211
+ });
212
+ });
213
+
214
+ geminiProcess.on('close', (code) => {
215
+ if (stdoutBuffer.trim()) {
216
+ try {
217
+ const finalEvent = parseGeminiJsonLine(stdoutBuffer);
218
+ if (finalEvent) {
219
+ handleJsonEvent(finalEvent);
220
+ }
221
+ } catch {}
222
+ }
223
+
224
+ activeGeminiProcesses.delete(processRegistryKey);
225
+
226
+ ws.send({
227
+ type: 'claude-complete',
228
+ sessionId: finalizeSessionId(),
229
+ provider: 'gemini',
230
+ exitCode: code,
231
+ isNewSession: !sessionId && !!command
232
+ });
233
+
234
+ if (code === 0) {
235
+ resolve();
236
+ } else {
237
+ reject(new Error(`Gemini CLI exited with code ${code}`));
238
+ }
239
+ });
240
+
241
+ geminiProcess.on('error', (error) => {
242
+ activeGeminiProcesses.delete(processRegistryKey);
243
+
244
+ ws.send({
245
+ type: 'claude-error',
246
+ error: error.message,
247
+ sessionId: finalizeSessionId()
248
+ });
249
+
250
+ reject(error);
251
+ });
252
+
253
+ geminiProcess.stdin.end();
254
+ });
255
+ }
256
+
257
+ function abortGeminiSession(sessionId) {
258
+ const process = activeGeminiProcesses.get(sessionId);
259
+ if (process) {
260
+ process.kill('SIGTERM');
261
+ activeGeminiProcesses.delete(sessionId);
262
+ return true;
263
+ }
264
+ return false;
265
+ }
266
+
267
+ function isGeminiSessionActive(sessionId) {
268
+ return activeGeminiProcesses.has(sessionId);
269
+ }
270
+
271
+ function getActiveGeminiSessions() {
272
+ return Array.from(activeGeminiProcesses.keys());
273
+ }
274
+
275
+ export {
276
+ queryGemini,
277
+ abortGeminiSession,
278
+ isGeminiSessionActive,
279
+ getActiveGeminiSessions
280
+ };