@yvhitxcel/opencode-remote 0.16.3 → 0.18.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.
@@ -1,403 +0,0 @@
1
- // Session manager - per-conversation state with disk persistence
2
- import { homedir } from 'os';
3
- import { join } from 'path';
4
- import { mkdir, readFile, writeFile, unlink } from 'fs/promises';
5
- import { existsSync, writeFileSync } from 'fs';
6
-
7
- const SESSIONS_DIR = join(homedir(), '.opencode-remote', 'sessions');
8
- const DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes
9
- const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
10
-
11
- class SessionManager {
12
- sessions = new Map();
13
- cleanupTimer;
14
-
15
- async start() {
16
- await mkdir(SESSIONS_DIR, { recursive: true });
17
- this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
18
- console.log(`Session manager started (sessions: ${SESSIONS_DIR})`);
19
- }
20
-
21
- stop() {
22
- if (this.cleanupTimer) {
23
- clearInterval(this.cleanupTimer);
24
- }
25
- }
26
-
27
- async getOrCreateSession(platform, channelId, threadId, agent = 'opencode') {
28
- const key = `${platform}:${channelId}:${threadId}`;
29
- const now = new Date();
30
-
31
- let session = this.sessions.get(key);
32
- if (session) {
33
- if (now.getTime() - session.lastActivity.getTime() > session.ttl) {
34
- session = undefined;
35
- } else {
36
- session.lastActivity = now;
37
- await this.saveSession(key, session);
38
- return session;
39
- }
40
- }
41
-
42
- session = await this.loadSession(key);
43
- if (session && now.getTime() - session.lastActivity.getTime() <= session.ttl) {
44
- session.lastActivity = now;
45
- this.sessions.set(key, session);
46
- await this.saveSession(key, session);
47
- return session;
48
- }
49
-
50
- session = {
51
- id: `${platform}-${channelId}-${threadId}-${Date.now()}`,
52
- channelId,
53
- threadId,
54
- platform,
55
- agent,
56
- createdAt: now,
57
- lastActivity: now,
58
- ttl: DEFAULT_TTL,
59
- messages: [],
60
- opencodeSessionId: undefined,
61
- pendingApprovals: [],
62
- viewMode: 'phone',
63
- projectDir: undefined,
64
- taskStartTime: null,
65
- currentTool: null,
66
- modifiedFiles: [],
67
- };
68
-
69
- this.sessions.set(key, session);
70
- await this.saveSession(key, session);
71
- return session;
72
- }
73
-
74
- async getExistingSession(platform, channelId, threadId) {
75
- const key = `${platform}:${channelId}:${threadId}`;
76
- const now = new Date();
77
-
78
- let session = this.sessions.get(key);
79
- if (session) {
80
- if (now.getTime() - session.lastActivity.getTime() > session.ttl) {
81
- return undefined;
82
- }
83
- return session;
84
- }
85
-
86
- session = await this.loadSession(key);
87
- if (session && now.getTime() - session.lastActivity.getTime() <= session.ttl) {
88
- this.sessions.set(key, session);
89
- return session;
90
- }
91
- return undefined;
92
- }
93
-
94
- async switchAgent(platform, channelId, threadId, newAgent) {
95
- const key = `${platform}:${channelId}:${threadId}`;
96
- const existing = this.sessions.get(key) || await this.loadSession(key);
97
- const now = new Date();
98
-
99
- const session = {
100
- id: `${platform}-${channelId}-${threadId}-${Date.now()}`,
101
- channelId,
102
- threadId,
103
- platform,
104
- agent: newAgent,
105
- createdAt: existing?.createdAt || now,
106
- lastActivity: now,
107
- ttl: DEFAULT_TTL,
108
- messages: existing?.messages || [],
109
- opencodeSessionId: existing?.opencodeSessionId,
110
- pendingApprovals: existing?.pendingApprovals || [],
111
- viewMode: existing?.viewMode || 'phone',
112
- };
113
-
114
- this.sessions.set(key, session);
115
- await this.saveSession(key, session);
116
- return session;
117
- }
118
-
119
- async addMessage(platform, channelId, threadId, message) {
120
- const key = `${platform}:${channelId}:${threadId}`;
121
- const session = this.sessions.get(key) || await this.loadSession(key);
122
- if (session) {
123
- session.messages.push(message);
124
- session.lastActivity = new Date();
125
- this.sessions.set(key, session);
126
- await this.saveSession(key, session);
127
- }
128
- }
129
-
130
- async resetConversation(platform, channelId, threadId) {
131
- const key = `${platform}:${channelId}:${threadId}`;
132
- const session = this.sessions.get(key) || await this.loadSession(key);
133
- if (session) {
134
- session.messages = [];
135
- session.lastActivity = new Date();
136
- session.id = `${platform}-${channelId}-${threadId}-${Date.now()}`;
137
- session.opencodeSessionId = undefined;
138
- session.pendingApprovals = [];
139
- this.sessions.set(key, session);
140
- await this.saveSession(key, session);
141
- return session;
142
- }
143
- return undefined;
144
- }
145
-
146
- async getSessionWithHistory(platform, channelId, threadId) {
147
- const session = await this.getExistingSession(platform, channelId, threadId);
148
- if (session) {
149
- return { session, messages: session.messages };
150
- }
151
- return undefined;
152
- }
153
-
154
- async saveSession(key, session) {
155
- const filePath = join(SESSIONS_DIR, `${key.replace(/:/g, '-')}.json`);
156
- try {
157
- await writeFile(filePath, JSON.stringify(session, null, 2));
158
- } catch {
159
- // Ignore save errors - in-memory still works
160
- }
161
- }
162
-
163
- async loadSession(key) {
164
- const filePath = join(SESSIONS_DIR, `${key.replace(/:/g, '-')}.json`);
165
- try {
166
- const data = await readFile(filePath, 'utf-8');
167
- const session = JSON.parse(data);
168
- session.createdAt = new Date(session.createdAt);
169
- session.lastActivity = new Date(session.lastActivity);
170
- if (session.messages) {
171
- session.messages = session.messages.map(msg => ({
172
- ...msg,
173
- timestamp: new Date(msg.timestamp)
174
- }));
175
- } else {
176
- session.messages = [];
177
- }
178
- // Ensure new fields have defaults
179
- if (session.taskStartTime === undefined) session.taskStartTime = null;
180
- if (session.currentTool === undefined) session.currentTool = null;
181
- if (!session.modifiedFiles) session.modifiedFiles = [];
182
- if (session.projectDir === undefined) session.projectDir = undefined;
183
- if (session.ttl === undefined) session.ttl = DEFAULT_TTL;
184
- if (session.opencodeSessionId === undefined) session.opencodeSessionId = undefined;
185
- // Don't persist agent switch across restarts; default to opencode
186
- session.currentAgent = undefined;
187
- return session;
188
- } catch {
189
- return undefined;
190
- }
191
- }
192
-
193
- async cleanup() {
194
- const now = Date.now();
195
- for (const [key, session] of this.sessions.entries()) {
196
- if (now - session.lastActivity.getTime() > session.ttl) {
197
- this.sessions.delete(key);
198
- const filePath = join(SESSIONS_DIR, `${key.replace(/:/g, '-')}.json`);
199
- try {
200
- await unlink(filePath);
201
- } catch {
202
- // Ignore delete errors
203
- }
204
- }
205
- }
206
- }
207
- }
208
-
209
- export const sessionManager = new SessionManager();
210
-
211
- // Legacy exports for backward compatibility
212
- const sessions = new Map();
213
-
214
- export function _getSessionsMap() {
215
- return sessions;
216
- }
217
-
218
- export function initSessionManager(config) {
219
- if (sessionManager.cleanupTimer) {
220
- clearInterval(sessionManager.cleanupTimer);
221
- }
222
- sessionManager.cleanupTimer = setInterval(() => sessionManager.cleanup(), config.cleanupIntervalMs || CLEANUP_INTERVAL);
223
- console.log(`Session manager initialized`);
224
- }
225
-
226
- export async function getOrCreateSession(threadId, platform) {
227
- const session = await sessionManager.getOrCreateSession(platform, threadId, threadId, 'opencode');
228
- sessions.set(threadId, session);
229
- return session;
230
- }
231
-
232
- export function getSession(threadId) {
233
- // Try legacy first, then sessionManager
234
- const legacy = sessions.get(threadId);
235
- if (legacy) return legacy;
236
- return sessionManager.sessions.get(threadId);
237
- }
238
-
239
- export function updateSession(threadId, updates) {
240
- const session = sessions.get(threadId) || sessionManager.sessions.get(threadId);
241
- if (!session) return undefined;
242
- Object.assign(session, updates, { lastActivity: Date.now() });
243
- return session;
244
- }
245
-
246
- export function deleteSession(threadId) {
247
- sessions.delete(threadId);
248
- sessionManager.sessions.delete(threadId);
249
- return true;
250
- }
251
-
252
- export function getAllSessions() {
253
- return Array.from(sessions.values());
254
- }
255
-
256
- export function getSessionCount() {
257
- return sessions.size;
258
- }
259
-
260
- export async function saveSessionCommandHistory(threadId, commandHistory) {
261
- const key = `weixin:${threadId}:${threadId}`;
262
-
263
- // Load existing session from disk
264
- let session = await sessionManager.loadSession(key);
265
-
266
- if (!session) {
267
- // Create new session
268
- session = {
269
- id: threadId,
270
- channelId: threadId,
271
- threadId: threadId,
272
- platform: 'weixin',
273
- agent: 'opencode',
274
- createdAt: new Date(),
275
- lastActivity: new Date(),
276
- ttl: 1800000,
277
- messages: [],
278
- pendingApprovals: [],
279
- viewMode: 'phone',
280
- commandHistory: [],
281
- };
282
- }
283
-
284
- // Merge: keep existing history and add new items
285
- const existing = session.commandHistory || [];
286
- const existingSet = new Set(existing);
287
- for (const item of commandHistory) {
288
- if (!existingSet.has(item)) {
289
- existing.push(item);
290
- existingSet.add(item);
291
- }
292
- }
293
- // Keep only last 50
294
- if (existing.length > 50) {
295
- existing.splice(0, existing.length - 50);
296
- }
297
-
298
- session.commandHistory = existing;
299
- session.lastActivity = new Date();
300
-
301
- // Update in-memory maps
302
- sessions.set(threadId, session);
303
- sessionManager.sessions.set(key, session);
304
-
305
- // Save to disk
306
- await sessionManager.saveSession(key, session);
307
- }
308
-
309
- export async function getSessionCommandHistory(threadId) {
310
- const key = `weixin:${threadId}:${threadId}`;
311
-
312
- // Try memory first
313
- let session = sessions.get(threadId) || sessionManager.sessions.get(key);
314
-
315
- // Then try disk and UPDATE memory
316
- if (!session) {
317
- session = await sessionManager.loadSession(key);
318
- if (session) {
319
- sessions.set(threadId, session);
320
- sessionManager.sessions.set(key, session);
321
- }
322
- }
323
-
324
- const history = session?.commandHistory || [];
325
- return history;
326
- }
327
-
328
- // Legacy exports for backward compatibility
329
- export function loadSessionMapping() {
330
- const mappingFile = join(CONFIG_DIR, 'session-mapping.json');
331
- try {
332
- if (!existsSync(mappingFile)) {
333
- return {};
334
- }
335
- const raw = readFileSync(mappingFile, 'utf-8');
336
- return JSON.parse(raw);
337
- } catch {
338
- return {};
339
- }
340
- }
341
-
342
- export function saveSessionMapping() {
343
- try {
344
- ensureConfigDir();
345
- const mapping = {};
346
- const processedKeys = new Set();
347
-
348
- // Process sessions from both sessionManager and legacy sessions
349
- const allSessions = new Map([...sessionManager.sessions.entries(), ...sessions.entries()]);
350
-
351
- for (const [key, session] of allSessions.entries()) {
352
- if (processedKeys.has(key)) continue;
353
- processedKeys.add(key);
354
-
355
- if (session.opencodeSessionId) {
356
- // Extract threadId from key (format: platform:channelId:threadId)
357
- const parts = key.split(':');
358
- const threadId = parts[parts.length - 1];
359
-
360
- mapping[threadId] = {
361
- opencodeSessionId: session.opencodeSessionId,
362
- lastActivity: session.lastActivity,
363
- platform: session.platform,
364
- viewMode: session.viewMode || 'phone',
365
- currentViewingFile: session.currentViewingFile,
366
- currentViewingMd: session.currentViewingMd,
367
- commandHistory: session.commandHistory || [],
368
- taskStartTime: session.taskStartTime || null,
369
- currentTool: session.currentTool || null,
370
- modifiedFiles: session.modifiedFiles || [],
371
- projectDir: session.projectDir || null,
372
- modelOverride: session.modelOverride || null,
373
- };
374
-
375
- // Also save full session to disk via sessionManager
376
- sessionManager.saveSession(key, session).catch(() => {});
377
- }
378
- }
379
-
380
- writeFileSync(join(CONFIG_DIR, 'session-mapping.json'), JSON.stringify(mapping, null, 2), 'utf-8');
381
- } catch (error) {
382
- console.warn('Failed to save session mapping:', error.message);
383
- }
384
- }
385
-
386
- export function getThreadsBySessionIdFromMapping(opencodeSessionId) {
387
- const mapping = loadSessionMapping();
388
- const threads = [];
389
- for (const [threadId, data] of Object.entries(mapping)) {
390
- if (data.opencodeSessionId === opencodeSessionId) {
391
- threads.push(threadId);
392
- }
393
- }
394
- return threads;
395
- }
396
-
397
- const CONFIG_DIR = join(homedir(), '.opencode-remote');
398
-
399
- function ensureConfigDir() {
400
- if (!existsSync(CONFIG_DIR)) {
401
- mkdirSync(CONFIG_DIR, { recursive: true });
402
- }
403
- }