@gricha/perry 0.2.1 → 0.2.3

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/run.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'http';
2
2
  import { RPCHandler } from '@orpc/server/node';
3
3
  import { loadAgentConfig, getConfigDir, ensureConfigDir } from '../config/loader';
4
- import { HOST_WORKSPACE_NAME } from '../shared/types';
4
+ 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';
@@ -13,17 +13,36 @@ import { createRouter } from './router';
13
13
  import { serveStatic } from './static';
14
14
  import { SessionsCacheManager } from '../sessions/cache';
15
15
  import { ModelCacheManager } from '../models/cache';
16
+ import { FileWatcher } from './file-watcher';
17
+ import { getTailscaleStatus, getTailscaleIdentity, startTailscaleServe, stopTailscaleServe, } from '../tailscale';
16
18
  import pkg from '../../package.json';
17
19
  const startTime = Date.now();
18
20
  function sendJson(res, status, data) {
19
21
  res.writeHead(status, { 'Content-Type': 'application/json' });
20
22
  res.end(JSON.stringify(data));
21
23
  }
22
- function createAgentServer(configDir, config) {
24
+ function createAgentServer(configDir, config, tailscale) {
23
25
  let currentConfig = config;
24
26
  const workspaces = new WorkspaceManager(configDir, currentConfig);
25
27
  const sessionsCache = new SessionsCacheManager(configDir);
26
28
  const modelCache = new ModelCacheManager(configDir);
29
+ const syncAllRunning = async () => {
30
+ const allWorkspaces = await workspaces.list();
31
+ const running = allWorkspaces.filter((ws) => ws.status === 'running');
32
+ for (const ws of running) {
33
+ try {
34
+ await workspaces.sync(ws.name);
35
+ console.log(`[sync] Synced workspace: ${ws.name}`);
36
+ }
37
+ catch (err) {
38
+ console.error(`[sync] Failed to sync ${ws.name}:`, err);
39
+ }
40
+ }
41
+ };
42
+ const fileWatcher = new FileWatcher({
43
+ config: currentConfig,
44
+ syncCallback: syncAllRunning,
45
+ });
27
46
  const isWorkspaceRunning = async (name) => {
28
47
  if (name === HOST_WORKSPACE_NAME) {
29
48
  return currentConfig.allowHostAccess === true;
@@ -45,6 +64,11 @@ function createAgentServer(configDir, config) {
45
64
  isHostAccessAllowed: () => currentConfig.allowHostAccess === true,
46
65
  getConfig: () => currentConfig,
47
66
  });
67
+ const triggerAutoSync = () => {
68
+ syncAllRunning().catch((err) => {
69
+ console.error('[sync] Auto-sync failed:', err);
70
+ });
71
+ };
48
72
  const router = createRouter({
49
73
  workspaces,
50
74
  config: {
@@ -52,6 +76,7 @@ function createAgentServer(configDir, config) {
52
76
  set: (newConfig) => {
53
77
  currentConfig = newConfig;
54
78
  workspaces.updateConfig(newConfig);
79
+ fileWatcher.updateConfig(newConfig);
55
80
  },
56
81
  },
57
82
  configDir,
@@ -60,12 +85,15 @@ function createAgentServer(configDir, config) {
60
85
  terminalServer,
61
86
  sessionsCache,
62
87
  modelCache,
88
+ tailscale,
89
+ triggerAutoSync,
63
90
  });
64
91
  const rpcHandler = new RPCHandler(router);
65
92
  const server = createServer(async (req, res) => {
66
93
  const url = new URL(req.url || '/', 'http://localhost');
67
94
  const method = req.method;
68
95
  const pathname = url.pathname;
96
+ const identity = getTailscaleIdentity(req);
69
97
  res.setHeader('Access-Control-Allow-Origin', '*');
70
98
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
71
99
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
@@ -76,7 +104,11 @@ function createAgentServer(configDir, config) {
76
104
  }
77
105
  try {
78
106
  if (pathname === '/health' && method === 'GET') {
79
- sendJson(res, 200, { status: 'ok', version: pkg.version });
107
+ const response = { status: 'ok', version: pkg.version };
108
+ if (identity) {
109
+ response.user = identity.email;
110
+ }
111
+ sendJson(res, 200, response);
80
112
  return;
81
113
  }
82
114
  if (pathname.startsWith('/rpc')) {
@@ -118,7 +150,7 @@ function createAgentServer(configDir, config) {
118
150
  socket.destroy();
119
151
  }
120
152
  });
121
- return { server, terminalServer, chatServer, opencodeServer };
153
+ return { server, terminalServer, chatServer, opencodeServer, fileWatcher };
122
154
  }
123
155
  async function getProcessUsingPort(port) {
124
156
  try {
@@ -151,7 +183,38 @@ export async function startAgent(options = {}) {
151
183
  const port = options.port || parseInt(process.env.PERRY_PORT || '', 10) || config.port || DEFAULT_AGENT_PORT;
152
184
  console.log(`[agent] Config directory: ${configDir}`);
153
185
  console.log(`[agent] Starting on port ${port}...`);
154
- const { server, terminalServer, chatServer, opencodeServer } = createAgentServer(configDir, config);
186
+ const tailscale = await getTailscaleStatus();
187
+ let tailscaleServeActive = false;
188
+ if (tailscale.running && tailscale.dnsName) {
189
+ console.log(`[agent] Tailscale detected: ${tailscale.dnsName}`);
190
+ if (!tailscale.httpsEnabled) {
191
+ console.log(`[agent] Tailscale HTTPS not enabled in tailnet, skipping Serve`);
192
+ }
193
+ else {
194
+ const result = await startTailscaleServe(port);
195
+ if (result.success) {
196
+ tailscaleServeActive = true;
197
+ console.log(`[agent] Tailscale Serve enabled`);
198
+ }
199
+ else if (result.error === 'permission_denied') {
200
+ console.log(`[agent] Tailscale Serve requires operator permissions`);
201
+ console.log(`[agent] To enable: ${result.message}`);
202
+ console.log(`[agent] Continuing without HTTPS...`);
203
+ }
204
+ else {
205
+ console.log(`[agent] Tailscale Serve failed: ${result.message || 'unknown error'}`);
206
+ }
207
+ }
208
+ }
209
+ const tailscaleInfo = tailscale.running && tailscale.dnsName
210
+ ? {
211
+ running: true,
212
+ dnsName: tailscale.dnsName,
213
+ serveActive: tailscaleServeActive,
214
+ httpsUrl: tailscaleServeActive ? `https://${tailscale.dnsName}` : undefined,
215
+ }
216
+ : undefined;
217
+ const { server, terminalServer, chatServer, opencodeServer, fileWatcher } = createAgentServer(configDir, config, tailscaleInfo);
155
218
  server.on('error', async (err) => {
156
219
  if (err.code === 'EADDRINUSE') {
157
220
  console.error(`[agent] Error: Port ${port} is already in use.`);
@@ -169,14 +232,26 @@ export async function startAgent(options = {}) {
169
232
  });
170
233
  server.listen(port, '::', () => {
171
234
  console.log(`[agent] Agent running at http://localhost:${port}`);
235
+ if (tailscale.running && tailscale.dnsName) {
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
+ }
241
+ }
172
242
  console.log(`[agent] oRPC endpoint: http://localhost:${port}/rpc`);
173
243
  console.log(`[agent] WebSocket terminal: ws://localhost:${port}/rpc/terminal/:name`);
174
244
  console.log(`[agent] WebSocket chat (Claude): ws://localhost:${port}/rpc/chat/:name`);
175
245
  console.log(`[agent] WebSocket chat (OpenCode): ws://localhost:${port}/rpc/opencode/:name`);
176
246
  startEagerImagePull();
177
247
  });
178
- const shutdown = () => {
248
+ const shutdown = async () => {
179
249
  console.log('[agent] Shutting down...');
250
+ fileWatcher.stop();
251
+ if (tailscaleServeActive) {
252
+ console.log('[agent] Stopping Tailscale Serve...');
253
+ await stopTailscaleServe();
254
+ }
180
255
  chatServer.close();
181
256
  opencodeServer.close();
182
257
  terminalServer.close();