@gricha/perry 0.2.3 → 0.2.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/README.md +3 -0
- package/dist/agent/file-watcher.js +2 -6
- package/dist/agent/router.js +116 -16
- package/dist/agent/run.js +15 -1
- package/dist/agent/web/assets/index-0UMxrAK_.js +104 -0
- package/dist/agent/web/assets/index-BwItLEFi.css +1 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/agents/__tests__/claude-code.test.js +125 -0
- package/dist/agents/__tests__/codex.test.js +64 -0
- package/dist/agents/__tests__/opencode.test.js +130 -0
- package/dist/agents/__tests__/sync.test.js +272 -0
- package/dist/agents/index.js +177 -0
- package/dist/agents/sync/claude-code.js +84 -0
- package/dist/agents/sync/codex.js +29 -0
- package/dist/agents/sync/copier.js +89 -0
- package/dist/agents/sync/opencode.js +51 -0
- package/dist/agents/sync/types.js +1 -0
- package/dist/agents/types.js +1 -0
- package/dist/chat/base-chat-websocket.js +1 -1
- package/dist/chat/opencode-websocket.js +1 -1
- package/dist/chat/websocket.js +2 -2
- package/dist/client/api.js +25 -0
- package/dist/config/loader.js +20 -2
- package/dist/docker/eager-pull.js +19 -3
- package/dist/docker/index.js +27 -0
- package/dist/index.js +83 -12
- package/dist/perry-worker +0 -0
- package/dist/workspace/manager.js +178 -115
- package/package.json +1 -1
- package/dist/agent/web/assets/index-BF-4SpMu.js +0 -104
- package/dist/agent/web/assets/index-DIOWcVH-.css +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const opencodeSync = {
|
|
2
|
+
getRequiredDirs() {
|
|
3
|
+
return ['/home/workspace/.config/opencode'];
|
|
4
|
+
},
|
|
5
|
+
async getFilesToSync(_context) {
|
|
6
|
+
return [];
|
|
7
|
+
},
|
|
8
|
+
async getDirectoriesToSync(_context) {
|
|
9
|
+
return [];
|
|
10
|
+
},
|
|
11
|
+
async getGeneratedConfigs(context) {
|
|
12
|
+
const zenToken = context.agentConfig.agents?.opencode?.zen_token;
|
|
13
|
+
if (!zenToken) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const hostConfigContent = await context.readHostFile('~/.config/opencode/opencode.json');
|
|
17
|
+
let mcpConfig = {};
|
|
18
|
+
if (hostConfigContent) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(hostConfigContent);
|
|
21
|
+
if (parsed.mcp && typeof parsed.mcp === 'object') {
|
|
22
|
+
mcpConfig = parsed.mcp;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Invalid JSON, ignore
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const config = {
|
|
30
|
+
provider: {
|
|
31
|
+
opencode: {
|
|
32
|
+
options: {
|
|
33
|
+
apiKey: zenToken,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
model: 'opencode/claude-sonnet-4',
|
|
38
|
+
};
|
|
39
|
+
if (Object.keys(mcpConfig).length > 0) {
|
|
40
|
+
config.mcp = mcpConfig;
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
dest: '/home/workspace/.config/opencode/opencode.json',
|
|
45
|
+
content: JSON.stringify(config, null, 2),
|
|
46
|
+
permissions: '600',
|
|
47
|
+
category: 'credential',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -41,7 +41,7 @@ export class BaseChatWebSocketServer extends BaseWebSocketServer {
|
|
|
41
41
|
};
|
|
42
42
|
if (!connection.session) {
|
|
43
43
|
if (isHostMode) {
|
|
44
|
-
connection.session = this.createHostSession(message.sessionId, onMessage, message.model);
|
|
44
|
+
connection.session = this.createHostSession(message.sessionId, onMessage, message.model, message.projectPath);
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
47
|
const containerName = getContainerName(workspaceName);
|
|
@@ -15,7 +15,7 @@ export class OpencodeWebSocketServer extends BaseChatWebSocketServer {
|
|
|
15
15
|
workspaceName,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
createHostSession(sessionId, onMessage, messageModel) {
|
|
18
|
+
createHostSession(sessionId, onMessage, messageModel, _projectPath) {
|
|
19
19
|
const model = messageModel || this.getConfig?.()?.agents?.opencode?.model;
|
|
20
20
|
return createHostOpencodeSession({ sessionId, model }, onMessage);
|
|
21
21
|
}
|
package/dist/chat/websocket.js
CHANGED
|
@@ -15,10 +15,10 @@ export class ChatWebSocketServer extends BaseChatWebSocketServer {
|
|
|
15
15
|
workspaceName,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
createHostSession(sessionId, onMessage, messageModel) {
|
|
18
|
+
createHostSession(sessionId, onMessage, messageModel, projectPath) {
|
|
19
19
|
const config = this.getConfig();
|
|
20
20
|
const model = messageModel || config.agents?.claude_code?.model;
|
|
21
|
-
return createHostChatSession({ sessionId, model }, onMessage);
|
|
21
|
+
return createHostChatSession({ sessionId, model, workDir: projectPath }, onMessage);
|
|
22
22
|
}
|
|
23
23
|
createContainerSession(containerName, sessionId, onMessage, messageModel) {
|
|
24
24
|
const config = this.getConfig();
|
package/dist/client/api.js
CHANGED
|
@@ -98,6 +98,31 @@ export class ApiClient {
|
|
|
98
98
|
throw this.wrapError(err);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
async getPortForwards(name) {
|
|
102
|
+
try {
|
|
103
|
+
const result = await this.client.workspaces.getPortForwards({ name });
|
|
104
|
+
return result.forwards;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw this.wrapError(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async setPortForwards(name, forwards) {
|
|
111
|
+
try {
|
|
112
|
+
return await this.client.workspaces.setPortForwards({ name, forwards });
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw this.wrapError(err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async cloneWorkspace(sourceName, cloneName) {
|
|
119
|
+
try {
|
|
120
|
+
return await this.client.workspaces.clone({ sourceName, cloneName });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw this.wrapError(err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
101
126
|
getTerminalUrl(name) {
|
|
102
127
|
const wsUrl = this.baseUrl.replace(/^http/, 'ws');
|
|
103
128
|
return `${wsUrl}/rpc/terminal/${encodeURIComponent(name)}`;
|
package/dist/config/loader.js
CHANGED
|
@@ -16,7 +16,10 @@ export function createDefaultAgentConfig() {
|
|
|
16
16
|
env: {},
|
|
17
17
|
files: {},
|
|
18
18
|
},
|
|
19
|
-
scripts: {
|
|
19
|
+
scripts: {
|
|
20
|
+
post_start: ['~/.perry/userscripts'],
|
|
21
|
+
fail_on_error: false,
|
|
22
|
+
},
|
|
20
23
|
agents: {},
|
|
21
24
|
allowHostAccess: true,
|
|
22
25
|
ssh: {
|
|
@@ -29,6 +32,18 @@ export function createDefaultAgentConfig() {
|
|
|
29
32
|
},
|
|
30
33
|
};
|
|
31
34
|
}
|
|
35
|
+
function migratePostStart(value) {
|
|
36
|
+
if (!value) {
|
|
37
|
+
return ['~/.perry/userscripts'];
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
return [value, '~/.perry/userscripts'];
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
return value.length > 0 ? value : ['~/.perry/userscripts'];
|
|
44
|
+
}
|
|
45
|
+
return ['~/.perry/userscripts'];
|
|
46
|
+
}
|
|
32
47
|
export async function loadAgentConfig(configDir) {
|
|
33
48
|
const dir = getConfigDir(configDir);
|
|
34
49
|
const configPath = path.join(dir, CONFIG_FILE);
|
|
@@ -41,7 +56,10 @@ export async function loadAgentConfig(configDir) {
|
|
|
41
56
|
env: config.credentials?.env || {},
|
|
42
57
|
files: config.credentials?.files || {},
|
|
43
58
|
},
|
|
44
|
-
scripts:
|
|
59
|
+
scripts: {
|
|
60
|
+
post_start: migratePostStart(config.scripts?.post_start),
|
|
61
|
+
fail_on_error: config.scripts?.fail_on_error ?? false,
|
|
62
|
+
},
|
|
45
63
|
agents: config.agents || {},
|
|
46
64
|
allowHostAccess: config.allowHostAccess ?? true,
|
|
47
65
|
ssh: {
|
|
@@ -5,6 +5,7 @@ const RETRY_INTERVAL_MS = 20000;
|
|
|
5
5
|
const MAX_RETRIES = 10;
|
|
6
6
|
let pullInProgress = false;
|
|
7
7
|
let pullComplete = false;
|
|
8
|
+
let abortController = null;
|
|
8
9
|
async function isDockerAvailable() {
|
|
9
10
|
try {
|
|
10
11
|
await getDockerVersion();
|
|
@@ -35,7 +36,13 @@ export async function startEagerImagePull() {
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
pullInProgress = true;
|
|
39
|
+
abortController = new AbortController();
|
|
40
|
+
const signal = abortController.signal;
|
|
38
41
|
const attemptPull = async (attempt) => {
|
|
42
|
+
if (signal.aborted) {
|
|
43
|
+
pullInProgress = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
39
46
|
if (attempt > MAX_RETRIES) {
|
|
40
47
|
console.log('[agent] Max retries reached for image pull - giving up background pull');
|
|
41
48
|
pullInProgress = false;
|
|
@@ -46,7 +53,8 @@ export async function startEagerImagePull() {
|
|
|
46
53
|
if (attempt === 1) {
|
|
47
54
|
console.log('[agent] Docker not available - will retry in background');
|
|
48
55
|
}
|
|
49
|
-
setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
56
|
+
const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
57
|
+
timer.unref();
|
|
50
58
|
return;
|
|
51
59
|
}
|
|
52
60
|
const success = await pullWorkspaceImage();
|
|
@@ -54,12 +62,20 @@ export async function startEagerImagePull() {
|
|
|
54
62
|
pullComplete = true;
|
|
55
63
|
pullInProgress = false;
|
|
56
64
|
}
|
|
57
|
-
else {
|
|
58
|
-
setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
65
|
+
else if (!signal.aborted) {
|
|
66
|
+
const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS);
|
|
67
|
+
timer.unref();
|
|
59
68
|
}
|
|
60
69
|
};
|
|
61
70
|
attemptPull(1);
|
|
62
71
|
}
|
|
72
|
+
export function stopEagerImagePull() {
|
|
73
|
+
if (abortController) {
|
|
74
|
+
abortController.abort();
|
|
75
|
+
abortController = null;
|
|
76
|
+
}
|
|
77
|
+
pullInProgress = false;
|
|
78
|
+
}
|
|
63
79
|
export function isImagePullComplete() {
|
|
64
80
|
return pullComplete;
|
|
65
81
|
}
|
package/dist/docker/index.js
CHANGED
|
@@ -410,3 +410,30 @@ export async function getLogs(containerName, options = {}) {
|
|
|
410
410
|
const { stdout, stderr } = await docker(args);
|
|
411
411
|
return stdout + stderr;
|
|
412
412
|
}
|
|
413
|
+
export async function cloneVolume(sourceVolume, destVolume) {
|
|
414
|
+
if (!(await volumeExists(sourceVolume))) {
|
|
415
|
+
throw new Error(`Source volume '${sourceVolume}' does not exist`);
|
|
416
|
+
}
|
|
417
|
+
if (await volumeExists(destVolume)) {
|
|
418
|
+
throw new Error(`Volume '${destVolume}' already exists`);
|
|
419
|
+
}
|
|
420
|
+
await createVolume(destVolume);
|
|
421
|
+
try {
|
|
422
|
+
await docker([
|
|
423
|
+
'run',
|
|
424
|
+
'--rm',
|
|
425
|
+
'-v',
|
|
426
|
+
`${sourceVolume}:/source:ro`,
|
|
427
|
+
'-v',
|
|
428
|
+
`${destVolume}:/dest`,
|
|
429
|
+
'alpine',
|
|
430
|
+
'sh',
|
|
431
|
+
'-c',
|
|
432
|
+
'cp -a /source/. /dest/',
|
|
433
|
+
]);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
await removeVolume(destVolume, true).catch(() => { });
|
|
437
|
+
throw new Error(`Failed to clone volume: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -177,6 +177,23 @@ program
|
|
|
177
177
|
handleError(err);
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
|
+
program
|
|
181
|
+
.command('clone <source> <clone-name>')
|
|
182
|
+
.description('Clone an existing workspace')
|
|
183
|
+
.action(async (source, cloneName) => {
|
|
184
|
+
try {
|
|
185
|
+
const client = await getClient();
|
|
186
|
+
console.log(`Cloning workspace '${source}' to '${cloneName}'...`);
|
|
187
|
+
console.log('This may take a while for large workspaces.');
|
|
188
|
+
const workspace = await client.cloneWorkspace(source, cloneName);
|
|
189
|
+
console.log(`Workspace '${cloneName}' created.`);
|
|
190
|
+
console.log(` Status: ${workspace.status}`);
|
|
191
|
+
console.log(` SSH Port: ${workspace.ports.ssh}`);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
handleError(err);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
180
197
|
program
|
|
181
198
|
.command('info [name]')
|
|
182
199
|
.description('Show workspace or agent info')
|
|
@@ -288,15 +305,21 @@ program
|
|
|
288
305
|
console.error(`Workspace '${name}' is not running (status: ${workspace.status})`);
|
|
289
306
|
process.exit(1);
|
|
290
307
|
}
|
|
308
|
+
let effectivePorts = ports;
|
|
291
309
|
if (ports.length === 0) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
310
|
+
const configuredForwards = workspace.ports.forwards || [];
|
|
311
|
+
if (configuredForwards.length === 0) {
|
|
312
|
+
console.log(`No ports configured for workspace '${name}'.`);
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log('Configure ports with: perry ports <name> <port> [<port>...]');
|
|
315
|
+
console.log(' Example: perry ports ' + name + ' 3000 5173');
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log('Or specify ports directly: perry proxy <name> <port> [<port>...]');
|
|
318
|
+
console.log(' Example: perry proxy ' + name + ' 3000 5173');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
effectivePorts = configuredForwards.map((p) => String(p));
|
|
322
|
+
console.log(`Using configured ports: ${configuredForwards.join(', ')}`);
|
|
300
323
|
}
|
|
301
324
|
if (isLocalWorker(worker)) {
|
|
302
325
|
const containerName = getContainerName(name);
|
|
@@ -305,7 +328,7 @@ program
|
|
|
305
328
|
console.error(`Could not get IP for container '${containerName}'`);
|
|
306
329
|
process.exit(1);
|
|
307
330
|
}
|
|
308
|
-
const forwards =
|
|
331
|
+
const forwards = effectivePorts.map(parseDockerPortForward);
|
|
309
332
|
console.log(`Forwarding ports: ${formatDockerPortForwards(forwards)}`);
|
|
310
333
|
console.log(`Container IP: ${containerIp}`);
|
|
311
334
|
console.log('Press Ctrl+C to stop.');
|
|
@@ -330,7 +353,7 @@ program
|
|
|
330
353
|
await new Promise(() => { });
|
|
331
354
|
}
|
|
332
355
|
else {
|
|
333
|
-
const forwards =
|
|
356
|
+
const forwards = effectivePorts.map(parsePortForward);
|
|
334
357
|
console.log(`Forwarding ports: ${formatPortForwards(forwards)}`);
|
|
335
358
|
console.log('Press Ctrl+C to stop.');
|
|
336
359
|
console.log('');
|
|
@@ -354,6 +377,47 @@ program
|
|
|
354
377
|
handleError(err);
|
|
355
378
|
}
|
|
356
379
|
});
|
|
380
|
+
program
|
|
381
|
+
.command('ports <name> [ports...]')
|
|
382
|
+
.description('Configure ports to forward for a workspace')
|
|
383
|
+
.action(async (name, ports) => {
|
|
384
|
+
try {
|
|
385
|
+
const client = await getClient();
|
|
386
|
+
const workspace = await client.getWorkspace(name);
|
|
387
|
+
if (!workspace) {
|
|
388
|
+
console.error(`Workspace '${name}' not found`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
if (ports.length === 0) {
|
|
392
|
+
const currentPorts = workspace.ports.forwards || [];
|
|
393
|
+
if (currentPorts.length === 0) {
|
|
394
|
+
console.log(`No ports configured for workspace '${name}'.`);
|
|
395
|
+
console.log('');
|
|
396
|
+
console.log('Usage: perry ports <name> <port> [<port>...]');
|
|
397
|
+
console.log(' Example: perry ports ' + name + ' 3000 5173 8080');
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
console.log(`Ports configured for '${name}': ${currentPorts.join(', ')}`);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const portNumbers = ports.map((p) => {
|
|
405
|
+
const num = parseInt(p, 10);
|
|
406
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
407
|
+
console.error(`Invalid port number: ${p}`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
return num;
|
|
411
|
+
});
|
|
412
|
+
await client.setPortForwards(name, portNumbers);
|
|
413
|
+
console.log(`Ports configured for '${name}': ${portNumbers.join(', ')}`);
|
|
414
|
+
console.log('');
|
|
415
|
+
console.log(`Run 'perry proxy ${name}' to start forwarding.`);
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
handleError(err);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
357
421
|
const configCmd = program.command('config').description('Manage configuration');
|
|
358
422
|
configCmd
|
|
359
423
|
.command('show')
|
|
@@ -400,8 +464,15 @@ configCmd
|
|
|
400
464
|
for (const [dest, src] of Object.entries(config.credentials.files)) {
|
|
401
465
|
console.log(` - ${dest} <- ${src}`);
|
|
402
466
|
}
|
|
403
|
-
|
|
404
|
-
|
|
467
|
+
const scripts = config.scripts.post_start;
|
|
468
|
+
if (scripts && scripts.length > 0) {
|
|
469
|
+
console.log(` Post-start Scripts: ${scripts.length}`);
|
|
470
|
+
for (const script of scripts) {
|
|
471
|
+
console.log(` - ${script}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (config.scripts.fail_on_error) {
|
|
475
|
+
console.log(` Scripts Fail on Error: enabled`);
|
|
405
476
|
}
|
|
406
477
|
});
|
|
407
478
|
const sshCmd = program.command('ssh').description('Manage SSH keys for workspaces');
|
package/dist/perry-worker
CHANGED
|
Binary file
|