@gricha/perry 0.3.0 → 0.3.2

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,181 +0,0 @@
1
- export class BaseOpencodeSession {
2
- process = null;
3
- sessionId;
4
- model;
5
- sessionModel;
6
- onMessage;
7
- buffer = '';
8
- historyLoaded = false;
9
- constructor(sessionId, model, onMessage) {
10
- this.sessionId = sessionId;
11
- this.model = model;
12
- this.sessionModel = model;
13
- this.onMessage = onMessage;
14
- }
15
- async sendMessage(userMessage) {
16
- if (this.sessionId && !this.historyLoaded) {
17
- await this.loadHistory();
18
- }
19
- const logPrefix = this.getLogPrefix();
20
- const { command, options } = this.getSpawnConfig(userMessage);
21
- console.log(`[${logPrefix}] Running:`, command.join(' '));
22
- this.onMessage({
23
- type: 'system',
24
- content: 'Processing your message...',
25
- timestamp: new Date().toISOString(),
26
- });
27
- try {
28
- const proc = Bun.spawn(command, {
29
- stdin: 'ignore',
30
- stdout: 'pipe',
31
- stderr: 'pipe',
32
- ...options,
33
- });
34
- this.process = proc;
35
- if (!proc.stdout || !proc.stderr) {
36
- throw new Error('Failed to get process streams');
37
- }
38
- console.log(`[${logPrefix}] Process spawned, waiting for output...`);
39
- const stderrPromise = new Response(proc.stderr).text();
40
- const decoder = new TextDecoder();
41
- let receivedAnyOutput = false;
42
- for await (const chunk of proc.stdout) {
43
- const text = decoder.decode(chunk);
44
- console.log(`[${logPrefix}] Received chunk:`, text.length, 'bytes');
45
- receivedAnyOutput = true;
46
- this.buffer += text;
47
- this.processBuffer();
48
- }
49
- const exitCode = await proc.exited;
50
- console.log(`[${logPrefix}] Process exited with code:`, exitCode, 'receivedOutput:', receivedAnyOutput);
51
- const stderrText = await stderrPromise;
52
- if (stderrText) {
53
- console.error(`[${logPrefix}] stderr:`, stderrText);
54
- }
55
- if (exitCode !== 0) {
56
- this.onMessage({
57
- type: 'error',
58
- content: stderrText || `OpenCode exited with code ${exitCode}`,
59
- timestamp: new Date().toISOString(),
60
- });
61
- return;
62
- }
63
- if (!receivedAnyOutput) {
64
- this.onMessage({
65
- type: 'error',
66
- content: this.getNoOutputErrorMessage(),
67
- timestamp: new Date().toISOString(),
68
- });
69
- return;
70
- }
71
- this.onMessage({
72
- type: 'done',
73
- content: 'Response complete',
74
- timestamp: new Date().toISOString(),
75
- });
76
- }
77
- catch (err) {
78
- console.error(`[${logPrefix}] Error:`, err);
79
- this.onMessage({
80
- type: 'error',
81
- content: err.message,
82
- timestamp: new Date().toISOString(),
83
- });
84
- }
85
- finally {
86
- this.process = null;
87
- }
88
- }
89
- processBuffer() {
90
- const lines = this.buffer.split('\n');
91
- this.buffer = lines.pop() || '';
92
- for (const line of lines) {
93
- if (!line.trim())
94
- continue;
95
- try {
96
- const event = JSON.parse(line);
97
- this.handleStreamEvent(event);
98
- }
99
- catch {
100
- console.error(`[${this.getLogPrefix()}] Failed to parse:`, line);
101
- }
102
- }
103
- }
104
- handleStreamEvent(event) {
105
- const timestamp = new Date().toISOString();
106
- if (event.type === 'step_start' && event.sessionID) {
107
- if (!this.sessionId) {
108
- this.sessionId = event.sessionID;
109
- this.sessionModel = this.model;
110
- this.historyLoaded = true;
111
- this.onMessage({
112
- type: 'system',
113
- content: `Session started ${this.sessionId}`,
114
- timestamp,
115
- });
116
- }
117
- return;
118
- }
119
- if (event.type === 'text' && event.part?.text) {
120
- this.onMessage({
121
- type: 'assistant',
122
- content: event.part.text,
123
- timestamp,
124
- });
125
- return;
126
- }
127
- if (event.type === 'tool_use' && event.part) {
128
- const toolName = event.part.tool || 'unknown';
129
- const toolId = event.part.callID || event.part.id;
130
- const input = event.part.state?.input;
131
- const output = event.part.state?.output;
132
- const title = event.part.state?.title || input?.description || toolName;
133
- console.log(`[${this.getLogPrefix()}] Tool use:`, toolName, title);
134
- this.onMessage({
135
- type: 'tool_use',
136
- content: JSON.stringify(input, null, 2),
137
- toolName: title || toolName,
138
- toolId,
139
- timestamp,
140
- });
141
- if (output) {
142
- this.onMessage({
143
- type: 'tool_result',
144
- content: output,
145
- toolName,
146
- toolId,
147
- timestamp,
148
- });
149
- }
150
- return;
151
- }
152
- }
153
- async interrupt() {
154
- if (this.process) {
155
- this.process.kill();
156
- this.process = null;
157
- this.onMessage({
158
- type: 'system',
159
- content: 'Chat interrupted',
160
- timestamp: new Date().toISOString(),
161
- });
162
- }
163
- }
164
- setModel(model) {
165
- if (this.model !== model) {
166
- this.model = model;
167
- if (this.sessionModel !== model) {
168
- this.sessionId = undefined;
169
- this.historyLoaded = false;
170
- this.onMessage({
171
- type: 'system',
172
- content: `Switching to model: ${model}`,
173
- timestamp: new Date().toISOString(),
174
- });
175
- }
176
- }
177
- }
178
- getSessionId() {
179
- return this.sessionId;
180
- }
181
- }
@@ -1,47 +0,0 @@
1
- import { BaseClaudeSession } from './base-claude-session';
2
- export class ChatSession extends BaseClaudeSession {
3
- containerName;
4
- workDir;
5
- constructor(options, onMessage) {
6
- super(options.sessionId, options.model, onMessage);
7
- this.containerName = options.containerName;
8
- this.workDir = options.workDir || '/home/workspace';
9
- }
10
- getLogPrefix() {
11
- return 'chat';
12
- }
13
- getSpawnConfig(userMessage) {
14
- const args = [
15
- 'docker',
16
- 'exec',
17
- '-u',
18
- 'workspace',
19
- '-w',
20
- this.workDir,
21
- this.containerName,
22
- 'claude',
23
- '--print',
24
- '--verbose',
25
- '--output-format',
26
- 'stream-json',
27
- '--include-partial-messages',
28
- '--model',
29
- this.model,
30
- '--dangerously-skip-permissions',
31
- ];
32
- if (this.sessionId) {
33
- args.push('--resume', this.sessionId);
34
- }
35
- args.push(userMessage);
36
- return {
37
- command: args,
38
- options: {},
39
- };
40
- }
41
- getNoOutputErrorMessage() {
42
- return 'No response from Claude. Check if Claude is authenticated in the workspace.';
43
- }
44
- }
45
- export function createChatSession(options, onMessage) {
46
- return new ChatSession(options, onMessage);
47
- }
@@ -1,41 +0,0 @@
1
- import { homedir } from 'os';
2
- import { BaseClaudeSession } from './base-claude-session';
3
- export class HostChatSession extends BaseClaudeSession {
4
- workDir;
5
- constructor(options, onMessage) {
6
- super(options.sessionId, options.model, onMessage);
7
- this.workDir = options.workDir || homedir();
8
- }
9
- getLogPrefix() {
10
- return 'host-chat';
11
- }
12
- getSpawnConfig(userMessage) {
13
- const args = [
14
- 'claude',
15
- '--print',
16
- '--verbose',
17
- '--output-format',
18
- 'stream-json',
19
- '--include-partial-messages',
20
- '--model',
21
- this.model,
22
- '--dangerously-skip-permissions',
23
- ];
24
- if (this.sessionId) {
25
- args.push('--resume', this.sessionId);
26
- }
27
- args.push(userMessage);
28
- return {
29
- command: args,
30
- options: {
31
- cwd: this.workDir,
32
- env: {
33
- ...process.env,
34
- },
35
- },
36
- };
37
- }
38
- }
39
- export function createHostChatSession(options, onMessage) {
40
- return new HostChatSession(options, onMessage);
41
- }
@@ -1,144 +0,0 @@
1
- import { homedir } from 'os';
2
- import { promises as fs } from 'fs';
3
- import path from 'path';
4
- import { BaseOpencodeSession } from './base-opencode-session';
5
- export class HostOpencodeSession extends BaseOpencodeSession {
6
- workDir;
7
- constructor(options, onMessage) {
8
- super(options.sessionId, options.model, onMessage);
9
- this.workDir = options.workDir || homedir();
10
- }
11
- getLogPrefix() {
12
- return 'host-opencode';
13
- }
14
- getNoOutputErrorMessage() {
15
- return 'No response from OpenCode. Check if OpenCode is installed and configured.';
16
- }
17
- getSpawnConfig(userMessage) {
18
- const args = ['stdbuf', '-oL', 'opencode', 'run', '--format', 'json'];
19
- if (this.sessionId) {
20
- args.push('--session', this.sessionId);
21
- }
22
- if (this.model) {
23
- args.push('--model', this.model);
24
- }
25
- args.push(userMessage);
26
- return {
27
- command: args,
28
- options: {
29
- cwd: this.workDir,
30
- env: {
31
- ...process.env,
32
- },
33
- },
34
- };
35
- }
36
- async loadHistory() {
37
- if (this.historyLoaded || !this.sessionId) {
38
- return;
39
- }
40
- this.historyLoaded = true;
41
- try {
42
- const homeDir = homedir();
43
- const sessionDir = path.join(homeDir, '.local', 'share', 'opencode', 'storage', 'session');
44
- const sessionFile = path.join(sessionDir, `${this.sessionId}.json`);
45
- let internalId;
46
- try {
47
- const sessionContent = await fs.readFile(sessionFile, 'utf-8');
48
- const session = JSON.parse(sessionContent);
49
- internalId = session.id;
50
- }
51
- catch {
52
- return;
53
- }
54
- const msgDir = path.join(homeDir, '.local', 'share', 'opencode', 'storage', 'message', internalId);
55
- const partDir = path.join(homeDir, '.local', 'share', 'opencode', 'storage', 'part');
56
- let msgFiles;
57
- try {
58
- const files = await fs.readdir(msgDir);
59
- msgFiles = files.filter((f) => f.startsWith('msg_') && f.endsWith('.json')).sort();
60
- }
61
- catch {
62
- return;
63
- }
64
- const messages = [];
65
- for (const msgFile of msgFiles) {
66
- try {
67
- const msgContent = await fs.readFile(path.join(msgDir, msgFile), 'utf-8');
68
- const msg = JSON.parse(msgContent);
69
- if (!msg.id || (msg.role !== 'user' && msg.role !== 'assistant'))
70
- continue;
71
- const timestamp = msg.time?.created
72
- ? new Date(msg.time.created).toISOString()
73
- : new Date().toISOString();
74
- const partMsgDir = path.join(partDir, msg.id);
75
- let partFiles;
76
- try {
77
- const files = await fs.readdir(partMsgDir);
78
- partFiles = files.filter((f) => f.startsWith('prt_') && f.endsWith('.json')).sort();
79
- }
80
- catch {
81
- continue;
82
- }
83
- for (const partFile of partFiles) {
84
- try {
85
- const partContent = await fs.readFile(path.join(partMsgDir, partFile), 'utf-8');
86
- const part = JSON.parse(partContent);
87
- if (part.type === 'text' && part.text) {
88
- messages.push({
89
- type: msg.role,
90
- content: part.text,
91
- timestamp,
92
- });
93
- }
94
- else if (part.type === 'tool' && part.tool) {
95
- messages.push({
96
- type: 'tool_use',
97
- content: JSON.stringify(part.state?.input, null, 2),
98
- toolName: part.state?.title || part.tool,
99
- toolId: part.callID || part.id,
100
- timestamp,
101
- });
102
- if (part.state?.output) {
103
- messages.push({
104
- type: 'tool_result',
105
- content: part.state.output,
106
- toolId: part.callID || part.id,
107
- timestamp,
108
- });
109
- }
110
- }
111
- }
112
- catch {
113
- continue;
114
- }
115
- }
116
- }
117
- catch {
118
- continue;
119
- }
120
- }
121
- if (messages.length > 0) {
122
- this.onMessage({
123
- type: 'system',
124
- content: `Loading ${messages.length} messages from session history...`,
125
- timestamp: new Date().toISOString(),
126
- });
127
- for (const msg of messages) {
128
- this.onMessage(msg);
129
- }
130
- this.onMessage({
131
- type: 'system',
132
- content: 'Session history loaded',
133
- timestamp: new Date().toISOString(),
134
- });
135
- }
136
- }
137
- catch (err) {
138
- console.error('[host-opencode] Failed to load history:', err);
139
- }
140
- }
141
- }
142
- export function createHostOpencodeSession(options, onMessage) {
143
- return new HostOpencodeSession(options, onMessage);
144
- }
@@ -1,2 +0,0 @@
1
- export * from './handler';
2
- export * from './websocket';
@@ -1,100 +0,0 @@
1
- import { BaseOpencodeSession } from './base-opencode-session';
2
- import { opencodeProvider } from '../sessions/agents/opencode';
3
- export class OpencodeSession extends BaseOpencodeSession {
4
- containerName;
5
- workDir;
6
- constructor(options, onMessage) {
7
- super(options.sessionId, options.model, onMessage);
8
- this.containerName = options.containerName;
9
- this.workDir = options.workDir || '/home/workspace';
10
- }
11
- getLogPrefix() {
12
- return 'opencode';
13
- }
14
- getNoOutputErrorMessage() {
15
- return 'No response from OpenCode. Check if OpenCode is configured in the workspace.';
16
- }
17
- getSpawnConfig(userMessage) {
18
- const args = [
19
- 'docker',
20
- 'exec',
21
- '-i',
22
- '-u',
23
- 'workspace',
24
- '-w',
25
- this.workDir,
26
- this.containerName,
27
- 'stdbuf',
28
- '-oL',
29
- 'opencode',
30
- 'run',
31
- '--format',
32
- 'json',
33
- ];
34
- if (this.sessionId) {
35
- args.push('--session', this.sessionId);
36
- }
37
- if (this.model) {
38
- args.push('--model', this.model);
39
- }
40
- args.push(userMessage);
41
- return {
42
- command: args,
43
- options: {},
44
- };
45
- }
46
- async loadHistory() {
47
- if (this.historyLoaded || !this.sessionId) {
48
- return;
49
- }
50
- this.historyLoaded = true;
51
- const exec = async (containerName, command, options) => {
52
- const args = ['exec'];
53
- if (options?.user) {
54
- args.push('-u', options.user);
55
- }
56
- args.push(containerName, ...command);
57
- const proc = Bun.spawn(['docker', ...args], {
58
- stdin: 'ignore',
59
- stdout: 'pipe',
60
- stderr: 'pipe',
61
- });
62
- const [stdout, stderr, exitCode] = await Promise.all([
63
- new Response(proc.stdout).text(),
64
- new Response(proc.stderr).text(),
65
- proc.exited,
66
- ]);
67
- return { stdout, stderr, exitCode };
68
- };
69
- try {
70
- const result = await opencodeProvider.getSessionMessages(this.containerName, this.sessionId, exec);
71
- if (result && result.messages.length > 0) {
72
- this.onMessage({
73
- type: 'system',
74
- content: `Loading ${result.messages.length} messages from session history...`,
75
- timestamp: new Date().toISOString(),
76
- });
77
- for (const msg of result.messages) {
78
- this.onMessage({
79
- type: msg.type,
80
- content: msg.content || '',
81
- toolName: msg.toolName,
82
- toolId: msg.toolId,
83
- timestamp: msg.timestamp || new Date().toISOString(),
84
- });
85
- }
86
- this.onMessage({
87
- type: 'system',
88
- content: 'Session history loaded',
89
- timestamp: new Date().toISOString(),
90
- });
91
- }
92
- }
93
- catch (err) {
94
- console.error('[opencode] Failed to load history:', err);
95
- }
96
- }
97
- }
98
- export function createOpencodeSession(options, onMessage) {
99
- return new OpencodeSession(options, onMessage);
100
- }