@gricha/perry 0.3.7 → 0.3.9

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.
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { startAgent } from './run';
3
+ process.on('unhandledRejection', (reason, promise) => {
4
+ console.error('[agent] Unhandled promise rejection:', reason);
5
+ console.error('[agent] Promise:', promise);
6
+ });
7
+ process.on('uncaughtException', (err) => {
8
+ console.error('[agent] Uncaught exception:', err);
9
+ process.exit(1);
10
+ });
3
11
  startAgent().catch((err) => {
4
12
  console.error('[agent] Fatal error:', err);
5
13
  process.exit(1);
@@ -4,15 +4,16 @@ import os_module from 'os';
4
4
  import { promises as fs } from 'fs';
5
5
  import path from 'path';
6
6
  import { HOST_WORKSPACE_NAME } from '../shared/client-types';
7
- import { getDockerVersion, execInContainer } from '../docker';
7
+ import { getDockerVersion, execInContainer, getContainerName } from '../docker';
8
+ import { createWorkerClient } from '../worker/client';
8
9
  import { saveAgentConfig } from '../config/loader';
9
10
  import { setSessionName, getSessionNamesForWorkspace, deleteSessionName, } from '../sessions/metadata';
10
11
  import * as sessionRegistry from '../sessions/registry';
11
12
  import { discoverSSHKeys } from '../ssh/discovery';
12
- import { parseClaudeSessionContent } from '../sessions/parser';
13
13
  import { discoverAllSessions, getSessionDetails as getAgentSessionDetails, getSessionMessages, findSessionMessages, deleteSession as deleteSessionFromProvider, searchSessions as searchSessionsInContainer, } from '../sessions/agents';
14
14
  import { discoverClaudeCodeModels, discoverHostOpencodeModels, discoverContainerOpencodeModels, } from '../models/discovery';
15
- import { listOpencodeSessions, getOpencodeSessionMessages, deleteOpencodeSession, } from '../sessions/agents/opencode-storage';
15
+ import { deleteOpencodeSession } from '../sessions/agents/opencode-storage';
16
+ import { SessionIndex } from '../worker/session-index';
16
17
  import { sessionManager } from '../session-manager';
17
18
  const WorkspaceStatusSchema = z.enum(['running', 'stopped', 'creating', 'error']);
18
19
  const WorkspacePortsSchema = z.object({
@@ -95,7 +96,19 @@ export function createRouter(ctx) {
95
96
  if (!workspace) {
96
97
  throw new ORPCError('NOT_FOUND', { message: 'Workspace not found' });
97
98
  }
98
- return workspace;
99
+ let workerVersion = null;
100
+ if (workspace.status === 'running') {
101
+ try {
102
+ const containerName = getContainerName(input.name);
103
+ const client = await createWorkerClient(containerName);
104
+ const health = await client.health();
105
+ workerVersion = health.version;
106
+ }
107
+ catch {
108
+ // Worker not reachable
109
+ }
110
+ }
111
+ return { ...workspace, workerVersion };
99
112
  });
100
113
  const createWorkspace = os
101
114
  .input(z.object({
@@ -190,6 +203,15 @@ export function createRouter(ctx) {
190
203
  results,
191
204
  };
192
205
  });
206
+ const updateWorker = os.input(z.object({ name: z.string() })).handler(async ({ input }) => {
207
+ try {
208
+ await ctx.workspaces.updateWorkerBinary(input.name);
209
+ return { success: true };
210
+ }
211
+ catch (err) {
212
+ mapErrorToORPC(err, 'Failed to update worker');
213
+ }
214
+ });
193
215
  const touchWorkspace = os
194
216
  .input(z.object({ name: z.string() }))
195
217
  .output(WorkspaceInfoSchema)
@@ -411,170 +433,59 @@ export function createRouter(ctx) {
411
433
  });
412
434
  }
413
435
  });
436
+ const hostSessionIndex = new SessionIndex();
437
+ let hostSessionIndexInitialized = false;
414
438
  async function listHostSessions(input) {
439
+ if (!hostSessionIndexInitialized) {
440
+ await hostSessionIndex.initialize();
441
+ hostSessionIndex.startWatchers();
442
+ hostSessionIndexInitialized = true;
443
+ }
415
444
  const limit = input.limit ?? 50;
416
445
  const offset = input.offset ?? 0;
417
- const homeDir = os_module.homedir();
418
- const rawSessions = [];
419
- if (!input.agentType || input.agentType === 'claude-code') {
420
- const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
421
- try {
422
- const projectDirs = await fs.readdir(claudeProjectsDir);
423
- for (const projectDir of projectDirs) {
424
- const projectPath = path.join(claudeProjectsDir, projectDir);
425
- const stat = await fs.stat(projectPath);
426
- if (!stat.isDirectory())
427
- continue;
428
- const files = await fs.readdir(projectPath);
429
- for (const file of files) {
430
- if (!file.endsWith('.jsonl') || file.startsWith('agent-'))
431
- continue;
432
- const filePath = path.join(projectPath, file);
433
- const fileStat = await fs.stat(filePath);
434
- const sessionId = file.replace('.jsonl', '');
435
- rawSessions.push({
436
- id: sessionId,
437
- agentType: 'claude-code',
438
- projectPath: projectDir.replace(/-/g, '/'),
439
- mtime: fileStat.mtimeMs,
440
- filePath,
441
- });
442
- }
443
- }
444
- }
445
- catch {
446
- // Directory doesn't exist or not readable
447
- }
448
- }
449
- if (!input.agentType || input.agentType === 'opencode') {
450
- const opencodeSessions = await listOpencodeSessions();
451
- for (const session of opencodeSessions) {
452
- rawSessions.push({
453
- id: session.id,
454
- agentType: 'opencode',
455
- projectPath: session.directory || homeDir,
456
- mtime: session.mtime,
457
- filePath: session.file,
458
- name: session.title || undefined,
459
- });
460
- }
446
+ let sessions = hostSessionIndex.list();
447
+ if (input.agentType) {
448
+ const filterType = input.agentType === 'claude-code' ? 'claude' : input.agentType;
449
+ sessions = sessions.filter((s) => s.agentType === filterType);
461
450
  }
462
- rawSessions.sort((a, b) => b.mtime - a.mtime);
451
+ const nonEmptySessions = sessions.filter((s) => s.messageCount > 0);
463
452
  const sessionNames = await getSessionNamesForWorkspace(ctx.configDir, HOST_WORKSPACE_NAME);
464
- const sessions = await Promise.all(rawSessions.map(async (raw) => {
465
- let firstPrompt = null;
466
- let messageCount = 0;
467
- if (raw.agentType === 'claude-code') {
468
- try {
469
- const fileContent = await fs.readFile(raw.filePath, 'utf-8');
470
- const lines = fileContent.trim().split('\n').filter(Boolean);
471
- messageCount = lines.length;
472
- for (const line of lines) {
473
- try {
474
- const entry = JSON.parse(line);
475
- if ((entry.type === 'user' || entry.type === 'human') && entry.message?.content) {
476
- const msgContent = entry.message.content;
477
- if (Array.isArray(msgContent)) {
478
- const textBlock = msgContent.find((b) => b.type === 'text');
479
- if (textBlock?.text) {
480
- firstPrompt = textBlock.text.slice(0, 200);
481
- break;
482
- }
483
- }
484
- else if (typeof msgContent === 'string') {
485
- firstPrompt = msgContent.slice(0, 200);
486
- break;
487
- }
488
- }
489
- }
490
- catch {
491
- continue;
492
- }
493
- }
494
- }
495
- catch {
496
- // Can't read file
497
- }
498
- }
499
- else if (raw.agentType === 'opencode') {
500
- const sessionMessages = await getOpencodeSessionMessages(raw.id);
501
- const userAssistantMessages = sessionMessages.messages.filter((m) => m.type === 'user' || m.type === 'assistant');
502
- messageCount = userAssistantMessages.length;
503
- if (raw.name) {
504
- firstPrompt = raw.name;
505
- }
506
- else {
507
- const firstUserMsg = userAssistantMessages.find((m) => m.type === 'user' && m.content);
508
- if (firstUserMsg?.content) {
509
- firstPrompt = firstUserMsg.content.slice(0, 200);
510
- }
511
- }
512
- }
513
- return {
514
- id: raw.id,
515
- name: sessionNames[raw.id] || null,
516
- agentType: raw.agentType,
517
- projectPath: raw.projectPath,
518
- messageCount,
519
- lastActivity: new Date(raw.mtime).toISOString(),
520
- firstPrompt,
521
- };
453
+ const paginatedSessions = nonEmptySessions.slice(offset, offset + limit).map((s) => ({
454
+ id: s.id,
455
+ name: sessionNames[s.id] || null,
456
+ agentType: (s.agentType === 'claude' ? 'claude-code' : s.agentType),
457
+ projectPath: s.directory,
458
+ messageCount: s.messageCount,
459
+ lastActivity: new Date(s.lastActivity).toISOString(),
460
+ firstPrompt: s.firstPrompt,
522
461
  }));
523
- const nonEmptySessions = sessions.filter((s) => s.messageCount > 0);
524
- const paginatedSessions = nonEmptySessions.slice(offset, offset + limit);
525
462
  return {
526
463
  sessions: paginatedSessions,
527
464
  total: nonEmptySessions.length,
528
465
  hasMore: offset + limit < nonEmptySessions.length,
529
466
  };
530
467
  }
531
- async function getHostSession(sessionId, agentType) {
532
- const homeDir = os_module.homedir();
533
- const messages = [];
534
- if (!agentType || agentType === 'claude-code') {
535
- const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '');
536
- const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
537
- try {
538
- const projectDirs = await fs.readdir(claudeProjectsDir);
539
- for (const projectDir of projectDirs) {
540
- const sessionFile = path.join(claudeProjectsDir, projectDir, `${safeSessionId}.jsonl`);
541
- try {
542
- const content = await fs.readFile(sessionFile, 'utf-8');
543
- const parsed = parseClaudeSessionContent(content)
544
- .filter((msg) => msg.type !== 'system')
545
- .filter((msg) => msg.type === 'tool_use' ||
546
- msg.type === 'tool_result' ||
547
- (msg.content && msg.content.trim().length > 0));
548
- messages.push(...parsed);
549
- break;
550
- }
551
- catch {
552
- // File not found in this project dir
553
- }
554
- }
555
- }
556
- catch {
557
- // Directory doesn't exist
558
- }
559
- if (messages.length > 0) {
560
- return { id: sessionId, agentType: 'claude-code', messages };
561
- }
562
- }
563
- if (!agentType || agentType === 'opencode') {
564
- const sessionData = await getOpencodeSessionMessages(sessionId);
565
- if (sessionData.messages.length > 0) {
566
- const opencodeMessages = sessionData.messages.map((m) => ({
567
- type: m.type,
568
- content: m.content,
569
- toolName: m.toolName,
570
- toolId: m.toolId,
571
- toolInput: m.toolInput,
572
- timestamp: m.timestamp,
573
- }));
574
- return { id: sessionId, agentType: 'opencode', messages: opencodeMessages };
575
- }
468
+ async function getHostSession(sessionId, _agentType) {
469
+ if (!hostSessionIndexInitialized) {
470
+ await hostSessionIndex.initialize();
471
+ hostSessionIndex.startWatchers();
472
+ hostSessionIndexInitialized = true;
576
473
  }
577
- return { id: sessionId, messages };
474
+ const session = hostSessionIndex.get(sessionId);
475
+ if (!session) {
476
+ return { id: sessionId, messages: [] };
477
+ }
478
+ const result = await hostSessionIndex.getMessages(sessionId, { limit: 10000, offset: 0 });
479
+ const agentType = session.agentType === 'claude' ? 'claude-code' : session.agentType;
480
+ const messages = result.messages.map((m) => ({
481
+ type: m.type,
482
+ content: m.content,
483
+ toolName: m.toolName,
484
+ toolId: m.toolId,
485
+ toolInput: m.toolInput,
486
+ timestamp: m.timestamp,
487
+ }));
488
+ return { id: sessionId, agentType, messages };
578
489
  }
579
490
  async function listSessionsCore(input) {
580
491
  const limit = input.limit ?? 50;
@@ -616,16 +527,13 @@ export function createRouter(ctx) {
616
527
  .filter((s) => !input.agentType || s.agentType === input.agentType)
617
528
  .sort((a, b) => b.mtime - a.mtime);
618
529
  const paginatedRawSessions = filteredSessions.slice(offset, offset + limit);
619
- const sessions = [];
620
- for (const rawSession of paginatedRawSessions) {
621
- const details = await getAgentSessionDetails(containerName, rawSession, execInContainer);
622
- if (details) {
623
- sessions.push({
624
- ...details,
625
- name: customNames[details.id] || details.name,
626
- });
627
- }
628
- }
530
+ const detailsResults = await Promise.all(paginatedRawSessions.map((rawSession) => getAgentSessionDetails(containerName, rawSession, execInContainer)));
531
+ const sessions = detailsResults
532
+ .filter((details) => details !== null)
533
+ .map((details) => ({
534
+ ...details,
535
+ name: customNames[details.id] || details.name,
536
+ }));
629
537
  return {
630
538
  sessions,
631
539
  total: filteredSessions.length,
@@ -647,6 +555,7 @@ export function createRouter(ctx) {
647
555
  workspaceName: z.string(),
648
556
  sessionId: z.string(),
649
557
  agentType: z.enum(['claude-code', 'opencode', 'codex']).optional(),
558
+ projectPath: z.string().optional(),
650
559
  limit: z.number().optional(),
651
560
  offset: z.number().optional(),
652
561
  }))
@@ -670,7 +579,7 @@ export function createRouter(ctx) {
670
579
  }
671
580
  const containerName = `workspace-${input.workspaceName}`;
672
581
  result = input.agentType
673
- ? await getSessionMessages(containerName, input.sessionId, input.agentType, execInContainer)
582
+ ? await getSessionMessages(containerName, input.sessionId, input.agentType, execInContainer, input.projectPath)
674
583
  : await findSessionMessages(containerName, input.sessionId, execInContainer);
675
584
  }
676
585
  if (!result) {
@@ -1113,6 +1022,7 @@ export function createRouter(ctx) {
1113
1022
  touch: touchWorkspace,
1114
1023
  getPortForwards: getPortForwards,
1115
1024
  setPortForwards: setPortForwards,
1025
+ updateWorker: updateWorker,
1116
1026
  },
1117
1027
  sessions: {
1118
1028
  list: listSessions,