upfynai-code 2.8.6 → 2.9.1

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 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>', 'Relay token (rt_xxx) — get from the web UI')
58
+ .option('--key <token>', 'Connection token — get from Settings > Keys & Connect')
59
59
  .action(async (options) => {
60
60
  await connect(options);
61
61
  });
@@ -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
- * Two modes:
9
- * - stream-json (connect.js) — full structured streaming with session tracking
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 runStructuredClaudeQuery(command, options, ctx, resolveBinary);
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
- const resolveBinary = ctx.resolveBinary || ((name) => name);
34
- return runClaudeTaskQuery(command, options, ctx, resolveBinary);
35
+ return runSDKClaudeTaskQuery(command, options, ctx);
35
36
  },
36
37
  },
37
38
  };
38
39
 
39
40
  /**
40
- * Structured streaming mode (stream-json).
41
- * Parses NDJSON events, computes text deltas, tracks session ID.
41
+ * Build SDK options from relay command options.
42
+ * Maps the relay format to SDK Options type.
42
43
  */
43
- function runStructuredClaudeQuery(command, options, ctx, resolveBinary) {
44
- return new Promise((resolve) => {
45
- const args = ['-p', '--output-format', 'stream-json', '--include-partial-messages'];
46
- if (options?.sessionId) args.push('--continue', options.sessionId);
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
- const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
49
- shell: true,
50
- cwd: options?.projectPath || os.homedir(),
51
- env: process.env,
52
- });
51
+ if (options.sessionId) {
52
+ sdkOptions.resume = options.sessionId;
53
+ }
53
54
 
54
- if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-query' });
55
+ if (options.model) {
56
+ sdkOptions.model = options.model;
57
+ }
55
58
 
56
- let stdoutBuffer = '';
57
- let capturedSessionId = null;
58
- let lastTextLength = 0;
59
+ if (options.maxBudgetUsd) {
60
+ sdkOptions.maxBudgetUsd = options.maxBudgetUsd;
61
+ }
59
62
 
60
- proc.stdout.on('data', (chunk) => {
61
- stdoutBuffer += chunk.toString();
62
- const lines = stdoutBuffer.split('\n');
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
- proc.stderr.on('data', (chunk) => {
94
- ctx.stream({ type: 'claude-error', content: chunk.toString() });
95
- });
67
+ if (options.allowedTools && options.allowedTools.length > 0) {
68
+ sdkOptions.allowedTools = options.allowedTools;
69
+ }
96
70
 
97
- proc.on('close', (code) => {
98
- if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
99
- resolve({ exitCode: code, sessionId: capturedSessionId });
100
- });
71
+ if (options.disallowedTools && options.disallowedTools.length > 0) {
72
+ sdkOptions.disallowedTools = options.disallowedTools;
73
+ }
101
74
 
102
- proc.on('error', () => {
103
- if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
104
- resolve({ exitCode: 1, sessionId: capturedSessionId });
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
- * Simple streaming mode (--print).
111
- * Just pipes stdout/stderr chunks.
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 runSimpleClaudeQuery(command, options, ctx, resolveBinary) {
114
- return new Promise((resolve) => {
115
- const args = ['--print'];
116
- if (options?.sessionId) args.push('--continue', options.sessionId);
117
-
118
- const proc = spawn(resolveBinary('claude'), [...args, command || ''], {
119
- shell: true,
120
- cwd: options?.projectPath || os.homedir(),
121
- env: process.env,
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
- if (ctx.trackProcess) ctx.trackProcess(ctx.requestId, { proc, action: 'claude-query' });
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
- proc.stdout.on('data', (chunk) => {
127
- ctx.stream({ type: 'claude-response', content: chunk.toString() });
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
- proc.stderr.on('data', (chunk) => {
131
- ctx.stream({ type: 'claude-error', content: chunk.toString() });
132
- });
152
+ // Restrict to read-only tools
153
+ sdkOptions.allowedTools = ['View', 'Glob', 'Grep', 'LS', 'Read'];
133
154
 
134
- proc.on('close', (code) => {
135
- if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
136
- resolve({ exitCode: code });
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
- proc.on('error', () => {
140
- if (ctx.untrackProcess) ctx.untrackProcess(ctx.requestId);
141
- resolve({ exitCode: 1 });
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
- * Sub-agent for read-only research tasks.
148
- * Uses --allowedTools to restrict to read-only operations.
195
+ * Simple streaming mode (--print).
196
+ * Just pipes stdout/stderr chunks. Kept for backward compat with relay-client.js.
149
197
  */
150
- function runClaudeTaskQuery(command, options, ctx, resolveBinary) {
198
+ function runSimpleClaudeQuery(command, options, ctx, resolveBinary) {
151
199
  return new Promise((resolve) => {
152
- const args = ['-p', '--output-format', 'stream-json', '--include-partial-messages'];
153
- args.push('--allowedTools', 'View,Glob,Grep,LS,Read');
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-task-query' });
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
- taskBuffer += chunk.toString();
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.8.6",
3
+ "version": "2.9.1",
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) ws.send(JSON.stringify({ type: 'ping' }));
293
- }, 30000);
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?.proc) {
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;