@gricha/perry 0.3.7 → 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.
- package/dist/agent/router.js +76 -166
- package/dist/agent/web/assets/{index-DCV4YnD4.js → index-hNfXv8YX.js} +34 -34
- package/dist/agent/web/index.html +1 -1
- package/dist/index.js +54 -2
- package/dist/perry-worker +0 -0
- package/dist/sessions/agents/claude.js +17 -10
- package/dist/sessions/agents/codex.js +1 -1
- package/dist/sessions/agents/index.js +31 -32
- package/dist/sessions/agents/opencode-storage.js +11 -0
- package/dist/sessions/agents/opencode.js +1 -1
- package/dist/sessions/agents/utils.js +3 -0
- package/dist/sessions/agents/worker-provider.js +66 -0
- package/dist/worker/client.js +105 -0
- package/dist/worker/server.js +69 -0
- package/dist/worker/session-index.js +331 -0
- package/dist/workspace/manager.js +63 -4
- package/package.json +1 -1
package/dist/agent/router.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
451
|
+
const nonEmptySessions = sessions.filter((s) => s.messageCount > 0);
|
|
463
452
|
const sessionNames = await getSessionNamesForWorkspace(ctx.configDir, HOST_WORKSPACE_NAME);
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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,
|