@gricha/perry 0.2.2 → 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/README.md +8 -0
- package/dist/agent/file-watcher.js +138 -0
- package/dist/agent/router.js +5 -1
- package/dist/agent/run.js +29 -3
- package/dist/agent/web/assets/{index-DN_QW9sL.js → index-BF-4SpMu.js} +21 -21
- package/dist/agent/web/index.html +1 -1
- package/dist/chat/base-chat-websocket.js +1 -1
- package/dist/chat/base-claude-session.js +169 -0
- package/dist/chat/base-opencode-session.js +181 -0
- package/dist/chat/handler.js +14 -157
- package/dist/chat/host-handler.js +13 -142
- package/dist/chat/host-opencode-handler.js +28 -187
- package/dist/chat/opencode-handler.js +38 -197
- package/dist/chat/types.js +1 -0
- package/dist/docker/index.js +1 -1
- package/dist/perry-worker +0 -0
- package/dist/shared/constants.js +1 -0
- package/dist/shared/types.js +0 -1
- package/dist/terminal/websocket.js +1 -1
- package/package.json +4 -3
|
@@ -5,7 +5,7 @@
|
|
|
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-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BF-4SpMu.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-DIOWcVH-.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseWebSocketServer, safeSend } from '../shared/base-websocket';
|
|
2
2
|
import { getContainerName } from '../docker';
|
|
3
|
-
import { HOST_WORKSPACE_NAME } from '../shared/types';
|
|
3
|
+
import { HOST_WORKSPACE_NAME } from '../shared/client-types';
|
|
4
4
|
export class BaseChatWebSocketServer extends BaseWebSocketServer {
|
|
5
5
|
isHostAccessAllowed;
|
|
6
6
|
constructor(options) {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { DEFAULT_CLAUDE_MODEL } from '../shared/constants';
|
|
2
|
+
export class BaseClaudeSession {
|
|
3
|
+
process = null;
|
|
4
|
+
sessionId;
|
|
5
|
+
model;
|
|
6
|
+
sessionModel;
|
|
7
|
+
onMessage;
|
|
8
|
+
buffer = '';
|
|
9
|
+
constructor(sessionId, model, onMessage) {
|
|
10
|
+
this.sessionId = sessionId;
|
|
11
|
+
this.model = model || DEFAULT_CLAUDE_MODEL;
|
|
12
|
+
this.sessionModel = this.model;
|
|
13
|
+
this.onMessage = onMessage;
|
|
14
|
+
}
|
|
15
|
+
async sendMessage(userMessage) {
|
|
16
|
+
const logPrefix = this.getLogPrefix();
|
|
17
|
+
const { command, options } = this.getSpawnConfig(userMessage);
|
|
18
|
+
console.log(`[${logPrefix}] Running:`, command.join(' '));
|
|
19
|
+
this.onMessage({
|
|
20
|
+
type: 'system',
|
|
21
|
+
content: 'Processing your message...',
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
const proc = Bun.spawn(command, {
|
|
26
|
+
stdin: 'ignore',
|
|
27
|
+
stdout: 'pipe',
|
|
28
|
+
stderr: 'pipe',
|
|
29
|
+
...options,
|
|
30
|
+
});
|
|
31
|
+
this.process = proc;
|
|
32
|
+
if (!proc.stdout || !proc.stderr) {
|
|
33
|
+
throw new Error('Failed to get process streams');
|
|
34
|
+
}
|
|
35
|
+
console.log(`[${logPrefix}] Process spawned, waiting for output...`);
|
|
36
|
+
const stderrPromise = new Response(proc.stderr).text();
|
|
37
|
+
const decoder = new TextDecoder();
|
|
38
|
+
let receivedAnyOutput = false;
|
|
39
|
+
for await (const chunk of proc.stdout) {
|
|
40
|
+
const text = decoder.decode(chunk);
|
|
41
|
+
console.log(`[${logPrefix}] Received chunk:`, text.length, 'bytes');
|
|
42
|
+
receivedAnyOutput = true;
|
|
43
|
+
this.buffer += text;
|
|
44
|
+
this.processBuffer();
|
|
45
|
+
}
|
|
46
|
+
const exitCode = await proc.exited;
|
|
47
|
+
console.log(`[${logPrefix}] Process exited with code:`, exitCode, 'receivedOutput:', receivedAnyOutput);
|
|
48
|
+
const stderrText = await stderrPromise;
|
|
49
|
+
if (stderrText) {
|
|
50
|
+
console.error(`[${logPrefix}] stderr:`, stderrText);
|
|
51
|
+
}
|
|
52
|
+
if (exitCode !== 0) {
|
|
53
|
+
this.onMessage({
|
|
54
|
+
type: 'error',
|
|
55
|
+
content: stderrText || `Claude exited with code ${exitCode}`,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!receivedAnyOutput) {
|
|
61
|
+
this.onMessage({
|
|
62
|
+
type: 'error',
|
|
63
|
+
content: this.getNoOutputErrorMessage(),
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.onMessage({
|
|
69
|
+
type: 'done',
|
|
70
|
+
content: 'Response complete',
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(`[${logPrefix}] Error:`, err);
|
|
76
|
+
this.onMessage({
|
|
77
|
+
type: 'error',
|
|
78
|
+
content: err.message,
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
this.process = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
getNoOutputErrorMessage() {
|
|
87
|
+
return 'No response from Claude. Check if Claude is authenticated.';
|
|
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 msg = JSON.parse(line);
|
|
97
|
+
this.handleStreamMessage(msg);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
console.error(`[${this.getLogPrefix()}] Failed to parse:`, line);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
handleStreamMessage(msg) {
|
|
105
|
+
const timestamp = new Date().toISOString();
|
|
106
|
+
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
107
|
+
this.sessionId = msg.session_id;
|
|
108
|
+
this.sessionModel = this.model;
|
|
109
|
+
this.onMessage({
|
|
110
|
+
type: 'system',
|
|
111
|
+
content: `Session started: ${msg.session_id?.slice(0, 8)}...`,
|
|
112
|
+
timestamp,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
117
|
+
for (const block of msg.message.content) {
|
|
118
|
+
if (block.type === 'tool_use') {
|
|
119
|
+
this.onMessage({
|
|
120
|
+
type: 'tool_use',
|
|
121
|
+
content: JSON.stringify(block.input, null, 2),
|
|
122
|
+
toolName: block.name,
|
|
123
|
+
toolId: block.id,
|
|
124
|
+
timestamp,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (msg.type === 'stream_event' && msg.event?.type === 'content_block_delta') {
|
|
131
|
+
const delta = msg.event?.delta;
|
|
132
|
+
if (delta?.type === 'text_delta' && delta?.text) {
|
|
133
|
+
this.onMessage({
|
|
134
|
+
type: 'assistant',
|
|
135
|
+
content: delta.text,
|
|
136
|
+
timestamp,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async interrupt() {
|
|
143
|
+
if (this.process) {
|
|
144
|
+
this.process.kill();
|
|
145
|
+
this.process = null;
|
|
146
|
+
this.onMessage({
|
|
147
|
+
type: 'system',
|
|
148
|
+
content: 'Chat interrupted',
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
setModel(model) {
|
|
154
|
+
if (this.model !== model) {
|
|
155
|
+
this.model = model;
|
|
156
|
+
if (this.sessionModel !== model) {
|
|
157
|
+
this.sessionId = undefined;
|
|
158
|
+
this.onMessage({
|
|
159
|
+
type: 'system',
|
|
160
|
+
content: `Switching to model: ${model}`,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
getSessionId() {
|
|
167
|
+
return this.sessionId;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
}
|
package/dist/chat/handler.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { BaseClaudeSession } from './base-claude-session';
|
|
2
|
+
export class ChatSession extends BaseClaudeSession {
|
|
3
3
|
containerName;
|
|
4
4
|
workDir;
|
|
5
|
-
sessionId;
|
|
6
|
-
model;
|
|
7
|
-
sessionModel;
|
|
8
|
-
onMessage;
|
|
9
|
-
buffer = '';
|
|
10
5
|
constructor(options, onMessage) {
|
|
6
|
+
super(options.sessionId, options.model, onMessage);
|
|
11
7
|
this.containerName = options.containerName;
|
|
12
8
|
this.workDir = options.workDir || '/home/workspace';
|
|
13
|
-
this.sessionId = options.sessionId;
|
|
14
|
-
this.model = options.model || 'sonnet';
|
|
15
|
-
this.sessionModel = this.model;
|
|
16
|
-
this.onMessage = onMessage;
|
|
17
9
|
}
|
|
18
|
-
|
|
10
|
+
getLogPrefix() {
|
|
11
|
+
return 'chat';
|
|
12
|
+
}
|
|
13
|
+
getSpawnConfig(userMessage) {
|
|
19
14
|
const args = [
|
|
15
|
+
'docker',
|
|
20
16
|
'exec',
|
|
21
17
|
'-u',
|
|
22
18
|
'workspace',
|
|
@@ -37,152 +33,13 @@ export class ChatSession {
|
|
|
37
33
|
args.push('--resume', this.sessionId);
|
|
38
34
|
}
|
|
39
35
|
args.push(userMessage);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
timestamp: new Date().toISOString(),
|
|
45
|
-
});
|
|
46
|
-
try {
|
|
47
|
-
const proc = Bun.spawn(['docker', ...args], {
|
|
48
|
-
stdin: 'ignore',
|
|
49
|
-
stdout: 'pipe',
|
|
50
|
-
stderr: 'pipe',
|
|
51
|
-
});
|
|
52
|
-
this.process = proc;
|
|
53
|
-
if (!proc.stdout || !proc.stderr) {
|
|
54
|
-
throw new Error('Failed to get process streams');
|
|
55
|
-
}
|
|
56
|
-
console.log('[chat] Process spawned, waiting for output...');
|
|
57
|
-
const stderrPromise = new Response(proc.stderr).text();
|
|
58
|
-
const decoder = new TextDecoder();
|
|
59
|
-
let receivedAnyOutput = false;
|
|
60
|
-
for await (const chunk of proc.stdout) {
|
|
61
|
-
const text = decoder.decode(chunk);
|
|
62
|
-
console.log('[chat] Received chunk:', text.length, 'bytes');
|
|
63
|
-
receivedAnyOutput = true;
|
|
64
|
-
this.buffer += text;
|
|
65
|
-
this.processBuffer();
|
|
66
|
-
}
|
|
67
|
-
const exitCode = await proc.exited;
|
|
68
|
-
console.log('[chat] Process exited with code:', exitCode, 'receivedOutput:', receivedAnyOutput);
|
|
69
|
-
const stderrText = await stderrPromise;
|
|
70
|
-
if (stderrText) {
|
|
71
|
-
console.error('[chat] stderr:', stderrText);
|
|
72
|
-
}
|
|
73
|
-
if (exitCode !== 0) {
|
|
74
|
-
this.onMessage({
|
|
75
|
-
type: 'error',
|
|
76
|
-
content: stderrText || `Claude exited with code ${exitCode}`,
|
|
77
|
-
timestamp: new Date().toISOString(),
|
|
78
|
-
});
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (!receivedAnyOutput) {
|
|
82
|
-
this.onMessage({
|
|
83
|
-
type: 'error',
|
|
84
|
-
content: 'No response from Claude. Check if Claude is authenticated in the workspace.',
|
|
85
|
-
timestamp: new Date().toISOString(),
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
this.onMessage({
|
|
90
|
-
type: 'done',
|
|
91
|
-
content: 'Response complete',
|
|
92
|
-
timestamp: new Date().toISOString(),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
console.error('[chat] Error:', err);
|
|
97
|
-
this.onMessage({
|
|
98
|
-
type: 'error',
|
|
99
|
-
content: err.message,
|
|
100
|
-
timestamp: new Date().toISOString(),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
finally {
|
|
104
|
-
this.process = null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
processBuffer() {
|
|
108
|
-
const lines = this.buffer.split('\n');
|
|
109
|
-
this.buffer = lines.pop() || '';
|
|
110
|
-
for (const line of lines) {
|
|
111
|
-
if (!line.trim())
|
|
112
|
-
continue;
|
|
113
|
-
try {
|
|
114
|
-
const msg = JSON.parse(line);
|
|
115
|
-
this.handleStreamMessage(msg);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
console.error('[chat] Failed to parse:', line);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
handleStreamMessage(msg) {
|
|
123
|
-
const timestamp = new Date().toISOString();
|
|
124
|
-
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
125
|
-
this.sessionId = msg.session_id;
|
|
126
|
-
this.sessionModel = this.model;
|
|
127
|
-
this.onMessage({
|
|
128
|
-
type: 'system',
|
|
129
|
-
content: `Session started: ${msg.session_id?.slice(0, 8)}...`,
|
|
130
|
-
timestamp,
|
|
131
|
-
});
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (msg.type === 'assistant' && msg.message?.content) {
|
|
135
|
-
for (const block of msg.message.content) {
|
|
136
|
-
if (block.type === 'tool_use') {
|
|
137
|
-
this.onMessage({
|
|
138
|
-
type: 'tool_use',
|
|
139
|
-
content: JSON.stringify(block.input, null, 2),
|
|
140
|
-
toolName: block.name,
|
|
141
|
-
toolId: block.id,
|
|
142
|
-
timestamp,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (msg.type === 'stream_event' && msg.event?.type === 'content_block_delta') {
|
|
149
|
-
const delta = msg.event?.delta;
|
|
150
|
-
if (delta?.type === 'text_delta' && delta?.text) {
|
|
151
|
-
this.onMessage({
|
|
152
|
-
type: 'assistant',
|
|
153
|
-
content: delta.text,
|
|
154
|
-
timestamp,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
async interrupt() {
|
|
161
|
-
if (this.process) {
|
|
162
|
-
this.process.kill();
|
|
163
|
-
this.process = null;
|
|
164
|
-
this.onMessage({
|
|
165
|
-
type: 'system',
|
|
166
|
-
content: 'Chat interrupted',
|
|
167
|
-
timestamp: new Date().toISOString(),
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
setModel(model) {
|
|
172
|
-
if (this.model !== model) {
|
|
173
|
-
this.model = model;
|
|
174
|
-
if (this.sessionModel !== model) {
|
|
175
|
-
this.sessionId = undefined;
|
|
176
|
-
this.onMessage({
|
|
177
|
-
type: 'system',
|
|
178
|
-
content: `Switching to model: ${model}`,
|
|
179
|
-
timestamp: new Date().toISOString(),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
36
|
+
return {
|
|
37
|
+
command: args,
|
|
38
|
+
options: {},
|
|
39
|
+
};
|
|
183
40
|
}
|
|
184
|
-
|
|
185
|
-
return
|
|
41
|
+
getNoOutputErrorMessage() {
|
|
42
|
+
return 'No response from Claude. Check if Claude is authenticated in the workspace.';
|
|
186
43
|
}
|
|
187
44
|
}
|
|
188
45
|
export function createChatSession(options, onMessage) {
|