@gricha/perry 0.2.0 → 0.2.2
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/README.md +18 -2
- package/dist/agent/router.js +235 -89
- package/dist/agent/run.js +55 -4
- package/dist/agent/web/assets/index-DIOWcVH-.css +1 -0
- package/dist/agent/web/assets/index-DN_QW9sL.js +104 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/client/api.js +2 -2
- package/dist/docker/eager-pull.js +65 -0
- package/dist/index.js +47 -23
- package/dist/perry-worker +0 -0
- package/dist/sessions/agents/claude.js +19 -0
- package/dist/sessions/agents/codex.js +40 -0
- package/dist/sessions/agents/index.js +63 -0
- package/dist/sessions/agents/opencode-storage.js +218 -0
- package/dist/sessions/agents/opencode.js +17 -3
- package/dist/sessions/cache.js +5 -0
- package/dist/shared/constants.js +1 -1
- package/dist/tailscale/index.js +80 -0
- package/dist/workspace/manager.js +104 -53
- package/package.json +3 -2
- package/dist/agent/web/assets/index-CaFOQOgc.css +0 -1
- package/dist/agent/web/assets/index-DQmM39Em.js +0 -104
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Perry</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DN_QW9sL.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DIOWcVH-.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist/client/api.js
CHANGED
|
@@ -66,9 +66,9 @@ export class ApiClient {
|
|
|
66
66
|
throw this.wrapError(err);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
async startWorkspace(name) {
|
|
69
|
+
async startWorkspace(name, options) {
|
|
70
70
|
try {
|
|
71
|
-
return await this.client.workspaces.start({ name });
|
|
71
|
+
return await this.client.workspaces.start({ name, clone: options?.clone, env: options?.env });
|
|
72
72
|
}
|
|
73
73
|
catch (err) {
|
|
74
74
|
throw this.wrapError(err);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { imageExists, tryPullImage, getDockerVersion } from './index';
|
|
2
|
+
import { WORKSPACE_IMAGE_REGISTRY } from '../shared/constants';
|
|
3
|
+
import pkg from '../../package.json';
|
|
4
|
+
const RETRY_INTERVAL_MS = 20000;
|
|
5
|
+
const MAX_RETRIES = 10;
|
|
6
|
+
let pullInProgress = false;
|
|
7
|
+
let pullComplete = false;
|
|
8
|
+
async function isDockerAvailable() {
|
|
9
|
+
try {
|
|
10
|
+
await getDockerVersion();
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function pullWorkspaceImage() {
|
|
18
|
+
const registryImage = `${WORKSPACE_IMAGE_REGISTRY}:${pkg.version}`;
|
|
19
|
+
const exists = await imageExists(registryImage);
|
|
20
|
+
if (exists) {
|
|
21
|
+
console.log(`[agent] Workspace image ${registryImage} already available`);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
console.log(`[agent] Pulling workspace image ${registryImage}...`);
|
|
25
|
+
const pulled = await tryPullImage(registryImage);
|
|
26
|
+
if (pulled) {
|
|
27
|
+
console.log('[agent] Workspace image pulled successfully');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
console.log('[agent] Failed to pull image - will retry later');
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export async function startEagerImagePull() {
|
|
34
|
+
if (pullInProgress || pullComplete) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
pullInProgress = true;
|
|
38
|
+
const attemptPull = async (attempt) => {
|
|
39
|
+
if (attempt > MAX_RETRIES) {
|
|
40
|
+
console.log('[agent] Max retries reached for image pull - giving up background pull');
|
|
41
|
+
pullInProgress = false;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const dockerAvailable = await isDockerAvailable();
|
|
45
|
+
if (!dockerAvailable) {
|
|
46
|
+
if (attempt === 1) {
|
|
47
|
+
console.log('[agent] Docker not available - will retry in background');
|
|
48
|
+
}
|
|
49
|
+
setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const success = await pullWorkspaceImage();
|
|
53
|
+
if (success) {
|
|
54
|
+
pullComplete = true;
|
|
55
|
+
pullInProgress = false;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
attemptPull(1);
|
|
62
|
+
}
|
|
63
|
+
export function isImagePullComplete() {
|
|
64
|
+
return pullComplete;
|
|
65
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -131,36 +131,18 @@ program
|
|
|
131
131
|
handleError(err);
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
|
-
program
|
|
135
|
-
.command('create <name>')
|
|
136
|
-
.description('Create a new workspace')
|
|
137
|
-
.option('--clone <url>', 'Git repository URL to clone')
|
|
138
|
-
.action(async (name, options) => {
|
|
139
|
-
try {
|
|
140
|
-
const client = await getClient();
|
|
141
|
-
console.log(`Creating workspace '${name}'...`);
|
|
142
|
-
const workspace = await client.createWorkspace({
|
|
143
|
-
name,
|
|
144
|
-
clone: options.clone,
|
|
145
|
-
});
|
|
146
|
-
console.log(`Workspace '${workspace.name}' created.`);
|
|
147
|
-
console.log(` Status: ${workspace.status}`);
|
|
148
|
-
console.log(` SSH Port: ${workspace.ports.ssh}`);
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
handleError(err);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
134
|
program
|
|
155
135
|
.command('start <name>')
|
|
156
|
-
.description('Start a
|
|
157
|
-
.
|
|
136
|
+
.description('Start a workspace (creates it if it does not exist)')
|
|
137
|
+
.option('--clone <url>', 'Git repository URL to clone (when creating)')
|
|
138
|
+
.action(async (name, options) => {
|
|
158
139
|
try {
|
|
159
140
|
const client = await getClient();
|
|
160
141
|
console.log(`Starting workspace '${name}'...`);
|
|
161
|
-
const workspace = await client.startWorkspace(name);
|
|
142
|
+
const workspace = await client.startWorkspace(name, { clone: options.clone });
|
|
162
143
|
console.log(`Workspace '${workspace.name}' started.`);
|
|
163
144
|
console.log(` Status: ${workspace.status}`);
|
|
145
|
+
console.log(` SSH Port: ${workspace.ports.ssh}`);
|
|
164
146
|
}
|
|
165
147
|
catch (err) {
|
|
166
148
|
handleError(err);
|
|
@@ -219,6 +201,12 @@ program
|
|
|
219
201
|
console.log(` Uptime: ${formatUptime(info.uptime)}`);
|
|
220
202
|
console.log(` Workspaces: ${info.workspacesCount}`);
|
|
221
203
|
console.log(` Docker: ${info.dockerVersion}`);
|
|
204
|
+
if (info.tailscale?.running) {
|
|
205
|
+
console.log(` Tailscale: ${info.tailscale.dnsName}`);
|
|
206
|
+
if (info.tailscale.httpsUrl) {
|
|
207
|
+
console.log(` HTTPS URL: ${info.tailscale.httpsUrl}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
222
210
|
}
|
|
223
211
|
}
|
|
224
212
|
catch (err) {
|
|
@@ -617,6 +605,42 @@ program
|
|
|
617
605
|
process.exit(1);
|
|
618
606
|
}
|
|
619
607
|
});
|
|
608
|
+
const workerCmd = program
|
|
609
|
+
.command('worker')
|
|
610
|
+
.description('Worker mode commands (for use inside containers)');
|
|
611
|
+
workerCmd
|
|
612
|
+
.command('sessions')
|
|
613
|
+
.argument('<subcommand>', 'Subcommand: list, messages, or delete')
|
|
614
|
+
.argument('[sessionId]', 'Session ID (required for messages and delete)')
|
|
615
|
+
.description('Manage OpenCode sessions')
|
|
616
|
+
.action(async (subcommand, sessionId) => {
|
|
617
|
+
const { listOpencodeSessions, getOpencodeSessionMessages, deleteOpencodeSession } = await import('./sessions/agents/opencode-storage');
|
|
618
|
+
if (subcommand === 'list') {
|
|
619
|
+
const sessions = await listOpencodeSessions();
|
|
620
|
+
console.log(JSON.stringify(sessions));
|
|
621
|
+
}
|
|
622
|
+
else if (subcommand === 'messages') {
|
|
623
|
+
if (!sessionId) {
|
|
624
|
+
console.error('Usage: perry worker sessions messages <session_id>');
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
const result = await getOpencodeSessionMessages(sessionId);
|
|
628
|
+
console.log(JSON.stringify(result));
|
|
629
|
+
}
|
|
630
|
+
else if (subcommand === 'delete') {
|
|
631
|
+
if (!sessionId) {
|
|
632
|
+
console.error('Usage: perry worker sessions delete <session_id>');
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const result = await deleteOpencodeSession(sessionId);
|
|
636
|
+
console.log(JSON.stringify(result));
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
640
|
+
console.error('Available: list, messages, delete');
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
620
644
|
function handleError(err) {
|
|
621
645
|
if (err instanceof ApiClientError) {
|
|
622
646
|
console.error(`Error: ${err.message}`);
|
|
Binary file
|
|
@@ -83,4 +83,23 @@ export const claudeProvider = {
|
|
|
83
83
|
(msg.content && msg.content.trim().length > 0));
|
|
84
84
|
return { id: sessionId, messages };
|
|
85
85
|
},
|
|
86
|
+
async deleteSession(containerName, sessionId, exec) {
|
|
87
|
+
const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
88
|
+
const findResult = await exec(containerName, [
|
|
89
|
+
'bash',
|
|
90
|
+
'-c',
|
|
91
|
+
`find /home/workspace/.claude/projects -name "${safeSessionId}.jsonl" -type f 2>/dev/null | head -1`,
|
|
92
|
+
], { user: 'workspace' });
|
|
93
|
+
if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
|
|
94
|
+
return { success: false, error: 'Session not found' };
|
|
95
|
+
}
|
|
96
|
+
const filePath = findResult.stdout.trim();
|
|
97
|
+
const rmResult = await exec(containerName, ['rm', '-f', filePath], {
|
|
98
|
+
user: 'workspace',
|
|
99
|
+
});
|
|
100
|
+
if (rmResult.exitCode !== 0) {
|
|
101
|
+
return { success: false, error: rmResult.stderr || 'Failed to delete session file' };
|
|
102
|
+
}
|
|
103
|
+
return { success: true };
|
|
104
|
+
},
|
|
86
105
|
};
|
|
@@ -107,4 +107,44 @@ export const codexProvider = {
|
|
|
107
107
|
}
|
|
108
108
|
return null;
|
|
109
109
|
},
|
|
110
|
+
async deleteSession(containerName, sessionId, exec) {
|
|
111
|
+
const findResult = await exec(containerName, ['bash', '-c', `find /home/workspace/.codex/sessions -name "*.jsonl" -type f 2>/dev/null`], { user: 'workspace' });
|
|
112
|
+
if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
|
|
113
|
+
return { success: false, error: 'No session files found' };
|
|
114
|
+
}
|
|
115
|
+
const files = findResult.stdout.trim().split('\n').filter(Boolean);
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const fileId = file.split('/').pop()?.replace('.jsonl', '') || '';
|
|
118
|
+
if (fileId === sessionId) {
|
|
119
|
+
const rmResult = await exec(containerName, ['rm', '-f', file], {
|
|
120
|
+
user: 'workspace',
|
|
121
|
+
});
|
|
122
|
+
if (rmResult.exitCode !== 0) {
|
|
123
|
+
return { success: false, error: rmResult.stderr || 'Failed to delete session file' };
|
|
124
|
+
}
|
|
125
|
+
return { success: true };
|
|
126
|
+
}
|
|
127
|
+
const headResult = await exec(containerName, ['head', '-1', file], {
|
|
128
|
+
user: 'workspace',
|
|
129
|
+
});
|
|
130
|
+
if (headResult.exitCode === 0) {
|
|
131
|
+
try {
|
|
132
|
+
const meta = JSON.parse(headResult.stdout);
|
|
133
|
+
if (meta.session_id === sessionId) {
|
|
134
|
+
const rmResult = await exec(containerName, ['rm', '-f', file], {
|
|
135
|
+
user: 'workspace',
|
|
136
|
+
});
|
|
137
|
+
if (rmResult.exitCode !== 0) {
|
|
138
|
+
return { success: false, error: rmResult.stderr || 'Failed to delete session file' };
|
|
139
|
+
}
|
|
140
|
+
return { success: true };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { success: false, error: 'Session not found' };
|
|
149
|
+
},
|
|
110
150
|
};
|
|
@@ -42,3 +42,66 @@ export async function findSessionMessages(containerName, sessionId, exec) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
+
export async function deleteSession(containerName, sessionId, agentType, exec) {
|
|
46
|
+
const provider = providers[agentType];
|
|
47
|
+
if (!provider) {
|
|
48
|
+
return { success: false, error: 'Unknown agent type' };
|
|
49
|
+
}
|
|
50
|
+
return provider.deleteSession(containerName, sessionId, exec);
|
|
51
|
+
}
|
|
52
|
+
export async function searchSessions(containerName, query, exec) {
|
|
53
|
+
const safeQuery = query.replace(/['"\\]/g, '\\$&');
|
|
54
|
+
const searchPaths = [
|
|
55
|
+
'/home/workspace/.claude/projects',
|
|
56
|
+
'/home/workspace/.local/share/opencode/storage',
|
|
57
|
+
'/home/workspace/.codex/sessions',
|
|
58
|
+
];
|
|
59
|
+
const rgCommand = `rg -l -i --no-messages "${safeQuery}" ${searchPaths.join(' ')} 2>/dev/null | head -100`;
|
|
60
|
+
const result = await exec(containerName, ['bash', '-c', rgCommand], {
|
|
61
|
+
user: 'workspace',
|
|
62
|
+
});
|
|
63
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const files = result.stdout.trim().split('\n').filter(Boolean);
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
let sessionId = null;
|
|
70
|
+
let agentType = null;
|
|
71
|
+
if (file.includes('/.claude/projects/')) {
|
|
72
|
+
const match = file.match(/\/([^/]+)\.jsonl$/);
|
|
73
|
+
if (match && !match[1].startsWith('agent-')) {
|
|
74
|
+
sessionId = match[1];
|
|
75
|
+
agentType = 'claude-code';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (file.includes('/.local/share/opencode/storage/')) {
|
|
79
|
+
if (file.includes('/session/') && file.endsWith('.json')) {
|
|
80
|
+
const match = file.match(/\/(ses_[^/]+)\.json$/);
|
|
81
|
+
if (match) {
|
|
82
|
+
sessionId = match[1];
|
|
83
|
+
agentType = 'opencode';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (file.includes('/part/') || file.includes('/message/')) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (file.includes('/.codex/sessions/')) {
|
|
91
|
+
const match = file.match(/\/([^/]+)\.jsonl$/);
|
|
92
|
+
if (match) {
|
|
93
|
+
sessionId = match[1];
|
|
94
|
+
agentType = 'codex';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (sessionId && agentType) {
|
|
98
|
+
results.push({
|
|
99
|
+
sessionId,
|
|
100
|
+
agentType,
|
|
101
|
+
filePath: file,
|
|
102
|
+
matchCount: 1,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
function getStorageBase(homeDir) {
|
|
5
|
+
const home = homeDir || os.homedir();
|
|
6
|
+
return path.join(home, '.local', 'share', 'opencode', 'storage');
|
|
7
|
+
}
|
|
8
|
+
export async function listOpencodeSessions(homeDir) {
|
|
9
|
+
const storageBase = getStorageBase(homeDir);
|
|
10
|
+
const sessionDir = path.join(storageBase, 'session');
|
|
11
|
+
const sessions = [];
|
|
12
|
+
try {
|
|
13
|
+
const projectDirs = await fs.readdir(sessionDir, { withFileTypes: true });
|
|
14
|
+
for (const projectDir of projectDirs) {
|
|
15
|
+
if (!projectDir.isDirectory())
|
|
16
|
+
continue;
|
|
17
|
+
const projectPath = path.join(sessionDir, projectDir.name);
|
|
18
|
+
const sessionFiles = await fs.readdir(projectPath);
|
|
19
|
+
for (const sessionFile of sessionFiles) {
|
|
20
|
+
if (!sessionFile.startsWith('ses_') || !sessionFile.endsWith('.json'))
|
|
21
|
+
continue;
|
|
22
|
+
const filePath = path.join(projectPath, sessionFile);
|
|
23
|
+
try {
|
|
24
|
+
const stat = await fs.stat(filePath);
|
|
25
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
26
|
+
const data = JSON.parse(content);
|
|
27
|
+
if (!data.id)
|
|
28
|
+
continue;
|
|
29
|
+
sessions.push({
|
|
30
|
+
id: data.id,
|
|
31
|
+
title: data.title || '',
|
|
32
|
+
directory: data.directory || '',
|
|
33
|
+
mtime: data.time?.updated || Math.floor(stat.mtimeMs),
|
|
34
|
+
file: filePath,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Storage doesn't exist
|
|
45
|
+
}
|
|
46
|
+
return sessions;
|
|
47
|
+
}
|
|
48
|
+
export async function getOpencodeSessionMessages(sessionId, homeDir) {
|
|
49
|
+
const storageBase = getStorageBase(homeDir);
|
|
50
|
+
const sessionDir = path.join(storageBase, 'session');
|
|
51
|
+
const messageDir = path.join(storageBase, 'message');
|
|
52
|
+
const partDir = path.join(storageBase, 'part');
|
|
53
|
+
const sessionFile = await findSessionFile(sessionDir, sessionId);
|
|
54
|
+
if (!sessionFile) {
|
|
55
|
+
return { id: sessionId, messages: [] };
|
|
56
|
+
}
|
|
57
|
+
let internalId;
|
|
58
|
+
try {
|
|
59
|
+
const content = await fs.readFile(sessionFile, 'utf-8');
|
|
60
|
+
const data = JSON.parse(content);
|
|
61
|
+
internalId = data.id;
|
|
62
|
+
if (!internalId) {
|
|
63
|
+
return { id: sessionId, messages: [] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { id: sessionId, messages: [] };
|
|
68
|
+
}
|
|
69
|
+
const msgDir = path.join(messageDir, internalId);
|
|
70
|
+
const messages = [];
|
|
71
|
+
try {
|
|
72
|
+
const msgFiles = (await fs.readdir(msgDir))
|
|
73
|
+
.filter((f) => f.startsWith('msg_') && f.endsWith('.json'))
|
|
74
|
+
.sort();
|
|
75
|
+
for (const msgFile of msgFiles) {
|
|
76
|
+
const msgPath = path.join(msgDir, msgFile);
|
|
77
|
+
try {
|
|
78
|
+
const content = await fs.readFile(msgPath, 'utf-8');
|
|
79
|
+
const msg = JSON.parse(content);
|
|
80
|
+
if (!msg.id || (msg.role !== 'user' && msg.role !== 'assistant'))
|
|
81
|
+
continue;
|
|
82
|
+
const partMsgDir = path.join(partDir, msg.id);
|
|
83
|
+
try {
|
|
84
|
+
const partFiles = (await fs.readdir(partMsgDir))
|
|
85
|
+
.filter((f) => f.startsWith('prt_') && f.endsWith('.json'))
|
|
86
|
+
.sort();
|
|
87
|
+
for (const partFile of partFiles) {
|
|
88
|
+
const partPath = path.join(partMsgDir, partFile);
|
|
89
|
+
try {
|
|
90
|
+
const partContent = await fs.readFile(partPath, 'utf-8');
|
|
91
|
+
const part = JSON.parse(partContent);
|
|
92
|
+
const timestamp = msg.time?.created
|
|
93
|
+
? new Date(msg.time.created).toISOString()
|
|
94
|
+
: undefined;
|
|
95
|
+
if (part.type === 'text' && part.text) {
|
|
96
|
+
messages.push({
|
|
97
|
+
type: msg.role,
|
|
98
|
+
content: part.text,
|
|
99
|
+
timestamp,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (part.type === 'tool') {
|
|
103
|
+
const toolName = part.state?.title || part.tool || '';
|
|
104
|
+
const callId = part.callID || part.id || '';
|
|
105
|
+
messages.push({
|
|
106
|
+
type: 'tool_use',
|
|
107
|
+
toolName,
|
|
108
|
+
toolId: callId,
|
|
109
|
+
toolInput: part.state?.input ? JSON.stringify(part.state.input) : '',
|
|
110
|
+
timestamp,
|
|
111
|
+
});
|
|
112
|
+
if (part.state?.output) {
|
|
113
|
+
messages.push({
|
|
114
|
+
type: 'tool_result',
|
|
115
|
+
content: part.state.output,
|
|
116
|
+
toolId: callId,
|
|
117
|
+
timestamp,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// No messages
|
|
138
|
+
}
|
|
139
|
+
return { id: sessionId, messages };
|
|
140
|
+
}
|
|
141
|
+
async function findSessionFile(sessionDir, sessionId) {
|
|
142
|
+
try {
|
|
143
|
+
const projectDirs = await fs.readdir(sessionDir, { withFileTypes: true });
|
|
144
|
+
for (const projectDir of projectDirs) {
|
|
145
|
+
if (!projectDir.isDirectory())
|
|
146
|
+
continue;
|
|
147
|
+
const filePath = path.join(sessionDir, projectDir.name, `${sessionId}.json`);
|
|
148
|
+
try {
|
|
149
|
+
await fs.access(filePath);
|
|
150
|
+
return filePath;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Directory doesn't exist
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
export async function deleteOpencodeSession(sessionId, homeDir) {
|
|
163
|
+
const storageBase = getStorageBase(homeDir);
|
|
164
|
+
const sessionDir = path.join(storageBase, 'session');
|
|
165
|
+
const messageDir = path.join(storageBase, 'message');
|
|
166
|
+
const partDir = path.join(storageBase, 'part');
|
|
167
|
+
const sessionFile = await findSessionFile(sessionDir, sessionId);
|
|
168
|
+
if (!sessionFile) {
|
|
169
|
+
return { success: false, error: 'Session not found' };
|
|
170
|
+
}
|
|
171
|
+
let internalId = null;
|
|
172
|
+
try {
|
|
173
|
+
const content = await fs.readFile(sessionFile, 'utf-8');
|
|
174
|
+
const data = JSON.parse(content);
|
|
175
|
+
internalId = data.id;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Continue with session file deletion only
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
await fs.unlink(sessionFile);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return { success: false, error: `Failed to delete session file: ${err}` };
|
|
185
|
+
}
|
|
186
|
+
if (internalId) {
|
|
187
|
+
const msgDir = path.join(messageDir, internalId);
|
|
188
|
+
try {
|
|
189
|
+
const msgFiles = await fs.readdir(msgDir);
|
|
190
|
+
for (const msgFile of msgFiles) {
|
|
191
|
+
if (!msgFile.startsWith('msg_') || !msgFile.endsWith('.json'))
|
|
192
|
+
continue;
|
|
193
|
+
const msgPath = path.join(msgDir, msgFile);
|
|
194
|
+
try {
|
|
195
|
+
const content = await fs.readFile(msgPath, 'utf-8');
|
|
196
|
+
const msg = JSON.parse(content);
|
|
197
|
+
if (msg.id) {
|
|
198
|
+
const partMsgDir = path.join(partDir, msg.id);
|
|
199
|
+
try {
|
|
200
|
+
await fs.rm(partMsgDir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Parts may not exist
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Skip malformed messages
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
await fs.rm(msgDir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Messages directory may not exist
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { success: true };
|
|
218
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const opencodeProvider = {
|
|
2
2
|
async discoverSessions(containerName, exec) {
|
|
3
|
-
const result = await exec(containerName, ['perry
|
|
3
|
+
const result = await exec(containerName, ['perry', 'worker', 'sessions', 'list'], {
|
|
4
4
|
user: 'workspace',
|
|
5
5
|
});
|
|
6
6
|
if (result.exitCode !== 0) {
|
|
@@ -22,7 +22,7 @@ export const opencodeProvider = {
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
async getSessionDetails(containerName, rawSession, exec) {
|
|
25
|
-
const result = await exec(containerName, ['perry
|
|
25
|
+
const result = await exec(containerName, ['perry', 'worker', 'sessions', 'messages', rawSession.id], {
|
|
26
26
|
user: 'workspace',
|
|
27
27
|
});
|
|
28
28
|
if (result.exitCode !== 0) {
|
|
@@ -50,7 +50,7 @@ export const opencodeProvider = {
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
async getSessionMessages(containerName, sessionId, exec) {
|
|
53
|
-
const result = await exec(containerName, ['perry
|
|
53
|
+
const result = await exec(containerName, ['perry', 'worker', 'sessions', 'messages', sessionId], {
|
|
54
54
|
user: 'workspace',
|
|
55
55
|
});
|
|
56
56
|
if (result.exitCode !== 0) {
|
|
@@ -67,4 +67,18 @@ export const opencodeProvider = {
|
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
|
+
async deleteSession(containerName, sessionId, exec) {
|
|
71
|
+
const result = await exec(containerName, ['perry', 'worker', 'sessions', 'delete', sessionId], {
|
|
72
|
+
user: 'workspace',
|
|
73
|
+
});
|
|
74
|
+
if (result.exitCode !== 0) {
|
|
75
|
+
return { success: false, error: result.stderr || 'Failed to delete session' };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(result.stdout);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { success: false, error: 'Invalid response from worker' };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
70
84
|
};
|
package/dist/sessions/cache.js
CHANGED
|
@@ -49,4 +49,9 @@ export class SessionsCacheManager {
|
|
|
49
49
|
cache.recent = cache.recent.filter((s) => s.workspaceName !== workspaceName);
|
|
50
50
|
await this.save();
|
|
51
51
|
}
|
|
52
|
+
async removeSession(workspaceName, sessionId) {
|
|
53
|
+
const cache = await this.load();
|
|
54
|
+
cache.recent = cache.recent.filter((s) => !(s.workspaceName === workspaceName && s.sessionId === sessionId));
|
|
55
|
+
await this.save();
|
|
56
|
+
}
|
|
52
57
|
}
|
package/dist/shared/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const DEFAULT_AGENT_PORT = 7391;
|
|
2
2
|
export const SSH_PORT_RANGE_START = 2200;
|
|
3
3
|
export const SSH_PORT_RANGE_END = 2400;
|
|
4
|
-
export const WORKSPACE_IMAGE_LOCAL = '
|
|
4
|
+
export const WORKSPACE_IMAGE_LOCAL = 'perry:latest';
|
|
5
5
|
export const WORKSPACE_IMAGE_REGISTRY = 'ghcr.io/gricha/perry';
|
|
6
6
|
export const VOLUME_PREFIX = 'workspace-';
|
|
7
7
|
export const CONTAINER_PREFIX = 'workspace-';
|