@gricha/perry 0.2.5 → 0.3.0
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/agent/router.js +156 -0
- package/dist/agent/run.js +161 -99
- package/dist/agent/static.js +32 -0
- package/dist/agent/web/assets/index-CYo-1I5o.css +1 -0
- package/dist/agent/web/assets/index-CZjSxNrg.js +104 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/chat/base-claude-session.js +48 -2
- package/dist/chat/opencode-server.js +241 -24
- package/dist/chat/session-monitor.js +186 -0
- package/dist/chat/session-utils.js +155 -0
- package/dist/client/api.js +19 -0
- package/dist/perry-worker +0 -0
- package/dist/session-manager/adapters/claude.js +256 -0
- package/dist/session-manager/adapters/index.js +2 -0
- package/dist/session-manager/adapters/opencode.js +317 -0
- package/dist/session-manager/bun-handler.js +175 -0
- package/dist/session-manager/index.js +3 -0
- package/dist/session-manager/manager.js +302 -0
- package/dist/session-manager/ring-buffer.js +66 -0
- package/dist/session-manager/types.js +1 -0
- package/dist/session-manager/websocket.js +153 -0
- package/dist/shared/base-websocket.js +39 -7
- package/dist/tailscale/index.js +20 -6
- package/dist/terminal/bun-handler.js +151 -0
- package/dist/terminal/handler.js +17 -1
- package/dist/terminal/host-handler.js +19 -1
- package/dist/terminal/websocket.js +5 -0
- package/package.json +3 -3
- package/dist/agent/web/assets/index-0UMxrAK_.js +0 -104
- package/dist/agent/web/assets/index-BwItLEFi.css +0 -1
package/dist/agent/router.js
CHANGED
|
@@ -12,6 +12,7 @@ import { parseClaudeSessionContent } from '../sessions/parser';
|
|
|
12
12
|
import { discoverAllSessions, getSessionDetails as getAgentSessionDetails, getSessionMessages, findSessionMessages, deleteSession as deleteSessionFromProvider, searchSessions as searchSessionsInContainer, } from '../sessions/agents';
|
|
13
13
|
import { discoverClaudeCodeModels, discoverHostOpencodeModels, discoverContainerOpencodeModels, } from '../models/discovery';
|
|
14
14
|
import { listOpencodeSessions, getOpencodeSessionMessages, deleteOpencodeSession, } from '../sessions/agents/opencode-storage';
|
|
15
|
+
import { sessionManager } from '../session-manager';
|
|
15
16
|
const WorkspaceStatusSchema = z.enum(['running', 'stopped', 'creating', 'error']);
|
|
16
17
|
const WorkspacePortsSchema = z.object({
|
|
17
18
|
ssh: z.number(),
|
|
@@ -71,6 +72,9 @@ const SSHKeyInfoSchema = z.object({
|
|
|
71
72
|
fingerprint: z.string(),
|
|
72
73
|
hasPrivateKey: z.boolean(),
|
|
73
74
|
});
|
|
75
|
+
const TerminalSettingsSchema = z.object({
|
|
76
|
+
preferredShell: z.string().optional(),
|
|
77
|
+
});
|
|
74
78
|
function mapErrorToORPC(err, defaultMessage) {
|
|
75
79
|
const message = err instanceof Error ? err.message : defaultMessage;
|
|
76
80
|
if (message.includes('not found')) {
|
|
@@ -314,6 +318,28 @@ export function createRouter(ctx) {
|
|
|
314
318
|
const listSSHKeys = os.output(z.array(SSHKeyInfoSchema)).handler(async () => {
|
|
315
319
|
return discoverSSHKeys();
|
|
316
320
|
});
|
|
321
|
+
const getTerminalSettings = os
|
|
322
|
+
.output(z.object({
|
|
323
|
+
preferredShell: z.string().optional(),
|
|
324
|
+
detectedShell: z.string().optional(),
|
|
325
|
+
}))
|
|
326
|
+
.handler(async () => {
|
|
327
|
+
const config = ctx.config.get();
|
|
328
|
+
return {
|
|
329
|
+
preferredShell: config.terminal?.preferredShell,
|
|
330
|
+
detectedShell: process.env.SHELL,
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
const updateTerminalSettings = os
|
|
334
|
+
.input(TerminalSettingsSchema)
|
|
335
|
+
.output(TerminalSettingsSchema)
|
|
336
|
+
.handler(async ({ input }) => {
|
|
337
|
+
const currentConfig = ctx.config.get();
|
|
338
|
+
const newConfig = { ...currentConfig, terminal: input };
|
|
339
|
+
ctx.config.set(newConfig);
|
|
340
|
+
await saveAgentConfig(newConfig, ctx.configDir);
|
|
341
|
+
return input;
|
|
342
|
+
});
|
|
317
343
|
const GitHubRepoSchema = z.object({
|
|
318
344
|
name: z.string(),
|
|
319
345
|
fullName: z.string(),
|
|
@@ -940,6 +966,122 @@ export function createRouter(ctx) {
|
|
|
940
966
|
}
|
|
941
967
|
return { models };
|
|
942
968
|
});
|
|
969
|
+
const LiveAgentTypeSchema = z.enum(['claude', 'opencode', 'codex']);
|
|
970
|
+
const listLiveSessions = os
|
|
971
|
+
.input(z.object({
|
|
972
|
+
workspaceName: z.string().optional(),
|
|
973
|
+
}))
|
|
974
|
+
.handler(async ({ input }) => {
|
|
975
|
+
const sessions = sessionManager.listActiveSessions(input.workspaceName);
|
|
976
|
+
return sessions.map((s) => ({
|
|
977
|
+
...s,
|
|
978
|
+
startedAt: s.startedAt.toISOString(),
|
|
979
|
+
lastActivity: s.lastActivity.toISOString(),
|
|
980
|
+
}));
|
|
981
|
+
});
|
|
982
|
+
const getLiveSession = os
|
|
983
|
+
.input(z.object({
|
|
984
|
+
sessionId: z.string(),
|
|
985
|
+
}))
|
|
986
|
+
.handler(async ({ input }) => {
|
|
987
|
+
const session = sessionManager.getSession(input.sessionId);
|
|
988
|
+
if (!session) {
|
|
989
|
+
throw new ORPCError('NOT_FOUND', { message: 'Live session not found' });
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
...session,
|
|
993
|
+
startedAt: session.startedAt.toISOString(),
|
|
994
|
+
lastActivity: session.lastActivity.toISOString(),
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
const getLiveSessionStatus = os
|
|
998
|
+
.input(z.object({
|
|
999
|
+
sessionId: z.string(),
|
|
1000
|
+
}))
|
|
1001
|
+
.handler(async ({ input }) => {
|
|
1002
|
+
const status = sessionManager.getSessionStatus(input.sessionId);
|
|
1003
|
+
if (!status) {
|
|
1004
|
+
throw new ORPCError('NOT_FOUND', { message: 'Live session not found' });
|
|
1005
|
+
}
|
|
1006
|
+
return { status };
|
|
1007
|
+
});
|
|
1008
|
+
const startLiveSession = os
|
|
1009
|
+
.input(z.object({
|
|
1010
|
+
workspaceName: z.string(),
|
|
1011
|
+
agentType: LiveAgentTypeSchema,
|
|
1012
|
+
sessionId: z.string().optional(),
|
|
1013
|
+
agentSessionId: z.string().optional(),
|
|
1014
|
+
model: z.string().optional(),
|
|
1015
|
+
projectPath: z.string().optional(),
|
|
1016
|
+
}))
|
|
1017
|
+
.handler(async ({ input }) => {
|
|
1018
|
+
if (input.workspaceName !== HOST_WORKSPACE_NAME) {
|
|
1019
|
+
const workspace = await ctx.workspaces.get(input.workspaceName);
|
|
1020
|
+
if (!workspace) {
|
|
1021
|
+
throw new ORPCError('NOT_FOUND', { message: 'Workspace not found' });
|
|
1022
|
+
}
|
|
1023
|
+
if (workspace.status !== 'running') {
|
|
1024
|
+
throw new ORPCError('PRECONDITION_FAILED', { message: 'Workspace is not running' });
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
const config = ctx.config.get();
|
|
1029
|
+
if (!config.allowHostAccess) {
|
|
1030
|
+
throw new ORPCError('PRECONDITION_FAILED', { message: 'Host access is disabled' });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const sessionId = await sessionManager.startSession({
|
|
1034
|
+
workspaceName: input.workspaceName,
|
|
1035
|
+
agentType: input.agentType,
|
|
1036
|
+
sessionId: input.sessionId,
|
|
1037
|
+
agentSessionId: input.agentSessionId,
|
|
1038
|
+
model: input.model,
|
|
1039
|
+
projectPath: input.projectPath,
|
|
1040
|
+
});
|
|
1041
|
+
return { sessionId };
|
|
1042
|
+
});
|
|
1043
|
+
const sendLiveMessage = os
|
|
1044
|
+
.input(z.object({
|
|
1045
|
+
sessionId: z.string(),
|
|
1046
|
+
message: z.string(),
|
|
1047
|
+
}))
|
|
1048
|
+
.handler(async ({ input }) => {
|
|
1049
|
+
const session = sessionManager.getSession(input.sessionId);
|
|
1050
|
+
if (!session) {
|
|
1051
|
+
throw new ORPCError('NOT_FOUND', { message: 'Live session not found' });
|
|
1052
|
+
}
|
|
1053
|
+
await sessionManager.sendMessage(input.sessionId, input.message);
|
|
1054
|
+
return { success: true };
|
|
1055
|
+
});
|
|
1056
|
+
const interruptLiveSession = os
|
|
1057
|
+
.input(z.object({
|
|
1058
|
+
sessionId: z.string(),
|
|
1059
|
+
}))
|
|
1060
|
+
.handler(async ({ input }) => {
|
|
1061
|
+
const session = sessionManager.getSession(input.sessionId);
|
|
1062
|
+
if (!session) {
|
|
1063
|
+
throw new ORPCError('NOT_FOUND', { message: 'Live session not found' });
|
|
1064
|
+
}
|
|
1065
|
+
await sessionManager.interrupt(input.sessionId);
|
|
1066
|
+
return { success: true };
|
|
1067
|
+
});
|
|
1068
|
+
const disposeLiveSession = os
|
|
1069
|
+
.input(z.object({
|
|
1070
|
+
sessionId: z.string(),
|
|
1071
|
+
}))
|
|
1072
|
+
.handler(async ({ input }) => {
|
|
1073
|
+
await sessionManager.disposeSession(input.sessionId);
|
|
1074
|
+
return { success: true };
|
|
1075
|
+
});
|
|
1076
|
+
const getLiveSessionMessages = os
|
|
1077
|
+
.input(z.object({
|
|
1078
|
+
sessionId: z.string(),
|
|
1079
|
+
sinceId: z.number().optional(),
|
|
1080
|
+
}))
|
|
1081
|
+
.handler(async ({ input }) => {
|
|
1082
|
+
const messages = sessionManager.getBufferedMessages(input.sessionId, input.sinceId);
|
|
1083
|
+
return { messages };
|
|
1084
|
+
});
|
|
943
1085
|
return {
|
|
944
1086
|
workspaces: {
|
|
945
1087
|
list: listWorkspaces,
|
|
@@ -966,6 +1108,16 @@ export function createRouter(ctx) {
|
|
|
966
1108
|
delete: deleteSession,
|
|
967
1109
|
search: searchSessions,
|
|
968
1110
|
},
|
|
1111
|
+
live: {
|
|
1112
|
+
list: listLiveSessions,
|
|
1113
|
+
get: getLiveSession,
|
|
1114
|
+
getStatus: getLiveSessionStatus,
|
|
1115
|
+
start: startLiveSession,
|
|
1116
|
+
sendMessage: sendLiveMessage,
|
|
1117
|
+
interrupt: interruptLiveSession,
|
|
1118
|
+
dispose: disposeLiveSession,
|
|
1119
|
+
getMessages: getLiveSessionMessages,
|
|
1120
|
+
},
|
|
969
1121
|
models: {
|
|
970
1122
|
list: listModels,
|
|
971
1123
|
},
|
|
@@ -994,6 +1146,10 @@ export function createRouter(ctx) {
|
|
|
994
1146
|
update: updateSSHSettings,
|
|
995
1147
|
listKeys: listSSHKeys,
|
|
996
1148
|
},
|
|
1149
|
+
terminal: {
|
|
1150
|
+
get: getTerminalSettings,
|
|
1151
|
+
update: updateTerminalSettings,
|
|
1152
|
+
},
|
|
997
1153
|
},
|
|
998
1154
|
};
|
|
999
1155
|
}
|
package/dist/agent/run.js
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RPCHandler } from '@orpc/server/node';
|
|
1
|
+
import { RPCHandler } from '@orpc/server/fetch';
|
|
3
2
|
import { loadAgentConfig, getConfigDir, ensureConfigDir } from '../config/loader';
|
|
4
3
|
import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
5
4
|
import { DEFAULT_AGENT_PORT } from '../shared/constants';
|
|
6
5
|
import { WorkspaceManager } from '../workspace/manager';
|
|
7
6
|
import { containerRunning, getContainerName } from '../docker';
|
|
8
7
|
import { startEagerImagePull, stopEagerImagePull } from '../docker/eager-pull';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { OpencodeWebSocketServer } from '../chat/opencode-websocket';
|
|
8
|
+
import { TerminalHandler } from '../terminal/bun-handler';
|
|
9
|
+
import { LiveChatHandler } from '../session-manager/bun-handler';
|
|
12
10
|
import { createRouter } from './router';
|
|
13
|
-
import {
|
|
11
|
+
import { serveStaticBun } from './static';
|
|
14
12
|
import { SessionsCacheManager } from '../sessions/cache';
|
|
15
13
|
import { ModelCacheManager } from '../models/cache';
|
|
16
14
|
import { FileWatcher } from './file-watcher';
|
|
17
15
|
import { getTailscaleStatus, getTailscaleIdentity, startTailscaleServe, stopTailscaleServe, } from '../tailscale';
|
|
18
16
|
import pkg from '../../package.json';
|
|
19
17
|
const startTime = Date.now();
|
|
20
|
-
function
|
|
21
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
22
|
-
res.end(JSON.stringify(data));
|
|
23
|
-
}
|
|
24
|
-
function createAgentServer(configDir, config, tailscale) {
|
|
18
|
+
function createAgentServer(configDir, config, port, tailscale) {
|
|
25
19
|
let currentConfig = config;
|
|
26
20
|
const workspaces = new WorkspaceManager(configDir, currentConfig);
|
|
27
21
|
const sessionsCache = new SessionsCacheManager(configDir);
|
|
@@ -49,20 +43,24 @@ function createAgentServer(configDir, config, tailscale) {
|
|
|
49
43
|
}
|
|
50
44
|
return containerRunning(getContainerName(name));
|
|
51
45
|
};
|
|
52
|
-
const
|
|
46
|
+
const getPreferredShell = () => {
|
|
47
|
+
return currentConfig.terminal?.preferredShell || process.env.SHELL;
|
|
48
|
+
};
|
|
49
|
+
const terminalHandler = new TerminalHandler({
|
|
53
50
|
getContainerName,
|
|
54
51
|
isWorkspaceRunning,
|
|
55
52
|
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
53
|
+
getPreferredShell,
|
|
56
54
|
});
|
|
57
|
-
const
|
|
55
|
+
const liveClaudeHandler = new LiveChatHandler({
|
|
58
56
|
isWorkspaceRunning,
|
|
59
|
-
getConfig: () => currentConfig,
|
|
60
57
|
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
58
|
+
agentType: 'claude',
|
|
61
59
|
});
|
|
62
|
-
const
|
|
60
|
+
const liveOpencodeHandler = new LiveChatHandler({
|
|
63
61
|
isWorkspaceRunning,
|
|
64
62
|
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
65
|
-
|
|
63
|
+
agentType: 'opencode',
|
|
66
64
|
});
|
|
67
65
|
const triggerAutoSync = () => {
|
|
68
66
|
syncAllRunning().catch((err) => {
|
|
@@ -82,75 +80,133 @@ function createAgentServer(configDir, config, tailscale) {
|
|
|
82
80
|
configDir,
|
|
83
81
|
stateDir: configDir,
|
|
84
82
|
startTime,
|
|
85
|
-
terminalServer,
|
|
83
|
+
terminalServer: terminalHandler,
|
|
86
84
|
sessionsCache,
|
|
87
85
|
modelCache,
|
|
88
86
|
tailscale,
|
|
89
87
|
triggerAutoSync,
|
|
90
88
|
});
|
|
91
89
|
const rpcHandler = new RPCHandler(router);
|
|
92
|
-
const server =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
90
|
+
const server = Bun.serve({
|
|
91
|
+
port,
|
|
92
|
+
hostname: '::',
|
|
93
|
+
async fetch(req, server) {
|
|
94
|
+
const url = new URL(req.url);
|
|
95
|
+
const pathname = url.pathname;
|
|
96
|
+
const method = req.method;
|
|
97
|
+
const corsHeaders = {
|
|
98
|
+
'Access-Control-Allow-Origin': '*',
|
|
99
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
100
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
101
|
+
};
|
|
102
|
+
if (method === 'OPTIONS') {
|
|
103
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
104
|
+
}
|
|
105
|
+
const terminalMatch = pathname.match(/^\/rpc\/terminal\/([^/]+)$/);
|
|
106
|
+
const liveClaudeMatch = pathname.match(/^\/rpc\/live\/claude\/([^/]+)$/);
|
|
107
|
+
const liveOpencodeMatch = pathname.match(/^\/rpc\/live\/opencode\/([^/]+)$/);
|
|
108
|
+
if (terminalMatch || liveClaudeMatch || liveOpencodeMatch) {
|
|
109
|
+
let type;
|
|
110
|
+
let workspaceName;
|
|
111
|
+
if (terminalMatch) {
|
|
112
|
+
type = 'terminal';
|
|
113
|
+
workspaceName = decodeURIComponent(terminalMatch[1]);
|
|
114
|
+
}
|
|
115
|
+
else if (liveClaudeMatch) {
|
|
116
|
+
type = 'live-claude';
|
|
117
|
+
workspaceName = decodeURIComponent(liveClaudeMatch[1]);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
type = 'live-opencode';
|
|
121
|
+
workspaceName = decodeURIComponent(liveOpencodeMatch[1]);
|
|
122
|
+
}
|
|
123
|
+
const running = await isWorkspaceRunning(workspaceName);
|
|
124
|
+
if (!running) {
|
|
125
|
+
return new Response('Not Found', { status: 404 });
|
|
126
|
+
}
|
|
127
|
+
const upgraded = server.upgrade(req, {
|
|
128
|
+
data: { type, workspaceName },
|
|
129
|
+
});
|
|
130
|
+
if (upgraded) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
134
|
+
}
|
|
106
135
|
if (pathname === '/health' && method === 'GET') {
|
|
136
|
+
const identity = getTailscaleIdentity(req);
|
|
107
137
|
const response = { status: 'ok', version: pkg.version };
|
|
108
138
|
if (identity) {
|
|
109
139
|
response.user = identity.email;
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
return;
|
|
141
|
+
return Response.json(response, { headers: corsHeaders });
|
|
113
142
|
}
|
|
114
143
|
if (pathname.startsWith('/rpc')) {
|
|
115
|
-
const { matched } = await rpcHandler.handle(req,
|
|
144
|
+
const { matched, response } = await rpcHandler.handle(req, {
|
|
116
145
|
prefix: '/rpc',
|
|
117
146
|
});
|
|
118
|
-
if (matched)
|
|
119
|
-
|
|
147
|
+
if (matched && response) {
|
|
148
|
+
const newHeaders = new Headers(response.headers);
|
|
149
|
+
Object.entries(corsHeaders).forEach(([k, v]) => newHeaders.set(k, v));
|
|
150
|
+
return new Response(response.body, {
|
|
151
|
+
status: response.status,
|
|
152
|
+
statusText: response.statusText,
|
|
153
|
+
headers: newHeaders,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
120
156
|
}
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
return;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
const staticResponse = await serveStaticBun(pathname);
|
|
158
|
+
if (staticResponse) {
|
|
159
|
+
return staticResponse;
|
|
160
|
+
}
|
|
161
|
+
return Response.json({ error: 'Not found' }, { status: 404, headers: corsHeaders });
|
|
162
|
+
},
|
|
163
|
+
websocket: {
|
|
164
|
+
open(ws) {
|
|
165
|
+
const { type, workspaceName } = ws.data;
|
|
166
|
+
if (type === 'terminal') {
|
|
167
|
+
terminalHandler.handleOpen(ws, workspaceName);
|
|
168
|
+
}
|
|
169
|
+
else if (type === 'live-claude') {
|
|
170
|
+
liveClaudeHandler.handleOpen(ws, workspaceName);
|
|
171
|
+
}
|
|
172
|
+
else if (type === 'live-opencode') {
|
|
173
|
+
liveOpencodeHandler.handleOpen(ws, workspaceName);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
message(ws, message) {
|
|
177
|
+
const { type } = ws.data;
|
|
178
|
+
const data = typeof message === 'string' ? message : message.toString();
|
|
179
|
+
if (type === 'terminal') {
|
|
180
|
+
terminalHandler.handleMessage(ws, data);
|
|
181
|
+
}
|
|
182
|
+
else if (type === 'live-claude') {
|
|
183
|
+
liveClaudeHandler.handleMessage(ws, data);
|
|
184
|
+
}
|
|
185
|
+
else if (type === 'live-opencode') {
|
|
186
|
+
liveOpencodeHandler.handleMessage(ws, data);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
close(ws, code, reason) {
|
|
190
|
+
const { type } = ws.data;
|
|
191
|
+
if (type === 'terminal') {
|
|
192
|
+
terminalHandler.handleClose(ws, code, reason);
|
|
193
|
+
}
|
|
194
|
+
else if (type === 'live-claude') {
|
|
195
|
+
liveClaudeHandler.handleClose(ws, code, reason);
|
|
196
|
+
}
|
|
197
|
+
else if (type === 'live-opencode') {
|
|
198
|
+
liveOpencodeHandler.handleClose(ws, code, reason);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
},
|
|
152
202
|
});
|
|
153
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
server,
|
|
205
|
+
terminalHandler,
|
|
206
|
+
liveClaudeHandler,
|
|
207
|
+
liveOpencodeHandler,
|
|
208
|
+
fileWatcher,
|
|
209
|
+
};
|
|
154
210
|
}
|
|
155
211
|
async function getProcessUsingPort(port) {
|
|
156
212
|
try {
|
|
@@ -214,9 +270,22 @@ export async function startAgent(options = {}) {
|
|
|
214
270
|
httpsUrl: tailscaleServeActive ? `https://${tailscale.dnsName}` : undefined,
|
|
215
271
|
}
|
|
216
272
|
: undefined;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
273
|
+
let server;
|
|
274
|
+
let fileWatcher;
|
|
275
|
+
let terminalHandler;
|
|
276
|
+
let liveClaudeHandler;
|
|
277
|
+
let liveOpencodeHandler;
|
|
278
|
+
try {
|
|
279
|
+
const result = createAgentServer(configDir, config, port, tailscaleInfo);
|
|
280
|
+
server = result.server;
|
|
281
|
+
fileWatcher = result.fileWatcher;
|
|
282
|
+
terminalHandler = result.terminalHandler;
|
|
283
|
+
liveClaudeHandler = result.liveClaudeHandler;
|
|
284
|
+
liveOpencodeHandler = result.liveOpencodeHandler;
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
const error = err;
|
|
288
|
+
if (error.code === 'EADDRINUSE') {
|
|
220
289
|
console.error(`[agent] Error: Port ${port} is already in use.`);
|
|
221
290
|
const processInfo = await getProcessUsingPort(port);
|
|
222
291
|
if (processInfo) {
|
|
@@ -225,26 +294,21 @@ export async function startAgent(options = {}) {
|
|
|
225
294
|
console.error(`[agent] Try using a different port with: perry agent run --port <port>`);
|
|
226
295
|
process.exit(1);
|
|
227
296
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const shortName = tailscale.dnsName.split('.')[0];
|
|
237
|
-
console.log(`[agent] Tailnet: http://${shortName}:${port}`);
|
|
238
|
-
if (tailscaleServeActive) {
|
|
239
|
-
console.log(`[agent] Tailnet HTTPS: https://${tailscale.dnsName}`);
|
|
240
|
-
}
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
console.log(`[agent] Agent running at http://localhost:${port}`);
|
|
300
|
+
if (tailscale.running && tailscale.dnsName) {
|
|
301
|
+
const shortName = tailscale.dnsName.split('.')[0];
|
|
302
|
+
console.log(`[agent] Tailnet: http://${shortName}:${port}`);
|
|
303
|
+
if (tailscaleServeActive) {
|
|
304
|
+
console.log(`[agent] Tailnet HTTPS: https://${tailscale.dnsName}`);
|
|
241
305
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
306
|
+
}
|
|
307
|
+
console.log(`[agent] oRPC endpoint: http://localhost:${port}/rpc`);
|
|
308
|
+
console.log(`[agent] WebSocket terminal: ws://localhost:${port}/rpc/terminal/:name`);
|
|
309
|
+
console.log(`[agent] WebSocket chat (Claude): ws://localhost:${port}/rpc/live/claude/:name`);
|
|
310
|
+
console.log(`[agent] WebSocket chat (OpenCode): ws://localhost:${port}/rpc/live/opencode/:name`);
|
|
311
|
+
startEagerImagePull();
|
|
248
312
|
let isShuttingDown = false;
|
|
249
313
|
const shutdown = async () => {
|
|
250
314
|
if (isShuttingDown) {
|
|
@@ -264,15 +328,13 @@ export async function startAgent(options = {}) {
|
|
|
264
328
|
console.log('[agent] Stopping Tailscale Serve...');
|
|
265
329
|
await stopTailscaleServe();
|
|
266
330
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
server.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
process.exit(0);
|
|
275
|
-
});
|
|
331
|
+
liveClaudeHandler.close();
|
|
332
|
+
liveOpencodeHandler.close();
|
|
333
|
+
terminalHandler.close();
|
|
334
|
+
server.stop();
|
|
335
|
+
clearTimeout(forceExitTimeout);
|
|
336
|
+
console.log('[agent] Server closed');
|
|
337
|
+
process.exit(0);
|
|
276
338
|
};
|
|
277
339
|
process.on('SIGTERM', shutdown);
|
|
278
340
|
process.on('SIGINT', shutdown);
|
package/dist/agent/static.js
CHANGED
|
@@ -65,3 +65,35 @@ export async function serveStatic(_req, res, pathname) {
|
|
|
65
65
|
res.end(content);
|
|
66
66
|
return true;
|
|
67
67
|
}
|
|
68
|
+
export async function serveStaticBun(pathname) {
|
|
69
|
+
const webDir = getWebDir();
|
|
70
|
+
const indexPath = path.join(webDir, 'index.html');
|
|
71
|
+
try {
|
|
72
|
+
await fs.access(indexPath);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const ext = path.extname(pathname).toLowerCase();
|
|
78
|
+
const isAsset = ext && ext !== '.html';
|
|
79
|
+
if (isAsset) {
|
|
80
|
+
const filePath = path.join(webDir, pathname);
|
|
81
|
+
try {
|
|
82
|
+
const file = Bun.file(filePath);
|
|
83
|
+
if (await file.exists()) {
|
|
84
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
85
|
+
return new Response(file, {
|
|
86
|
+
headers: { 'Content-Type': contentType },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const file = Bun.file(indexPath);
|
|
96
|
+
return new Response(file, {
|
|
97
|
+
headers: { 'Content-Type': 'text/html' },
|
|
98
|
+
});
|
|
99
|
+
}
|