upfynai-code 2.8.5 → 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/README.md +26 -86
- package/bin/cli.js +2 -2
- package/dist/agents/claude.js +146 -114
- package/package.json +5 -1
- package/src/connect.js +108 -39
package/README.md
CHANGED
|
@@ -2,35 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
> Unified AI coding interface — access AI chat, terminal, file explorer, git, and visual canvas from any browser.
|
|
4
4
|
|
|
5
|
-
**Upfyn-Code** connects your local development machine to a browser-based UI through a secure
|
|
5
|
+
**Upfyn-Code** connects your local development machine to a browser-based UI through a secure, encrypted connection. Write code with AI assistance, run shell commands, manage files, handle git operations, and plan on a visual canvas — all from any device, anywhere.
|
|
6
6
|
|
|
7
|
-
Your source code **never leaves your machine**. All file operations, terminal commands, and git actions execute locally
|
|
7
|
+
Your source code **never leaves your machine**. All file operations, terminal commands, and git actions execute locally.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install -g
|
|
12
|
+
npm install -g upfynai-code
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Or run directly with npx:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npx
|
|
18
|
+
npx upfynai-code
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
**Requirements:** Node.js
|
|
21
|
+
**Requirements:** Node.js 22 or later. Works on Windows, macOS, and Linux.
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
# 1. Authenticate with your Upfyn account
|
|
27
|
-
|
|
27
|
+
uc login
|
|
28
28
|
|
|
29
29
|
# 2. Connect your machine to the web interface
|
|
30
|
-
|
|
30
|
+
uc connect
|
|
31
31
|
|
|
32
32
|
# 3. Open the web UI (opens automatically)
|
|
33
|
-
|
|
33
|
+
uc start
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Once connected, open [cli.upfyn.com](https://cli.upfyn.com) on any device to access your full development environment.
|
|
@@ -39,45 +39,22 @@ Once connected, open [cli.upfyn.com](https://cli.upfyn.com) on any device to acc
|
|
|
39
39
|
|
|
40
40
|
| Command | Description |
|
|
41
41
|
|---------|-------------|
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
42
|
+
| `uc` | Launch Claude Code with Upfyn web UI in background |
|
|
43
|
+
| `uc start` | Start the web UI server |
|
|
44
|
+
| `uc connect` | Connect your local machine to the cloud interface |
|
|
45
|
+
| `uc login` | Authenticate with your Upfyn account |
|
|
46
|
+
| `uc logout` | Clear saved credentials |
|
|
47
|
+
| `uc status` | Show current configuration |
|
|
48
|
+
| `uc config` | View or set configuration |
|
|
49
|
+
| `uc help` | Show all available commands |
|
|
49
50
|
|
|
50
51
|
### Connect Options
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
|
-
|
|
54
|
+
uc connect --key <token>
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
- `--
|
|
57
|
-
- `--key <token>` — Relay token (get from the web UI after signing in)
|
|
58
|
-
|
|
59
|
-
### MCP Integration
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
upfyn-code mcp --server <url> --key <token>
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Outputs the MCP (Model Context Protocol) configuration JSON for use with Claude Desktop, Cursor, or other MCP-compatible tools.
|
|
66
|
-
|
|
67
|
-
**Example output:**
|
|
68
|
-
|
|
69
|
-
```json
|
|
70
|
-
{
|
|
71
|
-
"mcpServers": {
|
|
72
|
-
"upfyn-code": {
|
|
73
|
-
"url": "https://your-server.com/mcp",
|
|
74
|
-
"headers": {
|
|
75
|
-
"Authorization": "Bearer rt_xxxxx"
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
```
|
|
57
|
+
- `--key <token>` — Your connection token (get from the web UI after signing in)
|
|
81
58
|
|
|
82
59
|
## Features
|
|
83
60
|
|
|
@@ -102,55 +79,18 @@ Connect external tools and services via the Model Context Protocol. Use Upfyn-Co
|
|
|
102
79
|
## How It Works
|
|
103
80
|
|
|
104
81
|
1. **Install** the CLI on your development machine
|
|
105
|
-
2. **Authenticate** with `
|
|
106
|
-
3. **Connect** with `
|
|
82
|
+
2. **Authenticate** with `uc login`
|
|
83
|
+
3. **Connect** with `uc connect` — creates a secure, encrypted connection
|
|
107
84
|
4. **Open** [cli.upfyn.com](https://cli.upfyn.com) on any browser (desktop, tablet, or mobile)
|
|
108
85
|
|
|
109
|
-
The CLI
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
┌─────────────┐ WebSocket ┌──────────────┐ Browser ┌──────────────┐
|
|
113
|
-
│ Your Local │ ──────────────▶ │ Upfyn Relay │ ◀────────────── │ Web UI at │
|
|
114
|
-
│ Machine │ (encrypted) │ Server │ │ cli.upfyn.com│
|
|
115
|
-
└─────────────┘ └──────────────┘ └──────────────┘
|
|
116
|
-
```
|
|
86
|
+
The CLI connects your local filesystem, terminal, and git to the web interface. No ports are exposed and no firewall configuration is needed.
|
|
117
87
|
|
|
118
88
|
## Security
|
|
119
89
|
|
|
120
|
-
- All communication is encrypted
|
|
121
|
-
- Source code is
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
- Self-hosting is supported for full data sovereignty
|
|
125
|
-
|
|
126
|
-
## Self-Hosting
|
|
127
|
-
|
|
128
|
-
Upfyn-Code is open source under the GPL-3.0 license. You can deploy the entire stack on your own infrastructure:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
git clone https://github.com/AnitChaudhry/UpfynAI-Code.git
|
|
132
|
-
cd UpfynAI-Code
|
|
133
|
-
|
|
134
|
-
# Install dependencies
|
|
135
|
-
npm install
|
|
136
|
-
|
|
137
|
-
# Start the backend
|
|
138
|
-
cd backend && npm start
|
|
139
|
-
|
|
140
|
-
# Start the frontend
|
|
141
|
-
cd frontend && npm run dev
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
See the [repository](https://github.com/AnitChaudhry/UpfynAI-Code) for full deployment instructions.
|
|
145
|
-
|
|
146
|
-
## Configuration
|
|
147
|
-
|
|
148
|
-
Credentials and config are stored locally at:
|
|
149
|
-
|
|
150
|
-
| Platform | Path |
|
|
151
|
-
|----------|------|
|
|
152
|
-
| macOS / Linux | `~/.config/upfyn-code/` |
|
|
153
|
-
| Windows | `%APPDATA%\upfyn-code\` |
|
|
90
|
+
- All communication is encrypted end-to-end
|
|
91
|
+
- Source code is **never stored** on servers
|
|
92
|
+
- Secure authentication with automatic session management
|
|
93
|
+
- Each user's environment is fully isolated
|
|
154
94
|
|
|
155
95
|
## Links
|
|
156
96
|
|
|
@@ -163,4 +103,4 @@ Credentials and config are stored locally at:
|
|
|
163
103
|
|
|
164
104
|
GPL-3.0 — see [LICENSE](https://github.com/AnitChaudhry/UpfynAI-Code/blob/main/LICENSE) for details.
|
|
165
105
|
|
|
166
|
-
Built by [
|
|
106
|
+
Built by [Upfynai-Code](https://cli.upfyn.com).
|
package/bin/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
@@ -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",
|
|
@@ -67,6 +68,9 @@
|
|
|
67
68
|
"prompts": "^2.4.2",
|
|
68
69
|
"ws": "^8.18.0"
|
|
69
70
|
},
|
|
71
|
+
"optionalDependencies": {
|
|
72
|
+
"node-pty": "^1.1.0-beta34"
|
|
73
|
+
},
|
|
70
74
|
"engines": {
|
|
71
75
|
"node": ">=18.0.0"
|
|
72
76
|
},
|
package/src/connect.js
CHANGED
|
@@ -11,6 +11,10 @@ import { getPersistentShell } from './persistent-shell.js';
|
|
|
11
11
|
import { needsPermission, requestPermission, handlePermissionResponse } from './permissions.js';
|
|
12
12
|
import { playConnectAnimation } from './animation.js';
|
|
13
13
|
|
|
14
|
+
// Optional node-pty for proper terminal emulation (graceful fallback to spawn)
|
|
15
|
+
let pty;
|
|
16
|
+
try { pty = (await import('node-pty')).default; } catch { pty = null; }
|
|
17
|
+
|
|
14
18
|
// Resolve agents: dist/agents/ (npm package) or ../../shared/agents/ (monorepo)
|
|
15
19
|
const __connectDir = dirname(fileURLToPath(import.meta.url));
|
|
16
20
|
const _agentsPath = existsSync(join(__connectDir, '../dist/agents/index.js'))
|
|
@@ -26,15 +30,17 @@ const activeShellSessions = new Map();
|
|
|
26
30
|
|
|
27
31
|
/**
|
|
28
32
|
* Start an interactive shell session on the local machine, relayed to the browser.
|
|
29
|
-
*
|
|
33
|
+
* Uses node-pty for proper terminal emulation when available, falls back to spawn.
|
|
30
34
|
*/
|
|
31
35
|
function handleShellSessionStart(data, ws) {
|
|
32
36
|
const shellSessionId = data.requestId;
|
|
33
37
|
const projectPath = data.projectPath || process.cwd();
|
|
34
38
|
const isWin = process.platform === 'win32';
|
|
35
39
|
const shellType = data.shellType;
|
|
40
|
+
const cols = data.cols || 80;
|
|
41
|
+
const rows = data.rows || 24;
|
|
36
42
|
|
|
37
|
-
console.log(chalk.cyan(` [relay] Starting shell session in ${projectPath}`));
|
|
43
|
+
console.log(chalk.cyan(` [relay] Starting shell session in ${projectPath}${pty ? ' (pty)' : ' (spawn)'}`));
|
|
38
44
|
|
|
39
45
|
let shellCmd, shellArgs;
|
|
40
46
|
const provider = data.provider || 'claude';
|
|
@@ -42,8 +48,8 @@ function handleShellSessionStart(data, ws) {
|
|
|
42
48
|
|
|
43
49
|
function getInteractiveShell(projectDir) {
|
|
44
50
|
if (isWin) {
|
|
45
|
-
if (shellType === 'cmd') return { cmd: 'cmd.exe', args: [
|
|
46
|
-
return { cmd: 'powershell.exe', args: ['-NoExit'
|
|
51
|
+
if (shellType === 'cmd') return { cmd: 'cmd.exe', args: [] };
|
|
52
|
+
return { cmd: 'powershell.exe', args: ['-NoExit'] };
|
|
47
53
|
}
|
|
48
54
|
const sh = shellType || process.env.SHELL || 'bash';
|
|
49
55
|
return { cmd: sh, args: ['--login'] };
|
|
@@ -88,40 +94,68 @@ function handleShellSessionStart(data, ws) {
|
|
|
88
94
|
|
|
89
95
|
ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: '' }));
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
// Use node-pty for proper terminal emulation (resize, isatty, line discipline)
|
|
98
|
+
if (pty) {
|
|
99
|
+
const proc = pty.spawn(shellCmd, shellArgs, {
|
|
100
|
+
name: 'xterm-256color',
|
|
101
|
+
cols,
|
|
102
|
+
rows,
|
|
103
|
+
cwd: projectPath,
|
|
104
|
+
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', FORCE_COLOR: '3' },
|
|
105
|
+
});
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
activeShellSessions.set(shellSessionId, { proc, projectPath, isPty: true });
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
proc.onData((chunk) => {
|
|
110
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
111
|
+
ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: chunk }));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
ws.
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
proc.onExit(({ exitCode }) => {
|
|
116
|
+
activeShellSessions.delete(shellSessionId);
|
|
117
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
118
|
+
ws.send(JSON.stringify({ type: 'relay-shell-exited', shellSessionId, exitCode }));
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.dim(` [relay] Shell session ended (code ${exitCode})`));
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
// Fallback: spawn without PTY (text wrapping may be incorrect)
|
|
124
|
+
const proc = spawn(shellCmd, shellArgs, {
|
|
125
|
+
cwd: projectPath,
|
|
126
|
+
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', FORCE_COLOR: '3', COLUMNS: String(cols), LINES: String(rows) },
|
|
127
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
128
|
+
});
|
|
110
129
|
|
|
111
|
-
|
|
112
|
-
activeShellSessions.delete(shellSessionId);
|
|
113
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
114
|
-
ws.send(JSON.stringify({ type: 'relay-shell-exited', shellSessionId, exitCode: code }));
|
|
115
|
-
}
|
|
116
|
-
console.log(chalk.dim(` [relay] Shell session ended (code ${code})`));
|
|
117
|
-
});
|
|
130
|
+
activeShellSessions.set(shellSessionId, { proc, projectPath, isPty: false });
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
132
|
+
proc.stdout.on('data', (chunk) => {
|
|
133
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
134
|
+
ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: chunk.toString() }));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
proc.stderr.on('data', (chunk) => {
|
|
139
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
140
|
+
ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: chunk.toString() }));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
proc.on('close', (code) => {
|
|
145
|
+
activeShellSessions.delete(shellSessionId);
|
|
146
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
147
|
+
ws.send(JSON.stringify({ type: 'relay-shell-exited', shellSessionId, exitCode: code }));
|
|
148
|
+
}
|
|
149
|
+
console.log(chalk.dim(` [relay] Shell session ended (code ${code})`));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
proc.on('error', (err) => {
|
|
153
|
+
activeShellSessions.delete(shellSessionId);
|
|
154
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
155
|
+
ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: `\r\n\x1b[31mError: ${err.message}\x1b[0m\r\n` }));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
125
159
|
}
|
|
126
160
|
|
|
127
161
|
/**
|
|
@@ -241,6 +275,9 @@ export async function connect(options = {}) {
|
|
|
241
275
|
},
|
|
242
276
|
});
|
|
243
277
|
|
|
278
|
+
// Respond to native WebSocket pings from server (Railway proxy keepalive)
|
|
279
|
+
ws.on('ping', () => { try { ws.pong(); } catch { /* ignore */ } });
|
|
280
|
+
|
|
244
281
|
ws.on('open', () => {
|
|
245
282
|
reconnectAttempts = 0;
|
|
246
283
|
console.log(chalk.green(' Connected! Your local machine is now bridged to the server.'));
|
|
@@ -255,8 +292,11 @@ export async function connect(options = {}) {
|
|
|
255
292
|
console.log(chalk.dim(` Default project: ${cwd}\n`));
|
|
256
293
|
|
|
257
294
|
const heartbeat = setInterval(() => {
|
|
258
|
-
if (ws.readyState === WebSocket.OPEN)
|
|
259
|
-
|
|
295
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
296
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
297
|
+
try { ws.ping(); } catch { /* ignore */ }
|
|
298
|
+
}
|
|
299
|
+
}, 20000);
|
|
260
300
|
ws.on('close', () => clearInterval(heartbeat));
|
|
261
301
|
});
|
|
262
302
|
|
|
@@ -278,7 +318,14 @@ export async function connect(options = {}) {
|
|
|
278
318
|
}
|
|
279
319
|
if (data.type === 'relay-abort') {
|
|
280
320
|
const entry = activeProcesses.get(data.requestId);
|
|
281
|
-
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()
|
|
282
329
|
entry.proc.kill('SIGTERM');
|
|
283
330
|
activeProcesses.delete(data.requestId);
|
|
284
331
|
ws.send(JSON.stringify({ type: 'relay-complete', requestId: data.requestId, exitCode: -1, aborted: true }));
|
|
@@ -292,20 +339,42 @@ export async function connect(options = {}) {
|
|
|
292
339
|
}
|
|
293
340
|
if (data.type === 'relay-shell-input') {
|
|
294
341
|
const session = activeShellSessions.get(data.shellSessionId);
|
|
295
|
-
if (session
|
|
342
|
+
if (session) {
|
|
343
|
+
if (session.isPty) {
|
|
344
|
+
session.proc.write(data.data);
|
|
345
|
+
} else if (session.proc?.stdin?.writable) {
|
|
346
|
+
session.proc.stdin.write(data.data);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (data.type === 'relay-shell-resize') {
|
|
352
|
+
const session = activeShellSessions.get(data.shellSessionId);
|
|
353
|
+
if (session?.isPty && session.proc?.resize) {
|
|
354
|
+
try { session.proc.resize(data.cols || 80, data.rows || 24); } catch { /* ignore resize errors */ }
|
|
355
|
+
}
|
|
296
356
|
return;
|
|
297
357
|
}
|
|
298
|
-
if (data.type === 'relay-shell-resize') return;
|
|
299
358
|
if (data.type === 'relay-shell-kill') {
|
|
300
359
|
const session = activeShellSessions.get(data.shellSessionId);
|
|
301
360
|
if (session?.proc) {
|
|
302
|
-
session.
|
|
361
|
+
if (session.isPty) {
|
|
362
|
+
session.proc.kill();
|
|
363
|
+
} else {
|
|
364
|
+
session.proc.kill('SIGTERM');
|
|
365
|
+
}
|
|
303
366
|
activeShellSessions.delete(data.shellSessionId);
|
|
304
367
|
console.log(chalk.dim(' [relay] Shell session killed'));
|
|
305
368
|
}
|
|
306
369
|
return;
|
|
307
370
|
}
|
|
308
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
|
+
}
|
|
309
378
|
if (data.type === 'error') {
|
|
310
379
|
console.error(chalk.red(` Server error: ${data.error}`));
|
|
311
380
|
return;
|