@gricha/perry 0.0.1
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 +153 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/router.js +1017 -0
- package/dist/agent/run.js +182 -0
- package/dist/agent/static.js +58 -0
- package/dist/agent/systemd.js +229 -0
- package/dist/agent/web/assets/index-9t2sFIJM.js +101 -0
- package/dist/agent/web/assets/index-CCFpTruF.css +1 -0
- package/dist/agent/web/index.html +14 -0
- package/dist/agent/web/vite.svg +1 -0
- package/dist/chat/handler.js +174 -0
- package/dist/chat/host-handler.js +170 -0
- package/dist/chat/host-opencode-handler.js +169 -0
- package/dist/chat/index.js +2 -0
- package/dist/chat/opencode-handler.js +177 -0
- package/dist/chat/opencode-websocket.js +95 -0
- package/dist/chat/websocket.js +100 -0
- package/dist/client/api.js +138 -0
- package/dist/client/config.js +34 -0
- package/dist/client/docker-proxy.js +103 -0
- package/dist/client/index.js +4 -0
- package/dist/client/proxy.js +96 -0
- package/dist/client/shell.js +71 -0
- package/dist/client/ws-shell.js +120 -0
- package/dist/config/loader.js +59 -0
- package/dist/docker/index.js +372 -0
- package/dist/docker/types.js +1 -0
- package/dist/index.js +475 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/metadata.js +55 -0
- package/dist/sessions/parser.js +553 -0
- package/dist/sessions/types.js +1 -0
- package/dist/shared/base-websocket.js +51 -0
- package/dist/shared/client-types.js +1 -0
- package/dist/shared/constants.js +11 -0
- package/dist/shared/types.js +5 -0
- package/dist/terminal/handler.js +86 -0
- package/dist/terminal/host-handler.js +76 -0
- package/dist/terminal/index.js +3 -0
- package/dist/terminal/types.js +8 -0
- package/dist/terminal/websocket.js +115 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/manager.js +475 -0
- package/dist/workspace/state.js +66 -0
- package/dist/workspace/types.js +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import { os, ORPCError } from '@orpc/server';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import os_module from 'os';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { HOST_WORKSPACE_NAME } from '../shared/types';
|
|
7
|
+
import { getDockerVersion, execInContainer } from '../docker';
|
|
8
|
+
import { saveAgentConfig } from '../config/loader';
|
|
9
|
+
import { setSessionName, getSessionNamesForWorkspace, deleteSessionName, } from '../sessions/metadata';
|
|
10
|
+
import { parseClaudeSessionContent } from '../sessions/parser';
|
|
11
|
+
const WorkspaceStatusSchema = z.enum(['running', 'stopped', 'creating', 'error']);
|
|
12
|
+
const WorkspacePortsSchema = z.object({
|
|
13
|
+
ssh: z.number(),
|
|
14
|
+
http: z.number().optional(),
|
|
15
|
+
});
|
|
16
|
+
const WorkspaceInfoSchema = z.object({
|
|
17
|
+
name: z.string(),
|
|
18
|
+
status: WorkspaceStatusSchema,
|
|
19
|
+
containerId: z.string(),
|
|
20
|
+
created: z.string(),
|
|
21
|
+
repo: z.string().optional(),
|
|
22
|
+
ports: WorkspacePortsSchema,
|
|
23
|
+
});
|
|
24
|
+
const CredentialsSchema = z.object({
|
|
25
|
+
env: z.record(z.string(), z.string()),
|
|
26
|
+
files: z.record(z.string(), z.string()),
|
|
27
|
+
});
|
|
28
|
+
const ScriptsSchema = z.object({
|
|
29
|
+
post_start: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
const CodingAgentsSchema = z.object({
|
|
32
|
+
opencode: z
|
|
33
|
+
.object({
|
|
34
|
+
zen_token: z.string().optional(),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
github: z
|
|
38
|
+
.object({
|
|
39
|
+
token: z.string().optional(),
|
|
40
|
+
})
|
|
41
|
+
.optional(),
|
|
42
|
+
claude_code: z
|
|
43
|
+
.object({
|
|
44
|
+
oauth_token: z.string().optional(),
|
|
45
|
+
model: z.string().optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
});
|
|
49
|
+
function mapErrorToORPC(err, defaultMessage) {
|
|
50
|
+
const message = err instanceof Error ? err.message : defaultMessage;
|
|
51
|
+
if (message.includes('not found')) {
|
|
52
|
+
throw new ORPCError('NOT_FOUND', { message: 'Workspace not found' });
|
|
53
|
+
}
|
|
54
|
+
if (message.includes('already exists')) {
|
|
55
|
+
throw new ORPCError('CONFLICT', { message });
|
|
56
|
+
}
|
|
57
|
+
throw new ORPCError('INTERNAL_SERVER_ERROR', { message });
|
|
58
|
+
}
|
|
59
|
+
function decodeClaudeProjectPath(encoded) {
|
|
60
|
+
return encoded.replace(/-/g, '/');
|
|
61
|
+
}
|
|
62
|
+
function extractFirstUserPrompt(messages) {
|
|
63
|
+
const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0);
|
|
64
|
+
return firstPrompt?.content ? firstPrompt.content.slice(0, 200) : null;
|
|
65
|
+
}
|
|
66
|
+
function extractClaudeSessionName(content) {
|
|
67
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
try {
|
|
70
|
+
const obj = JSON.parse(line);
|
|
71
|
+
if (obj.type === 'system' && obj.subtype === 'session_name') {
|
|
72
|
+
return obj.name || null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function extractContent(content) {
|
|
82
|
+
if (typeof content === 'string')
|
|
83
|
+
return content;
|
|
84
|
+
if (Array.isArray(content)) {
|
|
85
|
+
const text = content.find((c) => c.type === 'text')?.text;
|
|
86
|
+
return typeof text === 'string' ? text : null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
export function createRouter(ctx) {
|
|
91
|
+
const listWorkspaces = os.handler(async () => {
|
|
92
|
+
return ctx.workspaces.list();
|
|
93
|
+
});
|
|
94
|
+
const getWorkspace = os.input(z.object({ name: z.string() })).handler(async ({ input }) => {
|
|
95
|
+
const workspace = await ctx.workspaces.get(input.name);
|
|
96
|
+
if (!workspace) {
|
|
97
|
+
throw new ORPCError('NOT_FOUND', { message: 'Workspace not found' });
|
|
98
|
+
}
|
|
99
|
+
return workspace;
|
|
100
|
+
});
|
|
101
|
+
const createWorkspace = os
|
|
102
|
+
.input(z.object({
|
|
103
|
+
name: z.string(),
|
|
104
|
+
clone: z.string().optional(),
|
|
105
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
106
|
+
}))
|
|
107
|
+
.output(WorkspaceInfoSchema)
|
|
108
|
+
.handler(async ({ input }) => {
|
|
109
|
+
try {
|
|
110
|
+
return await ctx.workspaces.create(input);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
mapErrorToORPC(err, 'Failed to create workspace');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const deleteWorkspace = os.input(z.object({ name: z.string() })).handler(async ({ input }) => {
|
|
117
|
+
try {
|
|
118
|
+
ctx.terminalServer.closeConnectionsForWorkspace(input.name);
|
|
119
|
+
await ctx.workspaces.delete(input.name);
|
|
120
|
+
return { success: true };
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
mapErrorToORPC(err, 'Failed to delete workspace');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const startWorkspace = os
|
|
127
|
+
.input(z.object({ name: z.string() }))
|
|
128
|
+
.output(WorkspaceInfoSchema)
|
|
129
|
+
.handler(async ({ input }) => {
|
|
130
|
+
try {
|
|
131
|
+
return await ctx.workspaces.start(input.name);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
mapErrorToORPC(err, 'Failed to start workspace');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const stopWorkspace = os
|
|
138
|
+
.input(z.object({ name: z.string() }))
|
|
139
|
+
.output(WorkspaceInfoSchema)
|
|
140
|
+
.handler(async ({ input }) => {
|
|
141
|
+
try {
|
|
142
|
+
ctx.terminalServer.closeConnectionsForWorkspace(input.name);
|
|
143
|
+
return await ctx.workspaces.stop(input.name);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
mapErrorToORPC(err, 'Failed to stop workspace');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
const getLogs = os
|
|
150
|
+
.input(z.object({ name: z.string(), tail: z.number().optional().default(100) }))
|
|
151
|
+
.handler(async ({ input }) => {
|
|
152
|
+
try {
|
|
153
|
+
return await ctx.workspaces.getLogs(input.name, input.tail);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
mapErrorToORPC(err, 'Failed to get logs');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const syncWorkspace = os.input(z.object({ name: z.string() })).handler(async ({ input }) => {
|
|
160
|
+
try {
|
|
161
|
+
await ctx.workspaces.sync(input.name);
|
|
162
|
+
return { success: true };
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
mapErrorToORPC(err, 'Failed to sync workspace');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const syncAllWorkspaces = os.handler(async () => {
|
|
169
|
+
const workspaces = await ctx.workspaces.list();
|
|
170
|
+
const runningWorkspaces = workspaces.filter((ws) => ws.status === 'running');
|
|
171
|
+
const results = [];
|
|
172
|
+
for (const ws of runningWorkspaces) {
|
|
173
|
+
try {
|
|
174
|
+
await ctx.workspaces.sync(ws.name);
|
|
175
|
+
results.push({ name: ws.name, success: true });
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
results.push({ name: ws.name, success: false, error: err.message });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
synced: results.filter((r) => r.success).length,
|
|
183
|
+
failed: results.filter((r) => !r.success).length,
|
|
184
|
+
results,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
const getInfo = os.handler(async () => {
|
|
188
|
+
let dockerVersion = 'unknown';
|
|
189
|
+
try {
|
|
190
|
+
dockerVersion = await getDockerVersion();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
dockerVersion = 'unavailable';
|
|
194
|
+
}
|
|
195
|
+
const allWorkspaces = await ctx.workspaces.list();
|
|
196
|
+
return {
|
|
197
|
+
hostname: os_module.hostname(),
|
|
198
|
+
uptime: Math.floor((Date.now() - ctx.startTime) / 1000),
|
|
199
|
+
workspacesCount: allWorkspaces.length,
|
|
200
|
+
dockerVersion,
|
|
201
|
+
terminalConnections: ctx.terminalServer.getConnectionCount(),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
const getCredentials = os.output(CredentialsSchema).handler(async () => {
|
|
205
|
+
return ctx.config.get().credentials;
|
|
206
|
+
});
|
|
207
|
+
const updateCredentials = os
|
|
208
|
+
.input(CredentialsSchema)
|
|
209
|
+
.output(CredentialsSchema)
|
|
210
|
+
.handler(async ({ input }) => {
|
|
211
|
+
const currentConfig = ctx.config.get();
|
|
212
|
+
const newConfig = { ...currentConfig, credentials: input };
|
|
213
|
+
ctx.config.set(newConfig);
|
|
214
|
+
await saveAgentConfig(newConfig, ctx.configDir);
|
|
215
|
+
return input;
|
|
216
|
+
});
|
|
217
|
+
const getScripts = os.output(ScriptsSchema).handler(async () => {
|
|
218
|
+
return ctx.config.get().scripts;
|
|
219
|
+
});
|
|
220
|
+
const updateScripts = os
|
|
221
|
+
.input(ScriptsSchema)
|
|
222
|
+
.output(ScriptsSchema)
|
|
223
|
+
.handler(async ({ input }) => {
|
|
224
|
+
const currentConfig = ctx.config.get();
|
|
225
|
+
const newConfig = { ...currentConfig, scripts: input };
|
|
226
|
+
ctx.config.set(newConfig);
|
|
227
|
+
await saveAgentConfig(newConfig, ctx.configDir);
|
|
228
|
+
return input;
|
|
229
|
+
});
|
|
230
|
+
const getAgents = os.output(CodingAgentsSchema).handler(async () => {
|
|
231
|
+
return ctx.config.get().agents || {};
|
|
232
|
+
});
|
|
233
|
+
const updateAgents = os
|
|
234
|
+
.input(CodingAgentsSchema)
|
|
235
|
+
.output(CodingAgentsSchema)
|
|
236
|
+
.handler(async ({ input }) => {
|
|
237
|
+
const currentConfig = ctx.config.get();
|
|
238
|
+
const newConfig = { ...currentConfig, agents: input };
|
|
239
|
+
ctx.config.set(newConfig);
|
|
240
|
+
await saveAgentConfig(newConfig, ctx.configDir);
|
|
241
|
+
return input;
|
|
242
|
+
});
|
|
243
|
+
async function listHostSessions(input) {
|
|
244
|
+
const limit = input.limit ?? 50;
|
|
245
|
+
const offset = input.offset ?? 0;
|
|
246
|
+
const homeDir = os_module.homedir();
|
|
247
|
+
const rawSessions = [];
|
|
248
|
+
if (!input.agentType || input.agentType === 'claude-code') {
|
|
249
|
+
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
|
250
|
+
try {
|
|
251
|
+
const projectDirs = await fs.readdir(claudeProjectsDir);
|
|
252
|
+
for (const projectDir of projectDirs) {
|
|
253
|
+
const projectPath = path.join(claudeProjectsDir, projectDir);
|
|
254
|
+
const stat = await fs.stat(projectPath);
|
|
255
|
+
if (!stat.isDirectory())
|
|
256
|
+
continue;
|
|
257
|
+
const files = await fs.readdir(projectPath);
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
if (!file.endsWith('.jsonl') || file.startsWith('agent-'))
|
|
260
|
+
continue;
|
|
261
|
+
const filePath = path.join(projectPath, file);
|
|
262
|
+
const fileStat = await fs.stat(filePath);
|
|
263
|
+
const sessionId = file.replace('.jsonl', '');
|
|
264
|
+
rawSessions.push({
|
|
265
|
+
id: sessionId,
|
|
266
|
+
agentType: 'claude-code',
|
|
267
|
+
projectPath: projectDir.replace(/-/g, '/'),
|
|
268
|
+
mtime: fileStat.mtimeMs,
|
|
269
|
+
filePath,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Directory doesn't exist or not readable
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!input.agentType || input.agentType === 'opencode') {
|
|
279
|
+
const opencodeDir = path.join(homeDir, '.opencode', 'sessions');
|
|
280
|
+
try {
|
|
281
|
+
const sessions = await fs.readdir(opencodeDir);
|
|
282
|
+
for (const sessionDir of sessions) {
|
|
283
|
+
const sessionPath = path.join(opencodeDir, sessionDir);
|
|
284
|
+
const stat = await fs.stat(sessionPath);
|
|
285
|
+
if (!stat.isDirectory())
|
|
286
|
+
continue;
|
|
287
|
+
const sessionFile = path.join(sessionPath, 'session.json');
|
|
288
|
+
try {
|
|
289
|
+
const sessionStat = await fs.stat(sessionFile);
|
|
290
|
+
rawSessions.push({
|
|
291
|
+
id: sessionDir,
|
|
292
|
+
agentType: 'opencode',
|
|
293
|
+
projectPath: homeDir,
|
|
294
|
+
mtime: sessionStat.mtimeMs,
|
|
295
|
+
filePath: sessionFile,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// session.json doesn't exist
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Directory doesn't exist
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
rawSessions.sort((a, b) => b.mtime - a.mtime);
|
|
308
|
+
const sessionNames = await getSessionNamesForWorkspace(ctx.configDir, HOST_WORKSPACE_NAME);
|
|
309
|
+
const sessions = await Promise.all(rawSessions.map(async (raw) => {
|
|
310
|
+
let firstPrompt = null;
|
|
311
|
+
let messageCount = 0;
|
|
312
|
+
if (raw.agentType === 'claude-code') {
|
|
313
|
+
try {
|
|
314
|
+
const fileContent = await fs.readFile(raw.filePath, 'utf-8');
|
|
315
|
+
const lines = fileContent.trim().split('\n').filter(Boolean);
|
|
316
|
+
messageCount = lines.length;
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
try {
|
|
319
|
+
const entry = JSON.parse(line);
|
|
320
|
+
if ((entry.type === 'user' || entry.type === 'human') && entry.message?.content) {
|
|
321
|
+
const msgContent = entry.message.content;
|
|
322
|
+
if (Array.isArray(msgContent)) {
|
|
323
|
+
const textBlock = msgContent.find((b) => b.type === 'text');
|
|
324
|
+
if (textBlock?.text) {
|
|
325
|
+
firstPrompt = textBlock.text.slice(0, 200);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (typeof msgContent === 'string') {
|
|
330
|
+
firstPrompt = msgContent.slice(0, 200);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Can't read file
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (raw.agentType === 'opencode') {
|
|
345
|
+
try {
|
|
346
|
+
const sessionContent = await fs.readFile(raw.filePath, 'utf-8');
|
|
347
|
+
const sessionData = JSON.parse(sessionContent);
|
|
348
|
+
messageCount = sessionData.messages?.length || 0;
|
|
349
|
+
if (sessionData.title) {
|
|
350
|
+
firstPrompt = sessionData.title;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// Can't read file
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
id: raw.id,
|
|
359
|
+
name: sessionNames[raw.id] || null,
|
|
360
|
+
agentType: raw.agentType,
|
|
361
|
+
projectPath: raw.projectPath,
|
|
362
|
+
messageCount,
|
|
363
|
+
lastActivity: new Date(raw.mtime).toISOString(),
|
|
364
|
+
firstPrompt,
|
|
365
|
+
};
|
|
366
|
+
}));
|
|
367
|
+
const nonEmptySessions = sessions.filter((s) => s.messageCount > 0);
|
|
368
|
+
const paginatedSessions = nonEmptySessions.slice(offset, offset + limit);
|
|
369
|
+
return {
|
|
370
|
+
sessions: paginatedSessions,
|
|
371
|
+
total: nonEmptySessions.length,
|
|
372
|
+
hasMore: offset + limit < nonEmptySessions.length,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function getHostSession(sessionId, agentType) {
|
|
376
|
+
const homeDir = os_module.homedir();
|
|
377
|
+
const messages = [];
|
|
378
|
+
if (!agentType || agentType === 'claude-code') {
|
|
379
|
+
const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
380
|
+
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
|
381
|
+
try {
|
|
382
|
+
const projectDirs = await fs.readdir(claudeProjectsDir);
|
|
383
|
+
for (const projectDir of projectDirs) {
|
|
384
|
+
const sessionFile = path.join(claudeProjectsDir, projectDir, `${safeSessionId}.jsonl`);
|
|
385
|
+
try {
|
|
386
|
+
const content = await fs.readFile(sessionFile, 'utf-8');
|
|
387
|
+
const parsed = parseClaudeSessionContent(content)
|
|
388
|
+
.filter((msg) => msg.type !== 'system')
|
|
389
|
+
.filter((msg) => msg.type === 'tool_use' ||
|
|
390
|
+
msg.type === 'tool_result' ||
|
|
391
|
+
(msg.content && msg.content.trim().length > 0));
|
|
392
|
+
messages.push(...parsed);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// File not found in this project dir
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Directory doesn't exist
|
|
402
|
+
}
|
|
403
|
+
if (messages.length > 0) {
|
|
404
|
+
return { id: sessionId, agentType: 'claude-code', messages };
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!agentType || agentType === 'opencode') {
|
|
408
|
+
const sessionDir = path.join(homeDir, '.opencode', 'sessions', sessionId);
|
|
409
|
+
const partsDir = path.join(sessionDir, 'part');
|
|
410
|
+
try {
|
|
411
|
+
const partFiles = await fs.readdir(partsDir);
|
|
412
|
+
const sortedParts = partFiles.sort();
|
|
413
|
+
for (const partFile of sortedParts) {
|
|
414
|
+
const partPath = path.join(partsDir, partFile);
|
|
415
|
+
try {
|
|
416
|
+
const partContent = await fs.readFile(partPath, 'utf-8');
|
|
417
|
+
const part = JSON.parse(partContent);
|
|
418
|
+
if (part.role === 'user' && part.content) {
|
|
419
|
+
const textContent = Array.isArray(part.content)
|
|
420
|
+
? part.content
|
|
421
|
+
.filter((c) => c.type === 'text')
|
|
422
|
+
.map((c) => c.text)
|
|
423
|
+
.join('\n')
|
|
424
|
+
: part.content;
|
|
425
|
+
messages.push({
|
|
426
|
+
type: 'user',
|
|
427
|
+
content: textContent,
|
|
428
|
+
timestamp: part.time || null,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else if (part.role === 'assistant') {
|
|
432
|
+
if (part.content) {
|
|
433
|
+
const textContent = Array.isArray(part.content)
|
|
434
|
+
? part.content
|
|
435
|
+
.filter((c) => c.type === 'text')
|
|
436
|
+
.map((c) => c.text)
|
|
437
|
+
.join('\n')
|
|
438
|
+
: part.content;
|
|
439
|
+
if (textContent) {
|
|
440
|
+
messages.push({
|
|
441
|
+
type: 'assistant',
|
|
442
|
+
content: textContent,
|
|
443
|
+
timestamp: part.time || null,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// Can't parse part
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (messages.length > 0) {
|
|
454
|
+
return { id: sessionId, agentType: 'opencode', messages };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Directory doesn't exist
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { id: sessionId, messages };
|
|
462
|
+
}
|
|
463
|
+
async function listSessionsCore(input) {
|
|
464
|
+
const limit = input.limit ?? 50;
|
|
465
|
+
const offset = input.offset ?? 0;
|
|
466
|
+
const isHost = input.workspaceName === HOST_WORKSPACE_NAME;
|
|
467
|
+
if (isHost) {
|
|
468
|
+
const config = ctx.config.get();
|
|
469
|
+
if (!config.allowHostAccess) {
|
|
470
|
+
throw new ORPCError('PRECONDITION_FAILED', { message: 'Host access is disabled' });
|
|
471
|
+
}
|
|
472
|
+
return listHostSessions(input);
|
|
473
|
+
}
|
|
474
|
+
const workspace = await ctx.workspaces.get(input.workspaceName);
|
|
475
|
+
if (!workspace) {
|
|
476
|
+
throw new ORPCError('NOT_FOUND', { message: 'Workspace not found' });
|
|
477
|
+
}
|
|
478
|
+
if (workspace.status !== 'running') {
|
|
479
|
+
throw new ORPCError('PRECONDITION_FAILED', { message: 'Workspace is not running' });
|
|
480
|
+
}
|
|
481
|
+
const containerName = `workspace-${input.workspaceName}`;
|
|
482
|
+
const rawSessions = [];
|
|
483
|
+
const claudeResult = await execInContainer(containerName, [
|
|
484
|
+
'bash',
|
|
485
|
+
'-c',
|
|
486
|
+
'find /home/workspace/.claude/projects -name "*.jsonl" -type f ! -name "agent-*.jsonl" -printf "%p\\t%T@\\t%s\\n" 2>/dev/null || true',
|
|
487
|
+
], { user: 'workspace' });
|
|
488
|
+
if (claudeResult.exitCode === 0 && claudeResult.stdout.trim()) {
|
|
489
|
+
const lines = claudeResult.stdout.trim().split('\n').filter(Boolean);
|
|
490
|
+
for (const line of lines) {
|
|
491
|
+
const parts = line.split('\t');
|
|
492
|
+
if (parts.length >= 3) {
|
|
493
|
+
const file = parts[0];
|
|
494
|
+
const mtime = Math.floor(parseFloat(parts[1]) || 0);
|
|
495
|
+
const size = parseInt(parts[2], 10) || 0;
|
|
496
|
+
if (size === 0)
|
|
497
|
+
continue;
|
|
498
|
+
const id = file.split('/').pop()?.replace('.jsonl', '') || '';
|
|
499
|
+
const projDir = file.split('/').slice(-2, -1)[0] || '';
|
|
500
|
+
const projectPath = decodeClaudeProjectPath(projDir);
|
|
501
|
+
if (!projectPath.startsWith('/workspace') && !projectPath.startsWith('/home/workspace'))
|
|
502
|
+
continue;
|
|
503
|
+
rawSessions.push({
|
|
504
|
+
id,
|
|
505
|
+
agentType: 'claude-code',
|
|
506
|
+
mtime,
|
|
507
|
+
projectPath,
|
|
508
|
+
filePath: file,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const opencodeResult = await execInContainer(containerName, [
|
|
514
|
+
'sh',
|
|
515
|
+
'-c',
|
|
516
|
+
'find /home/workspace/.local/share/opencode/storage/session -name "ses_*.json" -type f 2>/dev/null || true',
|
|
517
|
+
], { user: 'workspace' });
|
|
518
|
+
if (opencodeResult.exitCode === 0 && opencodeResult.stdout.trim()) {
|
|
519
|
+
const files = opencodeResult.stdout.trim().split('\n').filter(Boolean);
|
|
520
|
+
const catAll = await execInContainer(containerName, ['sh', '-c', `cat ${files.map((f) => `"${f}"`).join(' ')} 2>/dev/null | jq -s '.'`], { user: 'workspace' });
|
|
521
|
+
if (catAll.exitCode === 0) {
|
|
522
|
+
try {
|
|
523
|
+
const sessions = JSON.parse(catAll.stdout);
|
|
524
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
525
|
+
const data = sessions[i];
|
|
526
|
+
const file = files[i];
|
|
527
|
+
const id = data.id || file.split('/').pop()?.replace('.json', '') || '';
|
|
528
|
+
const mtime = Math.floor((data.time?.updated || 0) / 1000);
|
|
529
|
+
rawSessions.push({
|
|
530
|
+
id,
|
|
531
|
+
agentType: 'opencode',
|
|
532
|
+
projectPath: data.directory || '',
|
|
533
|
+
mtime,
|
|
534
|
+
name: data.title || undefined,
|
|
535
|
+
filePath: file,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// Skip on parse error
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const codexResult = await execInContainer(containerName, [
|
|
545
|
+
'sh',
|
|
546
|
+
'-c',
|
|
547
|
+
'find /home/workspace/.codex/sessions -name "rollout-*.jsonl" -type f -printf "%p\\t%T@\\t" -exec wc -l {} \\; 2>/dev/null || true',
|
|
548
|
+
], { user: 'workspace' });
|
|
549
|
+
if (codexResult.exitCode === 0 && codexResult.stdout.trim()) {
|
|
550
|
+
const lines = codexResult.stdout.trim().split('\n').filter(Boolean);
|
|
551
|
+
for (const line of lines) {
|
|
552
|
+
const parts = line.split('\t');
|
|
553
|
+
if (parts.length >= 2) {
|
|
554
|
+
const file = parts[0];
|
|
555
|
+
const mtime = Math.floor(parseFloat(parts[1]) || 0);
|
|
556
|
+
const id = file.split('/').pop()?.replace('.jsonl', '') || '';
|
|
557
|
+
const projPath = file
|
|
558
|
+
.replace('/home/workspace/.codex/sessions/', '')
|
|
559
|
+
.replace(/\/[^/]+$/, '');
|
|
560
|
+
rawSessions.push({
|
|
561
|
+
id,
|
|
562
|
+
agentType: 'codex',
|
|
563
|
+
projectPath: projPath,
|
|
564
|
+
mtime,
|
|
565
|
+
filePath: file,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const customNames = await getSessionNamesForWorkspace(ctx.stateDir, input.workspaceName);
|
|
571
|
+
const filteredSessions = rawSessions
|
|
572
|
+
.filter((s) => !input.agentType || s.agentType === input.agentType)
|
|
573
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
574
|
+
const paginatedRawSessions = filteredSessions.slice(offset, offset + limit);
|
|
575
|
+
const sessions = [];
|
|
576
|
+
for (const session of paginatedRawSessions) {
|
|
577
|
+
if (session.agentType === 'claude-code') {
|
|
578
|
+
const catResult = await execInContainer(containerName, ['cat', session.filePath], {
|
|
579
|
+
user: 'workspace',
|
|
580
|
+
});
|
|
581
|
+
if (catResult.exitCode !== 0) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const messages = parseClaudeSessionContent(catResult.stdout).filter((msg) => msg.type !== 'system');
|
|
585
|
+
const firstPrompt = extractFirstUserPrompt(messages);
|
|
586
|
+
const name = extractClaudeSessionName(catResult.stdout);
|
|
587
|
+
if (messages.length === 0) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
sessions.push({
|
|
591
|
+
id: session.id,
|
|
592
|
+
name: customNames[session.id] || name || null,
|
|
593
|
+
agentType: session.agentType,
|
|
594
|
+
projectPath: session.projectPath,
|
|
595
|
+
messageCount: messages.length,
|
|
596
|
+
lastActivity: new Date(session.mtime * 1000).toISOString(),
|
|
597
|
+
firstPrompt,
|
|
598
|
+
});
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (session.agentType === 'opencode') {
|
|
602
|
+
const msgDir = `/home/workspace/.local/share/opencode/storage/message/${session.id}`;
|
|
603
|
+
const listMsgsResult = await execInContainer(containerName, ['bash', '-c', `ls -1 "${msgDir}"/msg_*.json 2>/dev/null | sort`], { user: 'workspace' });
|
|
604
|
+
const messages = [];
|
|
605
|
+
if (listMsgsResult.exitCode === 0 && listMsgsResult.stdout.trim()) {
|
|
606
|
+
const msgFiles = listMsgsResult.stdout.trim().split('\n').filter(Boolean);
|
|
607
|
+
for (const msgFile of msgFiles) {
|
|
608
|
+
const msgResult = await execInContainer(containerName, ['cat', msgFile], {
|
|
609
|
+
user: 'workspace',
|
|
610
|
+
});
|
|
611
|
+
if (msgResult.exitCode !== 0)
|
|
612
|
+
continue;
|
|
613
|
+
try {
|
|
614
|
+
const msg = JSON.parse(msgResult.stdout);
|
|
615
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
616
|
+
const content = extractContent(msg.content);
|
|
617
|
+
messages.push({ type: msg.role, content: content || undefined });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0)?.content;
|
|
626
|
+
if (messages.length === 0) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
sessions.push({
|
|
630
|
+
id: session.id,
|
|
631
|
+
name: customNames[session.id] || session.name || null,
|
|
632
|
+
agentType: session.agentType,
|
|
633
|
+
projectPath: session.projectPath,
|
|
634
|
+
messageCount: messages.length,
|
|
635
|
+
lastActivity: new Date(session.mtime * 1000).toISOString(),
|
|
636
|
+
firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const catResult = await execInContainer(containerName, ['cat', session.filePath], {
|
|
641
|
+
user: 'workspace',
|
|
642
|
+
});
|
|
643
|
+
if (catResult.exitCode !== 0) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const lines = catResult.stdout.split('\n').filter(Boolean);
|
|
647
|
+
let sessionId = session.id;
|
|
648
|
+
if (lines.length > 0) {
|
|
649
|
+
try {
|
|
650
|
+
const meta = JSON.parse(lines[0]);
|
|
651
|
+
if (meta.session_id) {
|
|
652
|
+
sessionId = meta.session_id;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// ignore
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const messages = [];
|
|
660
|
+
for (let i = 1; i < lines.length; i++) {
|
|
661
|
+
try {
|
|
662
|
+
const event = JSON.parse(lines[i]);
|
|
663
|
+
const role = event.payload?.role || event.payload?.message?.role;
|
|
664
|
+
const content = event.payload?.content || event.payload?.message?.content;
|
|
665
|
+
if (role === 'user' || role === 'assistant') {
|
|
666
|
+
const textContent = extractContent(content);
|
|
667
|
+
messages.push({ type: role, content: textContent || undefined });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0)?.content;
|
|
675
|
+
if (messages.length === 0) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
sessions.push({
|
|
679
|
+
id: sessionId,
|
|
680
|
+
name: customNames[sessionId] || null,
|
|
681
|
+
agentType: session.agentType,
|
|
682
|
+
projectPath: session.projectPath,
|
|
683
|
+
messageCount: messages.length,
|
|
684
|
+
lastActivity: new Date(session.mtime * 1000).toISOString(),
|
|
685
|
+
firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
sessions,
|
|
690
|
+
total: filteredSessions.length,
|
|
691
|
+
hasMore: offset + limit < filteredSessions.length,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const listSessions = os
|
|
695
|
+
.input(z.object({
|
|
696
|
+
workspaceName: z.string(),
|
|
697
|
+
agentType: z.enum(['claude-code', 'opencode', 'codex']).optional(),
|
|
698
|
+
limit: z.number().optional().default(50),
|
|
699
|
+
offset: z.number().optional().default(0),
|
|
700
|
+
}))
|
|
701
|
+
.handler(async ({ input }) => {
|
|
702
|
+
return listSessionsCore(input);
|
|
703
|
+
});
|
|
704
|
+
const getSession = os
|
|
705
|
+
.input(z.object({
|
|
706
|
+
workspaceName: z.string(),
|
|
707
|
+
sessionId: z.string(),
|
|
708
|
+
agentType: z.enum(['claude-code', 'opencode', 'codex']).optional(),
|
|
709
|
+
}))
|
|
710
|
+
.handler(async ({ input }) => {
|
|
711
|
+
const isHost = input.workspaceName === HOST_WORKSPACE_NAME;
|
|
712
|
+
if (isHost) {
|
|
713
|
+
const config = ctx.config.get();
|
|
714
|
+
if (!config.allowHostAccess) {
|
|
715
|
+
throw new ORPCError('PRECONDITION_FAILED', { message: 'Host access is disabled' });
|
|
716
|
+
}
|
|
717
|
+
return getHostSession(input.sessionId, input.agentType);
|
|
718
|
+
}
|
|
719
|
+
const workspace = await ctx.workspaces.get(input.workspaceName);
|
|
720
|
+
if (!workspace) {
|
|
721
|
+
throw new ORPCError('NOT_FOUND', { message: 'Workspace not found' });
|
|
722
|
+
}
|
|
723
|
+
if (workspace.status !== 'running') {
|
|
724
|
+
throw new ORPCError('PRECONDITION_FAILED', { message: 'Workspace is not running' });
|
|
725
|
+
}
|
|
726
|
+
const containerName = `workspace-${input.workspaceName}`;
|
|
727
|
+
const messages = [];
|
|
728
|
+
if (!input.agentType || input.agentType === 'claude-code') {
|
|
729
|
+
const safeSessionId = input.sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
730
|
+
const findResult = await execInContainer(containerName, [
|
|
731
|
+
'find',
|
|
732
|
+
'/home/workspace/.claude/projects',
|
|
733
|
+
'-name',
|
|
734
|
+
`${safeSessionId}.jsonl`,
|
|
735
|
+
'-type',
|
|
736
|
+
'f',
|
|
737
|
+
], { user: 'workspace' });
|
|
738
|
+
const foundPath = findResult.stdout.trim().split('\n')[0];
|
|
739
|
+
if (findResult.exitCode === 0 && foundPath) {
|
|
740
|
+
const filePath = foundPath;
|
|
741
|
+
const catResult = await execInContainer(containerName, ['cat', filePath], {
|
|
742
|
+
user: 'workspace',
|
|
743
|
+
});
|
|
744
|
+
if (catResult.exitCode === 0) {
|
|
745
|
+
const parsed = parseClaudeSessionContent(catResult.stdout)
|
|
746
|
+
.filter((msg) => msg.type !== 'system')
|
|
747
|
+
.filter((msg) => msg.type === 'tool_use' ||
|
|
748
|
+
msg.type === 'tool_result' ||
|
|
749
|
+
(msg.content && msg.content.trim().length > 0));
|
|
750
|
+
return { id: input.sessionId, agentType: 'claude-code', messages: parsed };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (!input.agentType || input.agentType === 'opencode') {
|
|
755
|
+
const findResult = await execInContainer(containerName, [
|
|
756
|
+
'bash',
|
|
757
|
+
'-c',
|
|
758
|
+
`find /home/workspace/.local/share/opencode/storage/session -name "${input.sessionId}.json" -type f 2>/dev/null | head -1`,
|
|
759
|
+
], { user: 'workspace' });
|
|
760
|
+
if (findResult.exitCode === 0 && findResult.stdout.trim()) {
|
|
761
|
+
const filePath = findResult.stdout.trim();
|
|
762
|
+
const catResult = await execInContainer(containerName, ['cat', filePath], {
|
|
763
|
+
user: 'workspace',
|
|
764
|
+
});
|
|
765
|
+
if (catResult.exitCode === 0) {
|
|
766
|
+
try {
|
|
767
|
+
const session = JSON.parse(catResult.stdout);
|
|
768
|
+
const msgDir = `/home/workspace/.local/share/opencode/storage/message/${session.id}`;
|
|
769
|
+
const partDir = `/home/workspace/.local/share/opencode/storage/part`;
|
|
770
|
+
const listMsgsResult = await execInContainer(containerName, ['bash', '-c', `ls -1 "${msgDir}"/msg_*.json 2>/dev/null | sort`], { user: 'workspace' });
|
|
771
|
+
if (listMsgsResult.exitCode === 0 && listMsgsResult.stdout.trim()) {
|
|
772
|
+
const msgFiles = listMsgsResult.stdout.trim().split('\n').filter(Boolean);
|
|
773
|
+
for (const msgFile of msgFiles) {
|
|
774
|
+
const msgResult = await execInContainer(containerName, ['cat', msgFile], {
|
|
775
|
+
user: 'workspace',
|
|
776
|
+
});
|
|
777
|
+
if (msgResult.exitCode === 0) {
|
|
778
|
+
try {
|
|
779
|
+
const msg = JSON.parse(msgResult.stdout);
|
|
780
|
+
if (!msg.id || (msg.role !== 'user' && msg.role !== 'assistant'))
|
|
781
|
+
continue;
|
|
782
|
+
const timestamp = msg.time?.created
|
|
783
|
+
? new Date(msg.time.created).toISOString()
|
|
784
|
+
: undefined;
|
|
785
|
+
const listPartsResult = await execInContainer(containerName, [
|
|
786
|
+
'bash',
|
|
787
|
+
'-c',
|
|
788
|
+
`ls -1 "${partDir}/${msg.id}"/prt_*.json 2>/dev/null | sort`,
|
|
789
|
+
], { user: 'workspace' });
|
|
790
|
+
if (listPartsResult.exitCode === 0 && listPartsResult.stdout.trim()) {
|
|
791
|
+
const partFiles = listPartsResult.stdout.trim().split('\n').filter(Boolean);
|
|
792
|
+
for (const partFile of partFiles) {
|
|
793
|
+
const partResult = await execInContainer(containerName, ['cat', partFile], { user: 'workspace' });
|
|
794
|
+
if (partResult.exitCode === 0) {
|
|
795
|
+
try {
|
|
796
|
+
const part = JSON.parse(partResult.stdout);
|
|
797
|
+
if (part.type === 'text' && part.text) {
|
|
798
|
+
messages.push({
|
|
799
|
+
type: msg.role,
|
|
800
|
+
content: part.text,
|
|
801
|
+
timestamp,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
else if (part.type === 'tool' && part.tool) {
|
|
805
|
+
messages.push({
|
|
806
|
+
type: 'tool_use',
|
|
807
|
+
content: undefined,
|
|
808
|
+
toolName: part.state?.title || part.tool,
|
|
809
|
+
toolId: part.callID || part.id,
|
|
810
|
+
toolInput: JSON.stringify(part.state?.input, null, 2),
|
|
811
|
+
timestamp,
|
|
812
|
+
});
|
|
813
|
+
if (part.state?.output) {
|
|
814
|
+
messages.push({
|
|
815
|
+
type: 'tool_result',
|
|
816
|
+
content: part.state.output,
|
|
817
|
+
toolId: part.callID || part.id,
|
|
818
|
+
timestamp,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return { id: input.sessionId, agentType: 'opencode', messages };
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
// Session parse failed
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (!input.agentType || input.agentType === 'codex') {
|
|
845
|
+
const findResult = await execInContainer(containerName, [
|
|
846
|
+
'bash',
|
|
847
|
+
'-c',
|
|
848
|
+
`find /home/workspace/.codex/sessions -name "rollout-*.jsonl" -type f 2>/dev/null`,
|
|
849
|
+
], { user: 'workspace' });
|
|
850
|
+
if (findResult.exitCode === 0 && findResult.stdout.trim()) {
|
|
851
|
+
const files = findResult.stdout.trim().split('\n').filter(Boolean);
|
|
852
|
+
for (const filePath of files) {
|
|
853
|
+
const headResult = await execInContainer(containerName, ['bash', '-c', `head -1 "${filePath}"`], { user: 'workspace' });
|
|
854
|
+
let sessionId = filePath.split('/').pop()?.replace('.jsonl', '') || '';
|
|
855
|
+
if (headResult.exitCode === 0 && headResult.stdout.trim()) {
|
|
856
|
+
try {
|
|
857
|
+
const meta = JSON.parse(headResult.stdout.trim());
|
|
858
|
+
if (meta.session_id)
|
|
859
|
+
sessionId = meta.session_id;
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Use filename
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (sessionId === input.sessionId) {
|
|
866
|
+
const catResult = await execInContainer(containerName, ['cat', filePath], {
|
|
867
|
+
user: 'workspace',
|
|
868
|
+
});
|
|
869
|
+
if (catResult.exitCode === 0) {
|
|
870
|
+
const lines = catResult.stdout.split('\n').filter(Boolean);
|
|
871
|
+
for (let i = 1; i < lines.length; i++) {
|
|
872
|
+
try {
|
|
873
|
+
const event = JSON.parse(lines[i]);
|
|
874
|
+
const role = event.payload?.role || event.payload?.message?.role;
|
|
875
|
+
const content = event.payload?.content || event.payload?.message?.content;
|
|
876
|
+
if (role === 'user' || role === 'assistant') {
|
|
877
|
+
const parsedContent = extractContent(content);
|
|
878
|
+
messages.push({
|
|
879
|
+
type: role,
|
|
880
|
+
content: parsedContent || undefined,
|
|
881
|
+
timestamp: event.timestamp
|
|
882
|
+
? new Date(event.timestamp).toISOString()
|
|
883
|
+
: undefined,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return { id: input.sessionId, agentType: 'codex', messages };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
throw new ORPCError('NOT_FOUND', { message: 'Session not found' });
|
|
898
|
+
});
|
|
899
|
+
const renameSession = os
|
|
900
|
+
.input(z.object({
|
|
901
|
+
workspaceName: z.string(),
|
|
902
|
+
sessionId: z.string(),
|
|
903
|
+
name: z.string().min(1).max(200),
|
|
904
|
+
}))
|
|
905
|
+
.handler(async ({ input }) => {
|
|
906
|
+
await setSessionName(ctx.stateDir, input.workspaceName, input.sessionId, input.name);
|
|
907
|
+
return { success: true };
|
|
908
|
+
});
|
|
909
|
+
const clearSessionName = os
|
|
910
|
+
.input(z.object({
|
|
911
|
+
workspaceName: z.string(),
|
|
912
|
+
sessionId: z.string(),
|
|
913
|
+
}))
|
|
914
|
+
.handler(async ({ input }) => {
|
|
915
|
+
await deleteSessionName(ctx.stateDir, input.workspaceName, input.sessionId);
|
|
916
|
+
return { success: true };
|
|
917
|
+
});
|
|
918
|
+
const listAllSessions = os
|
|
919
|
+
.input(z.object({
|
|
920
|
+
agentType: z.enum(['claude-code', 'opencode', 'codex']).optional(),
|
|
921
|
+
limit: z.number().optional().default(100),
|
|
922
|
+
offset: z.number().optional().default(0),
|
|
923
|
+
}))
|
|
924
|
+
.handler(async ({ input }) => {
|
|
925
|
+
const allWorkspaces = await ctx.workspaces.list();
|
|
926
|
+
const runningWorkspaces = allWorkspaces.filter((w) => w.status === 'running');
|
|
927
|
+
const allSessions = [];
|
|
928
|
+
for (const workspace of runningWorkspaces) {
|
|
929
|
+
try {
|
|
930
|
+
const result = await listSessionsCore({
|
|
931
|
+
workspaceName: workspace.name,
|
|
932
|
+
agentType: input.agentType,
|
|
933
|
+
limit: 1000,
|
|
934
|
+
offset: 0,
|
|
935
|
+
});
|
|
936
|
+
for (const session of result.sessions) {
|
|
937
|
+
allSessions.push({
|
|
938
|
+
...session,
|
|
939
|
+
workspaceName: workspace.name,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
allSessions.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
948
|
+
const paginatedSessions = allSessions.slice(input.offset, input.offset + input.limit);
|
|
949
|
+
return {
|
|
950
|
+
sessions: paginatedSessions,
|
|
951
|
+
total: allSessions.length,
|
|
952
|
+
hasMore: input.offset + input.limit < allSessions.length,
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
const getHostInfo = os.handler(async () => {
|
|
956
|
+
const config = ctx.config.get();
|
|
957
|
+
return {
|
|
958
|
+
enabled: config.allowHostAccess === true,
|
|
959
|
+
hostname: os_module.hostname(),
|
|
960
|
+
username: os_module.userInfo().username,
|
|
961
|
+
homeDir: os_module.homedir(),
|
|
962
|
+
};
|
|
963
|
+
});
|
|
964
|
+
const updateHostAccess = os
|
|
965
|
+
.input(z.object({ enabled: z.boolean() }))
|
|
966
|
+
.handler(async ({ input }) => {
|
|
967
|
+
const currentConfig = ctx.config.get();
|
|
968
|
+
const newConfig = { ...currentConfig, allowHostAccess: input.enabled };
|
|
969
|
+
ctx.config.set(newConfig);
|
|
970
|
+
await saveAgentConfig(newConfig, ctx.configDir);
|
|
971
|
+
return {
|
|
972
|
+
enabled: input.enabled,
|
|
973
|
+
hostname: os_module.hostname(),
|
|
974
|
+
username: os_module.userInfo().username,
|
|
975
|
+
homeDir: os_module.homedir(),
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
return {
|
|
979
|
+
workspaces: {
|
|
980
|
+
list: listWorkspaces,
|
|
981
|
+
get: getWorkspace,
|
|
982
|
+
create: createWorkspace,
|
|
983
|
+
delete: deleteWorkspace,
|
|
984
|
+
start: startWorkspace,
|
|
985
|
+
stop: stopWorkspace,
|
|
986
|
+
logs: getLogs,
|
|
987
|
+
sync: syncWorkspace,
|
|
988
|
+
syncAll: syncAllWorkspaces,
|
|
989
|
+
},
|
|
990
|
+
sessions: {
|
|
991
|
+
list: listSessions,
|
|
992
|
+
listAll: listAllSessions,
|
|
993
|
+
get: getSession,
|
|
994
|
+
rename: renameSession,
|
|
995
|
+
clearName: clearSessionName,
|
|
996
|
+
},
|
|
997
|
+
host: {
|
|
998
|
+
info: getHostInfo,
|
|
999
|
+
updateAccess: updateHostAccess,
|
|
1000
|
+
},
|
|
1001
|
+
info: getInfo,
|
|
1002
|
+
config: {
|
|
1003
|
+
credentials: {
|
|
1004
|
+
get: getCredentials,
|
|
1005
|
+
update: updateCredentials,
|
|
1006
|
+
},
|
|
1007
|
+
scripts: {
|
|
1008
|
+
get: getScripts,
|
|
1009
|
+
update: updateScripts,
|
|
1010
|
+
},
|
|
1011
|
+
agents: {
|
|
1012
|
+
get: getAgents,
|
|
1013
|
+
update: updateAgents,
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|