@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.
- package/LICENSE +21 -0
- package/README.md +70 -1
- package/dist/autonomous/decisions.js +73 -0
- package/dist/autonomous/index.js +141 -0
- package/dist/cli.js +121 -19
- package/dist/core/adapter.js +12 -0
- package/dist/core/agent-registry.js +77 -0
- package/dist/core/crypto.js +80 -0
- package/dist/core/git-push.js +143 -0
- package/dist/core/handler.js +293 -0
- package/dist/core/log.js +92 -0
- package/dist/core/lru.js +98 -0
- package/dist/core/notifications.js +2 -2
- package/dist/core/qiniu.js +2 -2
- package/dist/core/retry.js +46 -0
- package/dist/core/router.js +62 -296
- package/dist/core/state.js +190 -0
- package/dist/core/stats.js +115 -0
- package/dist/feishu/adapter.js +0 -1
- package/dist/feishu/bot.js +4 -4
- package/dist/feishu/commands.js +28 -397
- package/dist/feishu/handler.js +9 -369
- package/dist/opencode/client.js +172 -168
- package/dist/patch_spawn.js +1 -0
- package/dist/plugins/agents/claude-code/index.js +59 -47
- package/dist/plugins/agents/codex/index.js +32 -6
- package/dist/plugins/agents/copilot/index.js +32 -6
- package/dist/plugins/agents/opencode/index.js +38 -12
- package/dist/telegram/adapter.js +22 -9
- package/dist/telegram/bot.js +1 -6
- package/dist/weixin/adapter.js +37 -15
- package/dist/weixin/api.js +47 -19
- package/dist/weixin/bot.js +172 -83
- package/dist/weixin/commands.js +476 -597
- package/dist/weixin/handler.js +27 -541
- package/dist/weixin/user-adapter-map.js +12 -0
- package/package.json +5 -3
- package/dist/core/session.js +0 -403
package/dist/core/session.js
DELETED
|
@@ -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
|
-
}
|