@gricha/perry 0.2.6 → 0.3.1
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 +127 -0
- package/dist/agent/run.js +157 -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/client/api.js +19 -0
- package/dist/client/docker-proxy.js +2 -16
- package/dist/client/port-forward.js +23 -0
- package/dist/client/proxy.js +2 -16
- package/dist/config/loader.js +2 -6
- package/dist/index.js +1 -15
- 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/sessions/agents/utils.js +6 -2
- package/dist/sessions/parser.js +1 -11
- package/dist/shared/base-websocket.js +39 -7
- package/dist/shared/format-utils.js +15 -0
- package/dist/shared/path-utils.js +8 -0
- package/dist/ssh/sync.js +1 -8
- package/dist/tailscale/index.js +20 -6
- package/dist/terminal/bun-handler.js +151 -0
- package/package.json +4 -7
- package/dist/agent/web/assets/index-BwItLEFi.css +0 -1
- package/dist/agent/web/assets/index-DhU_amC3.js +0 -104
- package/dist/chat/base-chat-websocket.js +0 -86
- package/dist/chat/base-claude-session.js +0 -169
- package/dist/chat/base-opencode-session.js +0 -181
- package/dist/chat/handler.js +0 -47
- package/dist/chat/host-handler.js +0 -41
- package/dist/chat/host-opencode-handler.js +0 -144
- package/dist/chat/index.js +0 -2
- package/dist/chat/opencode-handler.js +0 -100
- package/dist/chat/opencode-server.js +0 -285
- package/dist/chat/opencode-websocket.js +0 -31
- package/dist/chat/websocket.js +0 -33
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(),
|
|
@@ -965,6 +966,122 @@ export function createRouter(ctx) {
|
|
|
965
966
|
}
|
|
966
967
|
return { models };
|
|
967
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
|
+
});
|
|
968
1085
|
return {
|
|
969
1086
|
workspaces: {
|
|
970
1087
|
list: listWorkspaces,
|
|
@@ -991,6 +1108,16 @@ export function createRouter(ctx) {
|
|
|
991
1108
|
delete: deleteSession,
|
|
992
1109
|
search: searchSessions,
|
|
993
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
|
+
},
|
|
994
1121
|
models: {
|
|
995
1122
|
list: listModels,
|
|
996
1123
|
},
|
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);
|
|
@@ -52,21 +46,21 @@ function createAgentServer(configDir, config, tailscale) {
|
|
|
52
46
|
const getPreferredShell = () => {
|
|
53
47
|
return currentConfig.terminal?.preferredShell || process.env.SHELL;
|
|
54
48
|
};
|
|
55
|
-
const
|
|
49
|
+
const terminalHandler = new TerminalHandler({
|
|
56
50
|
getContainerName,
|
|
57
51
|
isWorkspaceRunning,
|
|
58
52
|
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
59
53
|
getPreferredShell,
|
|
60
54
|
});
|
|
61
|
-
const
|
|
55
|
+
const liveClaudeHandler = new LiveChatHandler({
|
|
62
56
|
isWorkspaceRunning,
|
|
63
|
-
getConfig: () => currentConfig,
|
|
64
57
|
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
58
|
+
agentType: 'claude',
|
|
65
59
|
});
|
|
66
|
-
const
|
|
60
|
+
const liveOpencodeHandler = new LiveChatHandler({
|
|
67
61
|
isWorkspaceRunning,
|
|
68
62
|
isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
|
|
69
|
-
|
|
63
|
+
agentType: 'opencode',
|
|
70
64
|
});
|
|
71
65
|
const triggerAutoSync = () => {
|
|
72
66
|
syncAllRunning().catch((err) => {
|
|
@@ -86,75 +80,133 @@ function createAgentServer(configDir, config, tailscale) {
|
|
|
86
80
|
configDir,
|
|
87
81
|
stateDir: configDir,
|
|
88
82
|
startTime,
|
|
89
|
-
terminalServer,
|
|
83
|
+
terminalServer: terminalHandler,
|
|
90
84
|
sessionsCache,
|
|
91
85
|
modelCache,
|
|
92
86
|
tailscale,
|
|
93
87
|
triggerAutoSync,
|
|
94
88
|
});
|
|
95
89
|
const rpcHandler = new RPCHandler(router);
|
|
96
|
-
const server =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|
|
110
135
|
if (pathname === '/health' && method === 'GET') {
|
|
136
|
+
const identity = getTailscaleIdentity(req);
|
|
111
137
|
const response = { status: 'ok', version: pkg.version };
|
|
112
138
|
if (identity) {
|
|
113
139
|
response.user = identity.email;
|
|
114
140
|
}
|
|
115
|
-
|
|
116
|
-
return;
|
|
141
|
+
return Response.json(response, { headers: corsHeaders });
|
|
117
142
|
}
|
|
118
143
|
if (pathname.startsWith('/rpc')) {
|
|
119
|
-
const { matched } = await rpcHandler.handle(req,
|
|
144
|
+
const { matched, response } = await rpcHandler.handle(req, {
|
|
120
145
|
prefix: '/rpc',
|
|
121
146
|
});
|
|
122
|
-
if (matched)
|
|
123
|
-
|
|
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
|
+
}
|
|
124
156
|
}
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
},
|
|
156
202
|
});
|
|
157
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
server,
|
|
205
|
+
terminalHandler,
|
|
206
|
+
liveClaudeHandler,
|
|
207
|
+
liveOpencodeHandler,
|
|
208
|
+
fileWatcher,
|
|
209
|
+
};
|
|
158
210
|
}
|
|
159
211
|
async function getProcessUsingPort(port) {
|
|
160
212
|
try {
|
|
@@ -218,9 +270,22 @@ export async function startAgent(options = {}) {
|
|
|
218
270
|
httpsUrl: tailscaleServeActive ? `https://${tailscale.dnsName}` : undefined,
|
|
219
271
|
}
|
|
220
272
|
: undefined;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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') {
|
|
224
289
|
console.error(`[agent] Error: Port ${port} is already in use.`);
|
|
225
290
|
const processInfo = await getProcessUsingPort(port);
|
|
226
291
|
if (processInfo) {
|
|
@@ -229,26 +294,21 @@ export async function startAgent(options = {}) {
|
|
|
229
294
|
console.error(`[agent] Try using a different port with: perry agent run --port <port>`);
|
|
230
295
|
process.exit(1);
|
|
231
296
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const shortName = tailscale.dnsName.split('.')[0];
|
|
241
|
-
console.log(`[agent] Tailnet: http://${shortName}:${port}`);
|
|
242
|
-
if (tailscaleServeActive) {
|
|
243
|
-
console.log(`[agent] Tailnet HTTPS: https://${tailscale.dnsName}`);
|
|
244
|
-
}
|
|
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}`);
|
|
245
305
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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();
|
|
252
312
|
let isShuttingDown = false;
|
|
253
313
|
const shutdown = async () => {
|
|
254
314
|
if (isShuttingDown) {
|
|
@@ -268,15 +328,13 @@ export async function startAgent(options = {}) {
|
|
|
268
328
|
console.log('[agent] Stopping Tailscale Serve...');
|
|
269
329
|
await stopTailscaleServe();
|
|
270
330
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
server.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
process.exit(0);
|
|
279
|
-
});
|
|
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);
|
|
280
338
|
};
|
|
281
339
|
process.on('SIGTERM', shutdown);
|
|
282
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
|
+
}
|