@gricha/perry 0.3.3 → 0.3.5
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/dist/index.js +17 -2
- package/dist/perry-worker +0 -0
- package/dist/session-manager/adapters/opencode.js +116 -30
- package/dist/update-checker.js +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -662,9 +662,24 @@ sshCmd
|
|
|
662
662
|
program
|
|
663
663
|
.command('update')
|
|
664
664
|
.description('Update Perry to the latest version')
|
|
665
|
-
.
|
|
665
|
+
.option('-f, --force', 'Force update even if already on latest version')
|
|
666
|
+
.action(async (options) => {
|
|
667
|
+
const { fetchLatestVersion, compareVersions } = await import('./update-checker.js');
|
|
668
|
+
const currentVersion = pkg.version;
|
|
669
|
+
console.log(`Current version: ${currentVersion}`);
|
|
670
|
+
console.log('Checking for updates...');
|
|
671
|
+
const latestVersion = await fetchLatestVersion();
|
|
672
|
+
if (!latestVersion) {
|
|
673
|
+
console.error('Failed to fetch latest version. Please try again later.');
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
console.log(`Latest version: ${latestVersion}`);
|
|
677
|
+
if (compareVersions(currentVersion, latestVersion) <= 0 && !options.force) {
|
|
678
|
+
console.log('Already up to date.');
|
|
679
|
+
process.exit(0);
|
|
680
|
+
}
|
|
681
|
+
console.log(`Updating Perry from ${currentVersion} to ${latestVersion}...`);
|
|
666
682
|
const { spawn } = await import('child_process');
|
|
667
|
-
console.log('Updating Perry...');
|
|
668
683
|
const child = spawn('bash', ['-c', 'curl -fsSL https://raw.githubusercontent.com/gricha/perry/main/install.sh | bash'], {
|
|
669
684
|
stdio: 'inherit',
|
|
670
685
|
});
|
package/dist/perry-worker
CHANGED
|
Binary file
|
|
@@ -3,6 +3,9 @@ const MESSAGE_TIMEOUT_MS = 30000;
|
|
|
3
3
|
const SSE_TIMEOUT_MS = 120000;
|
|
4
4
|
const serverPorts = new Map();
|
|
5
5
|
const serverStarting = new Map();
|
|
6
|
+
let hostServerPort = null;
|
|
7
|
+
let hostServerStarting = null;
|
|
8
|
+
let hostServerProcess = null;
|
|
6
9
|
async function findAvailablePort(containerName) {
|
|
7
10
|
const script = `import socket; s=socket.socket(); s.bind(('', 0)); print(s.getsockname()[1]); s.close()`;
|
|
8
11
|
const result = await execInContainer(containerName, ['python3', '-c', script], {
|
|
@@ -70,6 +73,7 @@ export class OpenCodeAdapter {
|
|
|
70
73
|
model;
|
|
71
74
|
status = 'idle';
|
|
72
75
|
port;
|
|
76
|
+
isHost = false;
|
|
73
77
|
sseProcess = null;
|
|
74
78
|
messageCallback;
|
|
75
79
|
statusCallback;
|
|
@@ -84,14 +88,17 @@ export class OpenCodeAdapter {
|
|
|
84
88
|
this.errorCallback = callback;
|
|
85
89
|
}
|
|
86
90
|
async start(options) {
|
|
87
|
-
|
|
88
|
-
throw new Error('OpenCode adapter does not support host mode');
|
|
89
|
-
}
|
|
91
|
+
this.isHost = options.isHost;
|
|
90
92
|
this.containerName = options.containerName;
|
|
91
93
|
this.agentSessionId = options.agentSessionId;
|
|
92
94
|
this.model = options.model;
|
|
93
95
|
try {
|
|
94
|
-
|
|
96
|
+
if (this.isHost) {
|
|
97
|
+
this.port = await this.startServerHost();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.port = await startServer(this.containerName);
|
|
101
|
+
}
|
|
95
102
|
this.setStatus('idle');
|
|
96
103
|
}
|
|
97
104
|
catch (err) {
|
|
@@ -99,8 +106,61 @@ export class OpenCodeAdapter {
|
|
|
99
106
|
throw err;
|
|
100
107
|
}
|
|
101
108
|
}
|
|
109
|
+
async startServerHost() {
|
|
110
|
+
if (hostServerPort && (await this.isServerRunningHost(hostServerPort))) {
|
|
111
|
+
return hostServerPort;
|
|
112
|
+
}
|
|
113
|
+
if (hostServerStarting) {
|
|
114
|
+
return hostServerStarting;
|
|
115
|
+
}
|
|
116
|
+
const startPromise = (async () => {
|
|
117
|
+
const port = await this.findAvailablePortHost();
|
|
118
|
+
console.log(`[opencode] Starting server on port ${port} on host`);
|
|
119
|
+
hostServerProcess = Bun.spawn(['opencode', 'serve', '--port', String(port), '--hostname', '127.0.0.1'], {
|
|
120
|
+
stdin: 'ignore',
|
|
121
|
+
stdout: 'pipe',
|
|
122
|
+
stderr: 'pipe',
|
|
123
|
+
});
|
|
124
|
+
for (let i = 0; i < 30; i++) {
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
126
|
+
if (await this.isServerRunningHost(port)) {
|
|
127
|
+
console.log(`[opencode] Server ready on port ${port}`);
|
|
128
|
+
hostServerPort = port;
|
|
129
|
+
hostServerStarting = null;
|
|
130
|
+
return port;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
hostServerStarting = null;
|
|
134
|
+
if (hostServerProcess) {
|
|
135
|
+
hostServerProcess.kill();
|
|
136
|
+
await hostServerProcess.exited;
|
|
137
|
+
hostServerProcess = null;
|
|
138
|
+
}
|
|
139
|
+
throw new Error('Failed to start OpenCode server on host');
|
|
140
|
+
})();
|
|
141
|
+
hostServerStarting = startPromise;
|
|
142
|
+
return startPromise;
|
|
143
|
+
}
|
|
144
|
+
async findAvailablePortHost() {
|
|
145
|
+
const server = Bun.serve({
|
|
146
|
+
port: 0,
|
|
147
|
+
fetch: () => new Response(''),
|
|
148
|
+
});
|
|
149
|
+
const port = server.port;
|
|
150
|
+
server.stop();
|
|
151
|
+
return port;
|
|
152
|
+
}
|
|
153
|
+
async isServerRunningHost(port) {
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetch(`http://localhost:${port}/session`, { method: 'GET' });
|
|
156
|
+
return response.ok;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
102
162
|
async sendMessage(message) {
|
|
103
|
-
if (!this.
|
|
163
|
+
if (!this.port) {
|
|
104
164
|
const err = new Error('Adapter not started');
|
|
105
165
|
this.emitError(err);
|
|
106
166
|
throw err;
|
|
@@ -131,7 +191,20 @@ export class OpenCodeAdapter {
|
|
|
131
191
|
}
|
|
132
192
|
}
|
|
133
193
|
async createSession(baseUrl) {
|
|
134
|
-
const payload = this.model ?
|
|
194
|
+
const payload = this.model ? { model: this.model } : {};
|
|
195
|
+
if (this.isHost) {
|
|
196
|
+
const response = await fetch(`${baseUrl}/session`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
body: JSON.stringify(payload),
|
|
200
|
+
signal: AbortSignal.timeout(MESSAGE_TIMEOUT_MS),
|
|
201
|
+
});
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
throw new Error(`Failed to create session: ${response.statusText}`);
|
|
204
|
+
}
|
|
205
|
+
const session = await response.json();
|
|
206
|
+
return session.id;
|
|
207
|
+
}
|
|
135
208
|
const result = await execInContainer(this.containerName, [
|
|
136
209
|
'curl',
|
|
137
210
|
'-s',
|
|
@@ -144,7 +217,7 @@ export class OpenCodeAdapter {
|
|
|
144
217
|
'-H',
|
|
145
218
|
'Content-Type: application/json',
|
|
146
219
|
'-d',
|
|
147
|
-
payload,
|
|
220
|
+
JSON.stringify(payload),
|
|
148
221
|
], { user: 'workspace' });
|
|
149
222
|
if (result.exitCode !== 0) {
|
|
150
223
|
throw new Error(`Failed to create session: ${result.stderr || 'Unknown error'}`);
|
|
@@ -155,23 +228,36 @@ export class OpenCodeAdapter {
|
|
|
155
228
|
async sendAndStream(baseUrl, message) {
|
|
156
229
|
const sseReady = this.startSSEStream();
|
|
157
230
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
158
|
-
const payload =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
231
|
+
const payload = { parts: [{ type: 'text', text: message }] };
|
|
232
|
+
if (this.isHost) {
|
|
233
|
+
const response = await fetch(`${baseUrl}/session/${this.agentSessionId}/message`, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify(payload),
|
|
237
|
+
signal: AbortSignal.timeout(MESSAGE_TIMEOUT_MS),
|
|
238
|
+
});
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error(`Failed to send message: ${response.statusText}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const result = await execInContainer(this.containerName, [
|
|
245
|
+
'curl',
|
|
246
|
+
'-s',
|
|
247
|
+
'-f',
|
|
248
|
+
'--max-time',
|
|
249
|
+
String(MESSAGE_TIMEOUT_MS / 1000),
|
|
250
|
+
'-X',
|
|
251
|
+
'POST',
|
|
252
|
+
`${baseUrl}/session/${this.agentSessionId}/message`,
|
|
253
|
+
'-H',
|
|
254
|
+
'Content-Type: application/json',
|
|
255
|
+
'-d',
|
|
256
|
+
JSON.stringify(payload),
|
|
257
|
+
], { user: 'workspace' });
|
|
258
|
+
if (result.exitCode !== 0) {
|
|
259
|
+
throw new Error(`Failed to send message: ${result.stderr || 'Connection failed'}`);
|
|
260
|
+
}
|
|
175
261
|
}
|
|
176
262
|
await sseReady;
|
|
177
263
|
}
|
|
@@ -180,18 +266,18 @@ export class OpenCodeAdapter {
|
|
|
180
266
|
const seenTools = new Set();
|
|
181
267
|
let resolved = false;
|
|
182
268
|
let receivedIdle = false;
|
|
183
|
-
const
|
|
184
|
-
'docker',
|
|
185
|
-
'exec',
|
|
186
|
-
'-i',
|
|
187
|
-
this.containerName,
|
|
269
|
+
const curlArgs = [
|
|
188
270
|
'curl',
|
|
189
271
|
'-s',
|
|
190
272
|
'-N',
|
|
191
273
|
'--max-time',
|
|
192
274
|
String(SSE_TIMEOUT_MS / 1000),
|
|
193
275
|
`http://localhost:${this.port}/event`,
|
|
194
|
-
]
|
|
276
|
+
];
|
|
277
|
+
const spawnArgs = this.isHost
|
|
278
|
+
? curlArgs
|
|
279
|
+
: ['docker', 'exec', '-i', this.containerName, ...curlArgs];
|
|
280
|
+
const proc = Bun.spawn(spawnArgs, { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
|
|
195
281
|
this.sseProcess = proc;
|
|
196
282
|
const decoder = new TextDecoder();
|
|
197
283
|
let buffer = '';
|
package/dist/update-checker.js
CHANGED
|
@@ -27,7 +27,7 @@ async function writeCache(cache) {
|
|
|
27
27
|
// Ignore cache write errors
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async function fetchLatestVersion() {
|
|
30
|
+
export async function fetchLatestVersion() {
|
|
31
31
|
try {
|
|
32
32
|
const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
|
|
33
33
|
signal: AbortSignal.timeout(3000),
|
|
@@ -46,7 +46,7 @@ async function fetchLatestVersion() {
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
function compareVersions(current, latest) {
|
|
49
|
+
export function compareVersions(current, latest) {
|
|
50
50
|
const currentParts = current.split('.').map(Number);
|
|
51
51
|
const latestParts = latest.split('.').map(Number);
|
|
52
52
|
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|