@gricha/perry 0.3.6 → 0.3.8

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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Perry</title>
8
- <script type="module" crossorigin src="/assets/index-C-xi0Vax.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CYo-1I5o.css">
8
+ <script type="module" crossorigin src="/assets/index-hNfXv8YX.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-B-qVBi35.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Command } from 'commander';
3
+ import { spawn } from 'child_process';
3
4
  import pkg from '../package.json';
4
5
  import { startAgent } from './agent/run';
5
- import { installService, uninstallService, showStatus } from './agent/systemd';
6
+ import { installService, uninstallService, showStatus, getServiceStatus } from './agent/systemd';
6
7
  import { createApiClient, ApiClientError } from './client/api';
7
8
  import { loadClientConfig, getWorker, setWorker } from './client/config';
8
9
  import { openWSShell, openDockerExec, getTerminalWSUrl, isLocalWorker } from './client/ws-shell';
@@ -74,6 +75,38 @@ agentCmd
74
75
  lines: parseInt(options.lines, 10),
75
76
  });
76
77
  });
78
+ agentCmd
79
+ .command('kill')
80
+ .description('Stop the running agent daemon')
81
+ .action(async () => {
82
+ const status = await getServiceStatus();
83
+ if (status.running) {
84
+ console.log('Stopping agent via systemd...');
85
+ const proc = spawn('systemctl', ['--user', 'stop', 'perry-agent'], {
86
+ stdio: 'inherit',
87
+ });
88
+ await new Promise((resolve) => proc.on('close', () => resolve()));
89
+ console.log('Agent stopped.');
90
+ return;
91
+ }
92
+ const agentRunning = await checkLocalAgent();
93
+ if (!agentRunning) {
94
+ console.log('No running agent found.');
95
+ return;
96
+ }
97
+ console.log('Stopping agent process...');
98
+ const proc = spawn('pkill', ['-f', 'perry.*agent.*run'], {
99
+ stdio: 'inherit',
100
+ });
101
+ await new Promise((resolve) => proc.on('close', () => resolve()));
102
+ await new Promise((r) => setTimeout(r, 500));
103
+ const stillRunning = await checkLocalAgent();
104
+ if (stillRunning) {
105
+ console.log('Agent still running, sending SIGKILL...');
106
+ spawn('pkill', ['-9', '-f', 'perry.*agent.*run'], { stdio: 'inherit' });
107
+ }
108
+ console.log('Agent stopped.');
109
+ });
77
110
  async function checkLocalAgent() {
78
111
  try {
79
112
  const response = await fetch(`http://localhost:${DEFAULT_AGENT_PORT}/health`, {
@@ -664,22 +697,49 @@ program
664
697
  .description('Update Perry to the latest version')
665
698
  .option('-f, --force', 'Force update even if already on latest version')
666
699
  .action(async (options) => {
667
- const { fetchLatestVersion, compareVersions } = await import('./update-checker.js');
700
+ const { fetchLatestVersionWithDetails, compareVersions } = await import('./update-checker.js');
668
701
  const currentVersion = pkg.version;
669
702
  console.log(`Current version: ${currentVersion}`);
670
703
  console.log('Checking for updates...');
671
- const latestVersion = await fetchLatestVersion();
672
- if (!latestVersion) {
673
- console.error('Failed to fetch latest version. Please try again later.');
704
+ const result = await fetchLatestVersionWithDetails();
705
+ if (!result.version) {
706
+ // Only show detailed error messages in interactive mode (TTY)
707
+ if (process.stderr.isTTY) {
708
+ if (result.status === 504) {
709
+ console.error('GitHub API returned 504 Gateway Timeout. This is usually a temporary issue.');
710
+ console.error('Please try again in a few moments.');
711
+ }
712
+ else if (result.status === 403) {
713
+ console.error('GitHub API rate limit exceeded. Please try again later.');
714
+ }
715
+ else if (result.error) {
716
+ console.error(`Failed to fetch latest version: ${result.error}`);
717
+ }
718
+ else {
719
+ console.error('Failed to fetch latest version. Please try again later.');
720
+ }
721
+ }
674
722
  process.exit(1);
675
723
  }
724
+ const latestVersion = result.version;
676
725
  console.log(`Latest version: ${latestVersion}`);
677
726
  if (compareVersions(currentVersion, latestVersion) <= 0 && !options.force) {
678
727
  console.log('Already up to date.');
679
728
  process.exit(0);
680
729
  }
730
+ const agentRunning = await checkLocalAgent();
731
+ if (agentRunning) {
732
+ console.error('');
733
+ console.error('Warning: Perry agent is currently running.');
734
+ console.error('The update may fail with "Text file busy" error.');
735
+ console.error('');
736
+ console.error('Stop the agent first with:');
737
+ console.error(' perry agent kill');
738
+ console.error('');
739
+ console.error('Then run the update again.');
740
+ process.exit(1);
741
+ }
681
742
  console.log(`Updating Perry from ${currentVersion} to ${latestVersion}...`);
682
- const { spawn } = await import('child_process');
683
743
  const child = spawn('bash', ['-c', 'curl -fsSL https://raw.githubusercontent.com/gricha/perry/main/install.sh | bash'], {
684
744
  stdio: 'inherit',
685
745
  });
@@ -741,6 +801,14 @@ workerCmd
741
801
  process.exit(1);
742
802
  }
743
803
  });
804
+ workerCmd
805
+ .command('serve')
806
+ .option('--port <port>', 'Port to listen on', '7392')
807
+ .description('Start worker API server')
808
+ .action(async (opts) => {
809
+ const { startWorkerServer } = await import('./worker/server');
810
+ await startWorkerServer({ port: parseInt(opts.port, 10) });
811
+ });
744
812
  function handleError(err) {
745
813
  if (err instanceof ApiClientError) {
746
814
  console.error(`Error: ${err.message}`);
package/dist/perry-worker CHANGED
Binary file
@@ -1,5 +1,5 @@
1
1
  import { parseClaudeSessionContent } from '../parser';
2
- import { decodeClaudeProjectPath, extractFirstUserPrompt, extractClaudeSessionName } from './utils';
2
+ import { decodeClaudeProjectPath, encodeClaudeProjectPath, extractFirstUserPrompt, extractClaudeSessionName, } from './utils';
3
3
  export const claudeProvider = {
4
4
  async discoverSessions(containerName, exec) {
5
5
  const result = await exec(containerName, [
@@ -59,17 +59,24 @@ export const claudeProvider = {
59
59
  firstPrompt,
60
60
  };
61
61
  },
62
- async getSessionMessages(containerName, sessionId, exec) {
62
+ async getSessionMessages(containerName, sessionId, exec, projectPath) {
63
63
  const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
64
- const findResult = await exec(containerName, [
65
- 'bash',
66
- '-c',
67
- `find /home/workspace/.claude/projects -name "${safeSessionId}.jsonl" -type f 2>/dev/null | head -1`,
68
- ], { user: 'workspace' });
69
- if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
70
- return null;
64
+ let filePath;
65
+ if (projectPath) {
66
+ const encodedPath = encodeClaudeProjectPath(projectPath);
67
+ filePath = `/home/workspace/.claude/projects/${encodedPath}/${safeSessionId}.jsonl`;
68
+ }
69
+ else {
70
+ const findResult = await exec(containerName, [
71
+ 'bash',
72
+ '-c',
73
+ `find /home/workspace/.claude/projects -name "${safeSessionId}.jsonl" -type f 2>/dev/null | head -1`,
74
+ ], { user: 'workspace' });
75
+ if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
76
+ return null;
77
+ }
78
+ filePath = findResult.stdout.trim();
71
79
  }
72
- const filePath = findResult.stdout.trim();
73
80
  const catResult = await exec(containerName, ['cat', filePath], {
74
81
  user: 'workspace',
75
82
  });
@@ -87,7 +87,7 @@ export const codexProvider = {
87
87
  firstPrompt: firstPrompt ? firstPrompt.slice(0, 200) : null,
88
88
  };
89
89
  },
90
- async getSessionMessages(containerName, sessionId, exec) {
90
+ async getSessionMessages(containerName, sessionId, exec, _projectPath) {
91
91
  const findResult = await exec(containerName, ['bash', '-c', `find /home/workspace/.codex/sessions -name "*.jsonl" -type f 2>/dev/null`], { user: 'workspace' });
92
92
  if (findResult.exitCode !== 0 || !findResult.stdout.trim()) {
93
93
  return null;
@@ -1,53 +1,52 @@
1
1
  import { claudeProvider } from './claude';
2
2
  import { opencodeProvider } from './opencode';
3
3
  import { codexProvider } from './codex';
4
+ import { discoverSessionsViaWorker, getSessionDetailsViaWorker, getSessionMessagesViaWorker, deleteSessionViaWorker, } from './worker-provider';
4
5
  export { claudeProvider } from './claude';
5
6
  export { opencodeProvider } from './opencode';
6
7
  export { codexProvider } from './codex';
7
- const providers = {
8
+ export { clearWorkerClientCache } from './worker-provider';
9
+ const _providers = {
8
10
  'claude-code': claudeProvider,
9
11
  opencode: opencodeProvider,
10
12
  codex: codexProvider,
11
13
  };
12
- export async function discoverAllSessions(containerName, exec) {
13
- const results = await Promise.all([
14
- claudeProvider.discoverSessions(containerName, exec),
15
- opencodeProvider.discoverSessions(containerName, exec),
16
- codexProvider.discoverSessions(containerName, exec),
17
- ]);
18
- return results.flat();
14
+ export async function discoverAllSessions(containerName, _exec) {
15
+ return discoverSessionsViaWorker(containerName);
19
16
  }
20
- export async function getSessionDetails(containerName, rawSession, exec) {
21
- const provider = providers[rawSession.agentType];
22
- if (!provider)
23
- return null;
24
- return provider.getSessionDetails(containerName, rawSession, exec);
17
+ export async function getSessionDetails(containerName, rawSession, _exec) {
18
+ return getSessionDetailsViaWorker(containerName, rawSession);
25
19
  }
26
- export async function getSessionMessages(containerName, sessionId, agentType, exec) {
27
- const provider = providers[agentType];
28
- if (!provider)
29
- return null;
30
- const result = await provider.getSessionMessages(containerName, sessionId, exec);
20
+ export async function getSessionMessages(containerName, sessionId, agentType, _exec, _projectPath) {
21
+ const result = await getSessionMessagesViaWorker(containerName, sessionId);
31
22
  if (!result)
32
23
  return null;
33
24
  return { ...result, agentType };
34
25
  }
35
- export async function findSessionMessages(containerName, sessionId, exec) {
36
- const agentTypes = ['claude-code', 'opencode', 'codex'];
37
- for (const agentType of agentTypes) {
38
- const result = await getSessionMessages(containerName, sessionId, agentType, exec);
39
- if (result && result.messages.length > 0) {
40
- return result;
41
- }
26
+ export async function findSessionMessages(containerName, sessionId, _exec) {
27
+ const { createWorkerClient } = await import('../../worker/client');
28
+ const client = await createWorkerClient(containerName);
29
+ const session = await client.getSession(sessionId);
30
+ if (!session) {
31
+ return null;
42
32
  }
43
- return null;
44
- }
45
- export async function deleteSession(containerName, sessionId, agentType, exec) {
46
- const provider = providers[agentType];
47
- if (!provider) {
48
- return { success: false, error: 'Unknown agent type' };
33
+ const result = await client.getMessages(sessionId, { limit: 1000, offset: 0 });
34
+ if (!result || result.messages.length === 0) {
35
+ return null;
49
36
  }
50
- return provider.deleteSession(containerName, sessionId, exec);
37
+ const agentType = session.agentType === 'claude' ? 'claude-code' : session.agentType;
38
+ const messages = result.messages.map((m) => ({
39
+ type: m.type,
40
+ content: m.content,
41
+ toolName: m.toolName,
42
+ toolId: m.toolId,
43
+ toolInput: m.toolInput,
44
+ timestamp: m.timestamp,
45
+ }));
46
+ return { id: sessionId, agentType, messages };
47
+ }
48
+ export async function deleteSession(containerName, sessionId, _agentType, _exec) {
49
+ return deleteSessionViaWorker(containerName, sessionId);
51
50
  }
52
51
  export async function searchSessions(containerName, query, exec) {
53
52
  const safeQuery = query.replace(/['"\\]/g, '\\$&');
@@ -8,6 +8,7 @@ function getStorageBase(homeDir) {
8
8
  export async function listOpencodeSessions(homeDir) {
9
9
  const storageBase = getStorageBase(homeDir);
10
10
  const sessionDir = path.join(storageBase, 'session');
11
+ const messageDir = path.join(storageBase, 'message');
11
12
  const sessions = [];
12
13
  try {
13
14
  const projectDirs = await fs.readdir(sessionDir, { withFileTypes: true });
@@ -26,12 +27,22 @@ export async function listOpencodeSessions(homeDir) {
26
27
  const data = JSON.parse(content);
27
28
  if (!data.id)
28
29
  continue;
30
+ let messageCount = 0;
31
+ try {
32
+ const msgDir = path.join(messageDir, data.id);
33
+ const msgFiles = await fs.readdir(msgDir);
34
+ messageCount = msgFiles.filter((f) => f.startsWith('msg_') && f.endsWith('.json')).length;
35
+ }
36
+ catch {
37
+ // No messages directory
38
+ }
29
39
  sessions.push({
30
40
  id: data.id,
31
41
  title: data.title || '',
32
42
  directory: data.directory || '',
33
43
  mtime: data.time?.updated || Math.floor(stat.mtimeMs),
34
44
  file: filePath,
45
+ messageCount,
35
46
  });
36
47
  }
37
48
  catch {
@@ -49,7 +49,7 @@ export const opencodeProvider = {
49
49
  return null;
50
50
  }
51
51
  },
52
- async getSessionMessages(containerName, sessionId, exec) {
52
+ async getSessionMessages(containerName, sessionId, exec, _projectPath) {
53
53
  const result = await exec(containerName, ['perry', 'worker', 'sessions', 'messages', sessionId], {
54
54
  user: 'workspace',
55
55
  });
@@ -1,6 +1,9 @@
1
1
  export function decodeClaudeProjectPath(encoded) {
2
2
  return encoded.replace(/-/g, '/');
3
3
  }
4
+ export function encodeClaudeProjectPath(projectPath) {
5
+ return projectPath.replace(/\//g, '-');
6
+ }
4
7
  export function extractFirstUserPrompt(messages) {
5
8
  const firstPrompt = messages.find((msg) => msg.type === 'user' && msg.content && msg.content.trim().length > 0);
6
9
  return firstPrompt?.content ? firstPrompt.content.slice(0, 200) : null;
@@ -0,0 +1,66 @@
1
+ import { createWorkerClient } from '../../worker/client';
2
+ const clientCache = new Map();
3
+ async function getClient(containerName) {
4
+ let client = clientCache.get(containerName);
5
+ if (!client) {
6
+ client = await createWorkerClient(containerName);
7
+ clientCache.set(containerName, client);
8
+ }
9
+ return client;
10
+ }
11
+ export async function discoverSessionsViaWorker(containerName) {
12
+ const client = await getClient(containerName);
13
+ const sessions = await client.listSessions();
14
+ return sessions.map((s) => ({
15
+ id: s.id,
16
+ agentType: (s.agentType === 'claude' ? 'claude-code' : s.agentType),
17
+ projectPath: s.directory,
18
+ mtime: Math.floor(s.lastActivity / 1000),
19
+ name: s.title || undefined,
20
+ filePath: s.filePath,
21
+ }));
22
+ }
23
+ export async function getSessionDetailsViaWorker(containerName, rawSession) {
24
+ const client = await getClient(containerName);
25
+ const session = await client.getSession(rawSession.id);
26
+ if (!session) {
27
+ return null;
28
+ }
29
+ return {
30
+ id: session.id,
31
+ name: session.title || null,
32
+ agentType: rawSession.agentType,
33
+ projectPath: session.directory,
34
+ messageCount: session.messageCount,
35
+ lastActivity: new Date(session.lastActivity).toISOString(),
36
+ firstPrompt: session.firstPrompt,
37
+ };
38
+ }
39
+ export async function getSessionMessagesViaWorker(containerName, sessionId) {
40
+ const client = await getClient(containerName);
41
+ const result = await client.getMessages(sessionId, { limit: 1000, offset: 0 });
42
+ if (!result || result.messages.length === 0) {
43
+ return null;
44
+ }
45
+ const messages = result.messages.map((m) => ({
46
+ type: m.type,
47
+ content: m.content,
48
+ toolName: m.toolName,
49
+ toolId: m.toolId,
50
+ toolInput: m.toolInput,
51
+ timestamp: m.timestamp,
52
+ }));
53
+ return { id: sessionId, messages };
54
+ }
55
+ export async function deleteSessionViaWorker(containerName, sessionId) {
56
+ const client = await getClient(containerName);
57
+ return client.deleteSession(sessionId);
58
+ }
59
+ export function clearWorkerClientCache(containerName) {
60
+ if (containerName) {
61
+ clientCache.delete(containerName);
62
+ }
63
+ else {
64
+ clientCache.clear();
65
+ }
66
+ }
@@ -28,22 +28,37 @@ async function writeCache(cache) {
28
28
  }
29
29
  }
30
30
  export async function fetchLatestVersion() {
31
+ const result = await fetchLatestVersionWithDetails();
32
+ return result.version;
33
+ }
34
+ export async function fetchLatestVersionWithDetails() {
31
35
  try {
32
36
  const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
33
- signal: AbortSignal.timeout(3000),
37
+ signal: AbortSignal.timeout(5000),
34
38
  headers: {
35
39
  Accept: 'application/vnd.github.v3+json',
36
40
  'User-Agent': 'perry-update-checker',
37
41
  },
38
42
  });
39
- if (!response.ok)
40
- return null;
43
+ if (!response.ok) {
44
+ return {
45
+ version: null,
46
+ error: `GitHub API returned ${response.status} ${response.statusText}`,
47
+ status: response.status,
48
+ };
49
+ }
41
50
  const data = (await response.json());
42
51
  const tag = data.tag_name || null;
43
- return tag ? tag.replace(/^v/, '') : null;
52
+ return { version: tag ? tag.replace(/^v/, '') : null };
44
53
  }
45
- catch {
46
- return null;
54
+ catch (err) {
55
+ if (err instanceof Error) {
56
+ if (err.name === 'TimeoutError' || err.name === 'AbortError') {
57
+ return { version: null, error: 'Request timed out' };
58
+ }
59
+ return { version: null, error: err.message };
60
+ }
61
+ return { version: null, error: 'Unknown error' };
47
62
  }
48
63
  }
49
64
  export function compareVersions(current, latest) {
@@ -0,0 +1,105 @@
1
+ import { getContainerIp, execInContainer } from '../docker';
2
+ const WORKER_PORT = 7392;
3
+ const HEALTH_TIMEOUT = 2000;
4
+ const REQUEST_TIMEOUT = 30000;
5
+ const STARTUP_TIMEOUT = 15000;
6
+ const STARTUP_POLL_INTERVAL = 200;
7
+ async function fetchWithTimeout(url, options = {}) {
8
+ const timeout = options.timeout ?? REQUEST_TIMEOUT;
9
+ const controller = new AbortController();
10
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
11
+ try {
12
+ const response = await fetch(url, {
13
+ ...options,
14
+ signal: controller.signal,
15
+ });
16
+ return response;
17
+ }
18
+ finally {
19
+ clearTimeout(timeoutId);
20
+ }
21
+ }
22
+ async function isWorkerRunning(ip) {
23
+ try {
24
+ const response = await fetchWithTimeout(`http://${ip}:${WORKER_PORT}/health`, {
25
+ timeout: HEALTH_TIMEOUT,
26
+ });
27
+ return response.ok;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function startWorkerInContainer(containerName) {
34
+ await execInContainer(containerName, ['sh', '-c', 'nohup perry worker serve > /tmp/perry-worker.log 2>&1 &'], { user: 'workspace' });
35
+ }
36
+ async function ensureWorkerRunning(containerName) {
37
+ const ip = await getContainerIp(containerName);
38
+ if (!ip) {
39
+ throw new Error(`Could not get IP for container: ${containerName}`);
40
+ }
41
+ if (await isWorkerRunning(ip)) {
42
+ return ip;
43
+ }
44
+ await startWorkerInContainer(containerName);
45
+ const deadline = Date.now() + STARTUP_TIMEOUT;
46
+ while (Date.now() < deadline) {
47
+ await new Promise((resolve) => setTimeout(resolve, STARTUP_POLL_INTERVAL));
48
+ if (await isWorkerRunning(ip)) {
49
+ return ip;
50
+ }
51
+ }
52
+ throw new Error(`Worker failed to start in container: ${containerName}`);
53
+ }
54
+ export async function createWorkerClient(containerName) {
55
+ const ip = await ensureWorkerRunning(containerName);
56
+ const baseUrl = `http://${ip}:${WORKER_PORT}`;
57
+ return {
58
+ async health() {
59
+ const response = await fetchWithTimeout(`${baseUrl}/health`);
60
+ if (!response.ok) {
61
+ throw new Error(`Failed to get health: ${response.statusText}`);
62
+ }
63
+ return response.json();
64
+ },
65
+ async listSessions() {
66
+ const response = await fetchWithTimeout(`${baseUrl}/sessions`);
67
+ if (!response.ok) {
68
+ throw new Error(`Failed to list sessions: ${response.statusText}`);
69
+ }
70
+ const data = await response.json();
71
+ return data.sessions;
72
+ },
73
+ async getSession(id) {
74
+ const response = await fetchWithTimeout(`${baseUrl}/sessions/${encodeURIComponent(id)}`);
75
+ if (response.status === 404) {
76
+ return null;
77
+ }
78
+ if (!response.ok) {
79
+ throw new Error(`Failed to get session: ${response.statusText}`);
80
+ }
81
+ const data = await response.json();
82
+ return data.session;
83
+ },
84
+ async getMessages(id, opts = {}) {
85
+ const params = new URLSearchParams();
86
+ if (opts.limit !== undefined)
87
+ params.set('limit', String(opts.limit));
88
+ if (opts.offset !== undefined)
89
+ params.set('offset', String(opts.offset));
90
+ const url = `${baseUrl}/sessions/${encodeURIComponent(id)}/messages?${params}`;
91
+ const response = await fetchWithTimeout(url);
92
+ if (!response.ok) {
93
+ throw new Error(`Failed to get messages: ${response.statusText}`);
94
+ }
95
+ return response.json();
96
+ },
97
+ async deleteSession(id) {
98
+ const response = await fetchWithTimeout(`${baseUrl}/sessions/${encodeURIComponent(id)}`, {
99
+ method: 'DELETE',
100
+ });
101
+ return response.json();
102
+ },
103
+ };
104
+ }
105
+ export { WORKER_PORT };
@@ -0,0 +1,69 @@
1
+ import { sessionIndex } from './session-index';
2
+ import pkg from '../../package.json' with { type: 'json' };
3
+ const DEFAULT_PORT = 7392;
4
+ export async function startWorkerServer(options = {}) {
5
+ const port = options.port ?? DEFAULT_PORT;
6
+ await sessionIndex.initialize();
7
+ sessionIndex.startWatchers();
8
+ const server = Bun.serve({
9
+ port,
10
+ hostname: '0.0.0.0',
11
+ async fetch(req) {
12
+ const url = new URL(req.url);
13
+ if (url.pathname === '/health' && req.method === 'GET') {
14
+ const response = {
15
+ status: 'ok',
16
+ version: pkg.version,
17
+ sessionCount: sessionIndex.list().length,
18
+ };
19
+ return Response.json(response);
20
+ }
21
+ if (url.pathname === '/sessions' && req.method === 'GET') {
22
+ await sessionIndex.refresh();
23
+ const sessions = sessionIndex.list();
24
+ const response = { sessions };
25
+ return Response.json(response);
26
+ }
27
+ if (url.pathname === '/refresh' && req.method === 'POST') {
28
+ await sessionIndex.refresh();
29
+ return Response.json({ success: true });
30
+ }
31
+ const messagesMatch = url.pathname.match(/^\/sessions\/([^/]+)\/messages$/);
32
+ if (messagesMatch && req.method === 'GET') {
33
+ const id = decodeURIComponent(messagesMatch[1]);
34
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
35
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
36
+ const result = await sessionIndex.getMessages(id, { limit, offset });
37
+ const response = result;
38
+ return Response.json(response);
39
+ }
40
+ const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
41
+ if (sessionMatch && req.method === 'GET') {
42
+ const id = decodeURIComponent(sessionMatch[1]);
43
+ const session = sessionIndex.get(id);
44
+ if (!session) {
45
+ return Response.json({ error: 'Session not found' }, { status: 404 });
46
+ }
47
+ return Response.json({ session });
48
+ }
49
+ if (sessionMatch && req.method === 'DELETE') {
50
+ const id = decodeURIComponent(sessionMatch[1]);
51
+ const result = await sessionIndex.delete(id);
52
+ const response = result;
53
+ return Response.json(response, { status: result.success ? 200 : 404 });
54
+ }
55
+ return Response.json({ error: 'Not Found' }, { status: 404 });
56
+ },
57
+ });
58
+ console.error(`Worker server listening on port ${server.port}`);
59
+ process.on('SIGINT', () => {
60
+ sessionIndex.stopWatchers();
61
+ server.stop();
62
+ process.exit(0);
63
+ });
64
+ process.on('SIGTERM', () => {
65
+ sessionIndex.stopWatchers();
66
+ server.stop();
67
+ process.exit(0);
68
+ });
69
+ }