upfynai-code 2.8.6 → 2.9.0
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/bin/cli.js +1 -1
- package/dist/agents/claude.js +146 -114
- package/package.json +2 -1
- package/src/connect.js +22 -3
package/bin/cli.js
CHANGED
|
@@ -55,7 +55,7 @@ program
|
|
|
55
55
|
.command('connect')
|
|
56
56
|
.description('Connect local machine to the remote server (bridges Claude Code, shell, files, git)')
|
|
57
57
|
.option('--server <url>', 'Server URL to connect to')
|
|
58
|
-
.option('--key <token>', '
|
|
58
|
+
.option('--key <token>', 'Connection token — get from Settings > Keys & Connect')
|
|
59
59
|
.action(async (options) => {
|
|
60
60
|
await connect(options);
|
|
61
61
|
});
|
package/dist/agents/claude.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* These are STREAMING actions — they use ctx.stream() to send chunks
|
|
6
6
|
* and return a Promise that resolves when the process completes.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
8
|
+
* Three modes:
|
|
9
|
+
* - sdk (connect.js default) — full SDK streaming with rich messages (tool_use, text, result)
|
|
10
10
|
* - simple --print (relay-client.js) — basic stdout streaming
|
|
11
11
|
*
|
|
12
12
|
* The caller chooses the mode via ctx.streamMode ('structured' | 'simple').
|
|
13
|
+
* 'structured' now maps to SDK mode for rich message forwarding.
|
|
13
14
|
*/
|
|
15
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
14
16
|
import { spawn } from 'child_process';
|
|
15
17
|
import os from 'os';
|
|
16
18
|
|
|
@@ -20,137 +22,183 @@ export default {
|
|
|
20
22
|
'claude-query': async (params, ctx) => {
|
|
21
23
|
const { command, options } = params;
|
|
22
24
|
const mode = ctx.streamMode || 'structured';
|
|
23
|
-
const resolveBinary = ctx.resolveBinary || ((name) => name);
|
|
24
25
|
|
|
25
26
|
if (mode === 'simple') {
|
|
27
|
+
const resolveBinary = ctx.resolveBinary || ((name) => name);
|
|
26
28
|
return runSimpleClaudeQuery(command, options, ctx, resolveBinary);
|
|
27
29
|
}
|
|
28
|
-
return
|
|
30
|
+
return runSDKClaudeQuery(command, options, ctx);
|
|
29
31
|
},
|
|
30
32
|
|
|
31
33
|
'claude-task-query': async (params, ctx) => {
|
|
32
34
|
const { command, options } = params;
|
|
33
|
-
|
|
34
|
-
return runClaudeTaskQuery(command, options, ctx, resolveBinary);
|
|
35
|
+
return runSDKClaudeTaskQuery(command, options, ctx);
|
|
35
36
|
},
|
|
36
37
|
},
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* Build SDK options from relay command options.
|
|
42
|
+
* Maps the relay format to SDK Options type.
|
|
42
43
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
function buildSDKOptions(options = {}) {
|
|
45
|
+
const sdkOptions = {
|
|
46
|
+
cwd: options.projectPath || os.homedir(),
|
|
47
|
+
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
48
|
+
settingSources: ['project', 'user', 'local'],
|
|
49
|
+
};
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
env: process.env,
|
|
52
|
-
});
|
|
51
|
+
if (options.sessionId) {
|
|
52
|
+
sdkOptions.resume = options.sessionId;
|
|
53
|
+
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
if (options.model) {
|
|
56
|
+
sdkOptions.model = options.model;
|
|
57
|
+
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
if (options.maxBudgetUsd) {
|
|
60
|
+
sdkOptions.maxBudgetUsd = options.maxBudgetUsd;
|
|
61
|
+
}
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
stdoutBuffer = lines.pop();
|
|
64
|
-
|
|
65
|
-
for (const line of lines) {
|
|
66
|
-
if (!line.trim()) continue;
|
|
67
|
-
try {
|
|
68
|
-
const evt = JSON.parse(line);
|
|
69
|
-
|
|
70
|
-
if (evt.type === 'system' && evt.subtype === 'init') {
|
|
71
|
-
if (evt.session_id) capturedSessionId = evt.session_id;
|
|
72
|
-
lastTextLength = 0;
|
|
73
|
-
ctx.stream({ type: 'claude-system', sessionId: evt.session_id, model: evt.model, cwd: evt.cwd });
|
|
74
|
-
} else if (evt.type === 'assistant' && evt.message?.content?.length) {
|
|
75
|
-
const fullText = evt.message.content[0].text || '';
|
|
76
|
-
const delta = fullText.slice(lastTextLength);
|
|
77
|
-
lastTextLength = fullText.length;
|
|
78
|
-
if (delta) {
|
|
79
|
-
ctx.stream({ type: 'claude-response', content: delta });
|
|
80
|
-
}
|
|
81
|
-
} else if (evt.type === 'result') {
|
|
82
|
-
lastTextLength = 0;
|
|
83
|
-
ctx.stream({ type: 'claude-result', sessionId: capturedSessionId, subtype: evt.subtype });
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
if (line.trim() && !line.startsWith('%') && !line.includes('claude query')) {
|
|
87
|
-
ctx.stream({ type: 'claude-response', content: line });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
63
|
+
if (options.permissionMode && options.permissionMode !== 'default') {
|
|
64
|
+
sdkOptions.permissionMode = options.permissionMode;
|
|
65
|
+
}
|
|
92
66
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
67
|
+
if (options.allowedTools && options.allowedTools.length > 0) {
|
|
68
|
+
sdkOptions.allowedTools = options.allowedTools;
|
|
69
|
+
}
|
|
96
70
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
71
|
+
if (options.disallowedTools && options.disallowedTools.length > 0) {
|
|
72
|
+
sdkOptions.disallowedTools = options.disallowedTools;
|
|
73
|
+
}
|
|
101
74
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
75
|
+
if (options.maxTurns) {
|
|
76
|
+
sdkOptions.maxTurns = options.maxTurns;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pass through tools preset to make all Claude Code tools available
|
|
80
|
+
sdkOptions.tools = { type: 'preset', preset: 'claude_code' };
|
|
81
|
+
|
|
82
|
+
return sdkOptions;
|
|
107
83
|
}
|
|
108
84
|
|
|
109
85
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
86
|
+
* SDK-based structured streaming mode.
|
|
87
|
+
* Forwards rich SDK messages (tool_use, tool_result, text, system, result)
|
|
88
|
+
* through ctx.stream() for the relay to forward to the frontend.
|
|
112
89
|
*/
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
90
|
+
async function runSDKClaudeQuery(command, options, ctx) {
|
|
91
|
+
const sdkOptions = buildSDKOptions(options);
|
|
92
|
+
|
|
93
|
+
// Set stream-close timeout for interactive tools
|
|
94
|
+
const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
|
95
|
+
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
|
|
96
|
+
|
|
97
|
+
let queryInstance;
|
|
98
|
+
try {
|
|
99
|
+
queryInstance = query({
|
|
100
|
+
prompt: command || '',
|
|
101
|
+
options: sdkOptions,
|
|
122
102
|
});
|
|
103
|
+
} finally {
|
|
104
|
+
// Restore immediately — Query constructor already captured the value
|
|
105
|
+
if (prevStreamTimeout !== undefined) {
|
|
106
|
+
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
|
|
107
|
+
} else {
|
|
108
|
+
delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Track the query instance for abort support (uses .interrupt() instead of .kill())
|
|
113
|
+
if (ctx.trackProcess) {
|
|
114
|
+
ctx.trackProcess(ctx.requestId, { instance: queryInstance, action: 'claude-query' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let capturedSessionId = null;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
for await (const message of queryInstance) {
|
|
121
|
+
// Capture session ID from first message that has one
|
|
122
|
+
if (message.session_id && !capturedSessionId) {
|
|
123
|
+
capturedSessionId = message.session_id;
|
|
124
|
+
}
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
// Forward the full SDK message — the backend routeViaRelay() will
|
|
127
|
+
// detect 'claude-sdk-message' type and forward directly to frontend,
|
|
128
|
+
// matching the same format as queryClaudeSDK() in claude-sdk.js
|
|
129
|
+
ctx.stream({
|
|
130
|
+
type: 'claude-sdk-message',
|
|
131
|
+
data: message,
|
|
132
|
+
sessionId: capturedSessionId,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { exitCode: 0, sessionId: capturedSessionId };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
ctx.stream({ type: 'claude-error', content: error.message || 'SDK query failed' });
|
|
139
|
+
return { exitCode: 1, sessionId: capturedSessionId };
|
|
140
|
+
} finally {
|
|
141
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
125
144
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
/**
|
|
146
|
+
* SDK-based sub-agent for read-only research tasks.
|
|
147
|
+
* Restricts tools to read-only operations.
|
|
148
|
+
*/
|
|
149
|
+
async function runSDKClaudeTaskQuery(command, options, ctx) {
|
|
150
|
+
const sdkOptions = buildSDKOptions(options);
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
152
|
+
// Restrict to read-only tools
|
|
153
|
+
sdkOptions.allowedTools = ['View', 'Glob', 'Grep', 'LS', 'Read'];
|
|
133
154
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
let queryInstance;
|
|
156
|
+
try {
|
|
157
|
+
queryInstance = query({
|
|
158
|
+
prompt: command || '',
|
|
159
|
+
options: sdkOptions,
|
|
137
160
|
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
ctx.stream({ type: 'claude-error', content: error.message || 'SDK task query failed' });
|
|
163
|
+
return { exitCode: 1 };
|
|
164
|
+
}
|
|
138
165
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
166
|
+
if (ctx.trackProcess) {
|
|
167
|
+
ctx.trackProcess(ctx.requestId, { instance: queryInstance, action: 'claude-task-query' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let capturedSessionId = null;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
for await (const message of queryInstance) {
|
|
174
|
+
if (message.session_id && !capturedSessionId) {
|
|
175
|
+
capturedSessionId = message.session_id;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ctx.stream({
|
|
179
|
+
type: 'claude-sdk-message',
|
|
180
|
+
data: message,
|
|
181
|
+
sessionId: capturedSessionId,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { exitCode: 0, sessionId: capturedSessionId };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
ctx.stream({ type: 'claude-error', content: error.message || 'SDK task query failed' });
|
|
188
|
+
return { exitCode: 1, sessionId: capturedSessionId };
|
|
189
|
+
} finally {
|
|
190
|
+
if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
|
|
191
|
+
}
|
|
144
192
|
}
|
|
145
193
|
|
|
146
194
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
195
|
+
* Simple streaming mode (--print).
|
|
196
|
+
* Just pipes stdout/stderr chunks. Kept for backward compat with relay-client.js.
|
|
149
197
|
*/
|
|
150
|
-
function
|
|
198
|
+
function runSimpleClaudeQuery(command, options, ctx, resolveBinary) {
|
|
151
199
|
return new Promise((resolve) => {
|
|
152
|
-
const args = ['
|
|
153
|
-
args.push('--
|
|
200
|
+
const args = ['--print'];
|
|
201
|
+
if (options?.sessionId) args.push('--continue', options.sessionId);
|
|
154
202
|
|
|
155
203
|
const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
|
|
156
204
|
shell: true,
|
|
@@ -158,26 +206,10 @@ function runClaudeTaskQuery(command, options, ctx, resolveBinary) {
|
|
|
158
206
|
env: process.env,
|
|
159
207
|
});
|
|
160
208
|
|
|
161
|
-
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-
|
|
162
|
-
let taskBuffer = '';
|
|
209
|
+
if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-query' });
|
|
163
210
|
|
|
164
211
|
proc.stdout.on('data', (chunk) => {
|
|
165
|
-
|
|
166
|
-
const lines = taskBuffer.split('\n');
|
|
167
|
-
taskBuffer = lines.pop();
|
|
168
|
-
for (const line of lines) {
|
|
169
|
-
if (!line.trim()) continue;
|
|
170
|
-
try {
|
|
171
|
-
const evt = JSON.parse(line);
|
|
172
|
-
if (evt.type === 'assistant' && evt.message?.content?.length) {
|
|
173
|
-
ctx.stream({ type: 'claude-response', content: evt.message.content[0].text || '' });
|
|
174
|
-
}
|
|
175
|
-
} catch {
|
|
176
|
-
if (line.trim()) {
|
|
177
|
-
ctx.stream({ type: 'claude-response', content: line });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
212
|
+
ctx.stream({ type: 'claude-response', content: chunk.toString() });
|
|
181
213
|
});
|
|
182
214
|
|
|
183
215
|
proc.stderr.on('data', (chunk) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upfynai-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Unified AI coding interface — access AI chat, terminal, file explorer, git, and visual canvas from any browser. Connect your local machine and code from anywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"url": "https://github.com/AnitChaudhry/UpfynAI-Code/issues"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.71",
|
|
61
62
|
"chalk": "^5.3.0",
|
|
62
63
|
"commander": "^12.1.0",
|
|
63
64
|
"express": "^4.21.0",
|
package/src/connect.js
CHANGED
|
@@ -275,6 +275,9 @@ export async function connect(options = {}) {
|
|
|
275
275
|
},
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
+
// Respond to native WebSocket pings from server (Railway proxy keepalive)
|
|
279
|
+
ws.on('ping', () => { try { ws.pong(); } catch { /* ignore */ } });
|
|
280
|
+
|
|
278
281
|
ws.on('open', () => {
|
|
279
282
|
reconnectAttempts = 0;
|
|
280
283
|
console.log(chalk.green(' Connected! Your local machine is now bridged to the server.'));
|
|
@@ -289,8 +292,11 @@ export async function connect(options = {}) {
|
|
|
289
292
|
console.log(chalk.dim(` Default project: ${cwd}\n`));
|
|
290
293
|
|
|
291
294
|
const heartbeat = setInterval(() => {
|
|
292
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
293
|
-
|
|
295
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
296
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
297
|
+
try { ws.ping(); } catch { /* ignore */ }
|
|
298
|
+
}
|
|
299
|
+
}, 20000);
|
|
294
300
|
ws.on('close', () => clearInterval(heartbeat));
|
|
295
301
|
});
|
|
296
302
|
|
|
@@ -312,7 +318,14 @@ export async function connect(options = {}) {
|
|
|
312
318
|
}
|
|
313
319
|
if (data.type === 'relay-abort') {
|
|
314
320
|
const entry = activeProcesses.get(data.requestId);
|
|
315
|
-
if (entry?.
|
|
321
|
+
if (entry?.instance?.interrupt) {
|
|
322
|
+
// SDK query instance — use interrupt()
|
|
323
|
+
entry.instance.interrupt().catch(() => {});
|
|
324
|
+
activeProcesses.delete(data.requestId);
|
|
325
|
+
ws.send(JSON.stringify({ type: 'relay-complete', requestId: data.requestId, exitCode: -1, aborted: true }));
|
|
326
|
+
console.log(chalk.yellow(' [relay] SDK session interrupted by user'));
|
|
327
|
+
} else if (entry?.proc) {
|
|
328
|
+
// Legacy subprocess — use kill()
|
|
316
329
|
entry.proc.kill('SIGTERM');
|
|
317
330
|
activeProcesses.delete(data.requestId);
|
|
318
331
|
ws.send(JSON.stringify({ type: 'relay-complete', requestId: data.requestId, exitCode: -1, aborted: true }));
|
|
@@ -356,6 +369,12 @@ export async function connect(options = {}) {
|
|
|
356
369
|
return;
|
|
357
370
|
}
|
|
358
371
|
if (data.type === 'pong') return;
|
|
372
|
+
if (data.type === 'server-ping') {
|
|
373
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
374
|
+
ws.send(JSON.stringify({ type: 'server-pong' }));
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
359
378
|
if (data.type === 'error') {
|
|
360
379
|
console.error(chalk.red(` Server error: ${data.error}`));
|
|
361
380
|
return;
|