@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
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<h1 align="center">Perry</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
+
<a href="https://gricha.github.io/perry/"><img src="https://img.shields.io/badge/docs-docusaurus-blue" alt="Documentation"></a>
|
|
8
9
|
<a href="https://github.com/gricha/perry/actions/workflows/test.yml"><img src="https://github.com/gricha/perry/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
|
|
9
10
|
<a href="https://github.com/gricha/perry/releases"><img src="https://img.shields.io/github/v/release/gricha/perry" alt="Release"></a>
|
|
10
11
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
@@ -28,6 +29,8 @@ It can be connected directly to your host, or it can create docker containers so
|
|
|
28
29
|
|
|
29
30
|
Continue your sessions on the go!
|
|
30
31
|
|
|
32
|
+
**[Get Started →](https://gricha.github.io/perry/docs/getting-started)**
|
|
33
|
+
|
|
31
34
|
## Features
|
|
32
35
|
|
|
33
36
|
- **AI Coding Agents** - Claude Code, OpenCode, Codex CLI pre-installed
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { watch } from 'fs';
|
|
2
2
|
import { access } from 'fs/promises';
|
|
3
3
|
import { expandPath } from '../config/loader';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'~/.claude/.credentials.json',
|
|
7
|
-
'~/.codex/auth.json',
|
|
8
|
-
'~/.codex/config.toml',
|
|
9
|
-
];
|
|
4
|
+
import { getCredentialFilePaths } from '../agents';
|
|
5
|
+
const STANDARD_CREDENTIAL_FILES = ['~/.gitconfig', ...getCredentialFilePaths()];
|
|
10
6
|
export class FileWatcher {
|
|
11
7
|
watchers = new Map();
|
|
12
8
|
config;
|
package/dist/agent/router.js
CHANGED
|
@@ -16,6 +16,7 @@ const WorkspaceStatusSchema = z.enum(['running', 'stopped', 'creating', 'error']
|
|
|
16
16
|
const WorkspacePortsSchema = z.object({
|
|
17
17
|
ssh: z.number(),
|
|
18
18
|
http: z.number().optional(),
|
|
19
|
+
forwards: z.array(z.number()).optional(),
|
|
19
20
|
});
|
|
20
21
|
const WorkspaceInfoSchema = z.object({
|
|
21
22
|
name: z.string(),
|
|
@@ -31,7 +32,8 @@ const CredentialsSchema = z.object({
|
|
|
31
32
|
files: z.record(z.string(), z.string()),
|
|
32
33
|
});
|
|
33
34
|
const ScriptsSchema = z.object({
|
|
34
|
-
post_start: z.string().optional(),
|
|
35
|
+
post_start: z.array(z.string()).optional(),
|
|
36
|
+
fail_on_error: z.boolean().optional(),
|
|
35
37
|
});
|
|
36
38
|
const CodingAgentsSchema = z.object({
|
|
37
39
|
opencode: z
|
|
@@ -193,6 +195,43 @@ export function createRouter(ctx) {
|
|
|
193
195
|
}
|
|
194
196
|
return workspace;
|
|
195
197
|
});
|
|
198
|
+
const getPortForwards = os
|
|
199
|
+
.input(z.object({ name: z.string() }))
|
|
200
|
+
.output(z.object({ forwards: z.array(z.number()) }))
|
|
201
|
+
.handler(async ({ input }) => {
|
|
202
|
+
try {
|
|
203
|
+
const forwards = await ctx.workspaces.getPortForwards(input.name);
|
|
204
|
+
return { forwards };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
mapErrorToORPC(err, 'Failed to get port forwards');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const setPortForwards = os
|
|
211
|
+
.input(z.object({ name: z.string(), forwards: z.array(z.number().int().min(1).max(65535)) }))
|
|
212
|
+
.output(WorkspaceInfoSchema)
|
|
213
|
+
.handler(async ({ input }) => {
|
|
214
|
+
try {
|
|
215
|
+
return await ctx.workspaces.setPortForwards(input.name, input.forwards);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
mapErrorToORPC(err, 'Failed to set port forwards');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
const cloneWorkspace = os
|
|
222
|
+
.input(z.object({
|
|
223
|
+
sourceName: z.string(),
|
|
224
|
+
cloneName: z.string(),
|
|
225
|
+
}))
|
|
226
|
+
.output(WorkspaceInfoSchema)
|
|
227
|
+
.handler(async ({ input }) => {
|
|
228
|
+
try {
|
|
229
|
+
return await ctx.workspaces.clone(input.sourceName, input.cloneName);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
mapErrorToORPC(err, 'Failed to clone workspace');
|
|
233
|
+
}
|
|
234
|
+
});
|
|
196
235
|
const getInfo = os.handler(async () => {
|
|
197
236
|
let dockerVersion = 'unknown';
|
|
198
237
|
try {
|
|
@@ -275,6 +314,76 @@ export function createRouter(ctx) {
|
|
|
275
314
|
const listSSHKeys = os.output(z.array(SSHKeyInfoSchema)).handler(async () => {
|
|
276
315
|
return discoverSSHKeys();
|
|
277
316
|
});
|
|
317
|
+
const GitHubRepoSchema = z.object({
|
|
318
|
+
name: z.string(),
|
|
319
|
+
fullName: z.string(),
|
|
320
|
+
cloneUrl: z.string(),
|
|
321
|
+
sshUrl: z.string(),
|
|
322
|
+
private: z.boolean(),
|
|
323
|
+
description: z.string().nullable(),
|
|
324
|
+
updatedAt: z.string(),
|
|
325
|
+
});
|
|
326
|
+
const listGitHubRepos = os
|
|
327
|
+
.input(z.object({
|
|
328
|
+
search: z.string().optional(),
|
|
329
|
+
perPage: z.number().optional().default(30),
|
|
330
|
+
page: z.number().optional().default(1),
|
|
331
|
+
}))
|
|
332
|
+
.output(z.object({
|
|
333
|
+
configured: z.boolean(),
|
|
334
|
+
repos: z.array(GitHubRepoSchema),
|
|
335
|
+
hasMore: z.boolean(),
|
|
336
|
+
}))
|
|
337
|
+
.handler(async ({ input }) => {
|
|
338
|
+
const config = ctx.config.get();
|
|
339
|
+
const token = config.agents?.github?.token;
|
|
340
|
+
if (!token) {
|
|
341
|
+
return { configured: false, repos: [], hasMore: false };
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const params = new URLSearchParams({
|
|
345
|
+
per_page: String(input.perPage),
|
|
346
|
+
page: String(input.page),
|
|
347
|
+
sort: 'updated',
|
|
348
|
+
direction: 'desc',
|
|
349
|
+
});
|
|
350
|
+
const url = input.search
|
|
351
|
+
? `https://api.github.com/search/repositories?q=${encodeURIComponent(input.search)}+user:@me&${params}`
|
|
352
|
+
: `https://api.github.com/user/repos?${params}`;
|
|
353
|
+
const response = await fetch(url, {
|
|
354
|
+
headers: {
|
|
355
|
+
Authorization: `Bearer ${token}`,
|
|
356
|
+
Accept: 'application/vnd.github+json',
|
|
357
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
if (response.status === 401) {
|
|
362
|
+
return { configured: false, repos: [], hasMore: false };
|
|
363
|
+
}
|
|
364
|
+
throw new Error(`GitHub API error: ${response.status}`);
|
|
365
|
+
}
|
|
366
|
+
const data = await response.json();
|
|
367
|
+
const items = input.search ? data.items : data;
|
|
368
|
+
const repos = items.map((repo) => ({
|
|
369
|
+
name: repo.name,
|
|
370
|
+
fullName: repo.full_name,
|
|
371
|
+
cloneUrl: repo.clone_url,
|
|
372
|
+
sshUrl: repo.ssh_url,
|
|
373
|
+
private: repo.private,
|
|
374
|
+
description: repo.description,
|
|
375
|
+
updatedAt: repo.updated_at,
|
|
376
|
+
}));
|
|
377
|
+
const linkHeader = response.headers.get('Link');
|
|
378
|
+
const hasMore = linkHeader?.includes('rel="next"') ?? false;
|
|
379
|
+
return { configured: true, repos, hasMore };
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
throw new ORPCError('INTERNAL_SERVER_ERROR', {
|
|
383
|
+
message: `Failed to fetch GitHub repos: ${err.message}`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
278
387
|
async function listHostSessions(input) {
|
|
279
388
|
const limit = input.limit ?? 50;
|
|
280
389
|
const offset = input.offset ?? 0;
|
|
@@ -777,20 +886,6 @@ export function createRouter(ctx) {
|
|
|
777
886
|
homeDir: os_module.homedir(),
|
|
778
887
|
};
|
|
779
888
|
});
|
|
780
|
-
const updateHostAccess = os
|
|
781
|
-
.input(z.object({ enabled: z.boolean() }))
|
|
782
|
-
.handler(async ({ input }) => {
|
|
783
|
-
const currentConfig = ctx.config.get();
|
|
784
|
-
const newConfig = { ...currentConfig, allowHostAccess: input.enabled };
|
|
785
|
-
ctx.config.set(newConfig);
|
|
786
|
-
await saveAgentConfig(newConfig, ctx.configDir);
|
|
787
|
-
return {
|
|
788
|
-
enabled: input.enabled,
|
|
789
|
-
hostname: os_module.hostname(),
|
|
790
|
-
username: os_module.userInfo().username,
|
|
791
|
-
homeDir: os_module.homedir(),
|
|
792
|
-
};
|
|
793
|
-
});
|
|
794
889
|
const listModels = os
|
|
795
890
|
.input(z.object({
|
|
796
891
|
agentType: z.enum(['claude-code', 'opencode']),
|
|
@@ -850,6 +945,7 @@ export function createRouter(ctx) {
|
|
|
850
945
|
list: listWorkspaces,
|
|
851
946
|
get: getWorkspace,
|
|
852
947
|
create: createWorkspace,
|
|
948
|
+
clone: cloneWorkspace,
|
|
853
949
|
delete: deleteWorkspace,
|
|
854
950
|
start: startWorkspace,
|
|
855
951
|
stop: stopWorkspace,
|
|
@@ -857,6 +953,8 @@ export function createRouter(ctx) {
|
|
|
857
953
|
sync: syncWorkspace,
|
|
858
954
|
syncAll: syncAllWorkspaces,
|
|
859
955
|
touch: touchWorkspace,
|
|
956
|
+
getPortForwards: getPortForwards,
|
|
957
|
+
setPortForwards: setPortForwards,
|
|
860
958
|
},
|
|
861
959
|
sessions: {
|
|
862
960
|
list: listSessions,
|
|
@@ -871,9 +969,11 @@ export function createRouter(ctx) {
|
|
|
871
969
|
models: {
|
|
872
970
|
list: listModels,
|
|
873
971
|
},
|
|
972
|
+
github: {
|
|
973
|
+
listRepos: listGitHubRepos,
|
|
974
|
+
},
|
|
874
975
|
host: {
|
|
875
976
|
info: getHostInfo,
|
|
876
|
-
updateAccess: updateHostAccess,
|
|
877
977
|
},
|
|
878
978
|
info: getInfo,
|
|
879
979
|
config: {
|
package/dist/agent/run.js
CHANGED
|
@@ -5,7 +5,7 @@ import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
|
5
5
|
import { DEFAULT_AGENT_PORT } from '../shared/constants';
|
|
6
6
|
import { WorkspaceManager } from '../workspace/manager';
|
|
7
7
|
import { containerRunning, getContainerName } from '../docker';
|
|
8
|
-
import { startEagerImagePull } from '../docker/eager-pull';
|
|
8
|
+
import { startEagerImagePull, stopEagerImagePull } from '../docker/eager-pull';
|
|
9
9
|
import { TerminalWebSocketServer } from '../terminal/websocket';
|
|
10
10
|
import { ChatWebSocketServer } from '../chat/websocket';
|
|
11
11
|
import { OpencodeWebSocketServer } from '../chat/opencode-websocket';
|
|
@@ -245,8 +245,20 @@ export async function startAgent(options = {}) {
|
|
|
245
245
|
console.log(`[agent] WebSocket chat (OpenCode): ws://localhost:${port}/rpc/opencode/:name`);
|
|
246
246
|
startEagerImagePull();
|
|
247
247
|
});
|
|
248
|
+
let isShuttingDown = false;
|
|
248
249
|
const shutdown = async () => {
|
|
250
|
+
if (isShuttingDown) {
|
|
251
|
+
console.log('[agent] Force exit');
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
isShuttingDown = true;
|
|
249
255
|
console.log('[agent] Shutting down...');
|
|
256
|
+
const forceExitTimeout = setTimeout(() => {
|
|
257
|
+
console.log('[agent] Force exit after timeout');
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}, 3000);
|
|
260
|
+
forceExitTimeout.unref();
|
|
261
|
+
stopEagerImagePull();
|
|
250
262
|
fileWatcher.stop();
|
|
251
263
|
if (tailscaleServeActive) {
|
|
252
264
|
console.log('[agent] Stopping Tailscale Serve...');
|
|
@@ -255,7 +267,9 @@ export async function startAgent(options = {}) {
|
|
|
255
267
|
chatServer.close();
|
|
256
268
|
opencodeServer.close();
|
|
257
269
|
terminalServer.close();
|
|
270
|
+
server.closeAllConnections();
|
|
258
271
|
server.close(() => {
|
|
272
|
+
clearTimeout(forceExitTimeout);
|
|
259
273
|
console.log('[agent] Server closed');
|
|
260
274
|
process.exit(0);
|
|
261
275
|
});
|