@anusornneal/chat-relay 0.6.0 → 0.7.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/README.md +320 -230
- package/agent/desktop-manager.mjs +183 -0
- package/agent/file-manager.mjs +369 -265
- package/agent/local-agent.mjs +228 -157
- package/agent/terminal-manager.mjs +507 -229
- package/agent/windows-desktop.ps1 +288 -0
- package/cli/config.mjs +114 -113
- package/cli/device-login.mjs +170 -168
- package/cli/index.mjs +201 -145
- package/cli/remote.mjs +94 -88
- package/package.json +75 -65
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory=$true)][string]$Operation,
|
|
3
|
+
[Parameter(Mandatory=$true)][string]$InputBase64
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = "Stop"
|
|
7
|
+
$ProgressPreference = "SilentlyContinue"
|
|
8
|
+
|
|
9
|
+
function Write-Result($Value) {
|
|
10
|
+
$Value | ConvertTo-Json -Compress -Depth 8
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function Fail($Code) {
|
|
14
|
+
Write-Result ([pscustomobject]@{ ok = $false; error = $Code })
|
|
15
|
+
exit 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
$inputJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($InputBase64))
|
|
20
|
+
$payload = if ($inputJson) { $inputJson | ConvertFrom-Json } else { [pscustomobject]@{} }
|
|
21
|
+
|
|
22
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
+
Add-Type -AssemblyName System.Drawing
|
|
24
|
+
Add-Type @"
|
|
25
|
+
using System;
|
|
26
|
+
using System.Runtime.InteropServices;
|
|
27
|
+
|
|
28
|
+
public static class ChatRelayNative {
|
|
29
|
+
public const uint INPUT_MOUSE = 0;
|
|
30
|
+
public const uint INPUT_KEYBOARD = 1;
|
|
31
|
+
public const uint KEYEVENTF_KEYUP = 0x0002;
|
|
32
|
+
public const uint KEYEVENTF_UNICODE = 0x0004;
|
|
33
|
+
|
|
34
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
35
|
+
public struct MOUSEINPUT {
|
|
36
|
+
public int dx;
|
|
37
|
+
public int dy;
|
|
38
|
+
public uint mouseData;
|
|
39
|
+
public uint dwFlags;
|
|
40
|
+
public uint time;
|
|
41
|
+
public UIntPtr dwExtraInfo;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
45
|
+
public struct KEYBDINPUT {
|
|
46
|
+
public ushort wVk;
|
|
47
|
+
public ushort wScan;
|
|
48
|
+
public uint dwFlags;
|
|
49
|
+
public uint time;
|
|
50
|
+
public UIntPtr dwExtraInfo;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
54
|
+
public struct INPUTUNION {
|
|
55
|
+
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
56
|
+
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
60
|
+
public struct INPUT {
|
|
61
|
+
public uint type;
|
|
62
|
+
public INPUTUNION U;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[DllImport("user32.dll")] public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
|
66
|
+
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
|
|
67
|
+
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
68
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
69
|
+
|
|
70
|
+
public static bool MouseButton(uint downFlag, uint upFlag) {
|
|
71
|
+
var inputs = new INPUT[2];
|
|
72
|
+
inputs[0].type = INPUT_MOUSE;
|
|
73
|
+
inputs[0].U.mi.dwFlags = downFlag;
|
|
74
|
+
inputs[1].type = INPUT_MOUSE;
|
|
75
|
+
inputs[1].U.mi.dwFlags = upFlag;
|
|
76
|
+
return SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) == 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public static bool Key(ushort virtualKey, bool down) {
|
|
80
|
+
var input = new INPUT[1];
|
|
81
|
+
input[0].type = INPUT_KEYBOARD;
|
|
82
|
+
input[0].U.ki.wVk = virtualKey;
|
|
83
|
+
input[0].U.ki.dwFlags = down ? 0u : KEYEVENTF_KEYUP;
|
|
84
|
+
return SendInput(1, input, Marshal.SizeOf(typeof(INPUT))) == 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public static bool UnicodeText(string text) {
|
|
88
|
+
foreach (char c in text) {
|
|
89
|
+
var inputs = new INPUT[2];
|
|
90
|
+
inputs[0].type = INPUT_KEYBOARD;
|
|
91
|
+
inputs[0].U.ki.wScan = c;
|
|
92
|
+
inputs[0].U.ki.dwFlags = KEYEVENTF_UNICODE;
|
|
93
|
+
inputs[1].type = INPUT_KEYBOARD;
|
|
94
|
+
inputs[1].U.ki.wScan = c;
|
|
95
|
+
inputs[1].U.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
|
|
96
|
+
if (SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT))) != 2) return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
"@
|
|
102
|
+
|
|
103
|
+
[ChatRelayNative]::SetProcessDPIAware() | Out-Null
|
|
104
|
+
if (-not [System.Windows.Forms.SystemInformation]::UserInteractive) {
|
|
105
|
+
Fail "session_unavailable"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ($Operation -eq "screenshot") {
|
|
109
|
+
$screens = @([System.Windows.Forms.Screen]::AllScreens)
|
|
110
|
+
if ($screens.Count -eq 0) { Fail "session_unavailable" }
|
|
111
|
+
|
|
112
|
+
$monitor = if ($null -ne $payload.monitor) { $payload.monitor } else { "primary" }
|
|
113
|
+
$screen = $null
|
|
114
|
+
$monitorIndex = -1
|
|
115
|
+
|
|
116
|
+
if ([string]$monitor -eq "primary") {
|
|
117
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
118
|
+
for ($i = 0; $i -lt $screens.Count; $i++) { if ($screens[$i].Primary) { $monitorIndex = $i; break } }
|
|
119
|
+
} elseif ([string]$monitor -eq "secondary") {
|
|
120
|
+
for ($i = 0; $i -lt $screens.Count; $i++) {
|
|
121
|
+
if (-not $screens[$i].Primary) { $screen = $screens[$i]; $monitorIndex = $i; break }
|
|
122
|
+
}
|
|
123
|
+
if ($null -eq $screen) { Fail "monitor_not_found" }
|
|
124
|
+
} else {
|
|
125
|
+
$parsedIndex = 0
|
|
126
|
+
if (-not [int]::TryParse([string]$monitor, [ref]$parsedIndex)) { Fail "invalid_monitor" }
|
|
127
|
+
if ($parsedIndex -lt 0 -or $parsedIndex -ge $screens.Count) { Fail "monitor_not_found" }
|
|
128
|
+
$screen = $screens[$parsedIndex]
|
|
129
|
+
$monitorIndex = $parsedIndex
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ($null -eq $screen) { Fail "session_unavailable" }
|
|
133
|
+
$bounds = $screen.Bounds
|
|
134
|
+
if ($bounds.Width -le 0 -or $bounds.Height -le 0) { Fail "session_unavailable" }
|
|
135
|
+
|
|
136
|
+
$source = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
|
137
|
+
$graphics = [System.Drawing.Graphics]::FromImage($source)
|
|
138
|
+
try {
|
|
139
|
+
$graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size)
|
|
140
|
+
} finally {
|
|
141
|
+
$graphics.Dispose()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
|
|
145
|
+
Where-Object { $_.MimeType -eq "image/jpeg" } |
|
|
146
|
+
Select-Object -First 1
|
|
147
|
+
if ($null -eq $codec) {
|
|
148
|
+
$source.Dispose()
|
|
149
|
+
Fail "capture_failed"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
$maxBytes = 32768
|
|
153
|
+
$targetWidth = [Math]::Min($bounds.Width, 960)
|
|
154
|
+
$scale = $targetWidth / [double]$bounds.Width
|
|
155
|
+
$targetHeight = [Math]::Max(1, [int][Math]::Round($bounds.Height * $scale))
|
|
156
|
+
$qualities = @(70L, 55L, 42L, 32L, 24L)
|
|
157
|
+
$bytes = $null
|
|
158
|
+
$finalWidth = $targetWidth
|
|
159
|
+
$finalHeight = $targetHeight
|
|
160
|
+
|
|
161
|
+
for ($sizeAttempt = 0; $sizeAttempt -lt 4 -and $null -eq $bytes; $sizeAttempt++) {
|
|
162
|
+
$resized = New-Object System.Drawing.Bitmap $targetWidth, $targetHeight
|
|
163
|
+
$draw = [System.Drawing.Graphics]::FromImage($resized)
|
|
164
|
+
try {
|
|
165
|
+
$draw.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
166
|
+
$draw.DrawImage($source, 0, 0, $targetWidth, $targetHeight)
|
|
167
|
+
} finally {
|
|
168
|
+
$draw.Dispose()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
foreach ($quality in $qualities) {
|
|
172
|
+
$stream = New-Object System.IO.MemoryStream
|
|
173
|
+
$parameters = New-Object System.Drawing.Imaging.EncoderParameters 1
|
|
174
|
+
$parameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter(
|
|
175
|
+
[System.Drawing.Imaging.Encoder]::Quality,
|
|
176
|
+
$quality
|
|
177
|
+
)
|
|
178
|
+
try {
|
|
179
|
+
$resized.Save($stream, $codec, $parameters)
|
|
180
|
+
if ($stream.Length -le $maxBytes) {
|
|
181
|
+
$bytes = $stream.ToArray()
|
|
182
|
+
$finalWidth = $targetWidth
|
|
183
|
+
$finalHeight = $targetHeight
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
$parameters.Dispose()
|
|
188
|
+
$stream.Dispose()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
$resized.Dispose()
|
|
193
|
+
if ($null -eq $bytes) {
|
|
194
|
+
$targetWidth = [Math]::Max(320, [int][Math]::Floor($targetWidth * 0.78))
|
|
195
|
+
$targetHeight = [Math]::Max(180, [int][Math]::Round($bounds.Height * ($targetWidth / [double]$bounds.Width)))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
$source.Dispose()
|
|
200
|
+
if ($null -eq $bytes) { Fail "screenshot_too_large" }
|
|
201
|
+
|
|
202
|
+
Write-Result ([pscustomobject]@{
|
|
203
|
+
ok = $true
|
|
204
|
+
mimeType = "image/jpeg"
|
|
205
|
+
data = [Convert]::ToBase64String($bytes)
|
|
206
|
+
width = $finalWidth
|
|
207
|
+
height = $finalHeight
|
|
208
|
+
desktopOriginX = $bounds.Left
|
|
209
|
+
desktopOriginY = $bounds.Top
|
|
210
|
+
desktopWidth = $bounds.Width
|
|
211
|
+
desktopHeight = $bounds.Height
|
|
212
|
+
scaleX = $bounds.Width / [double]$finalWidth
|
|
213
|
+
scaleY = $bounds.Height / [double]$finalHeight
|
|
214
|
+
monitorIndex = $monitorIndex
|
|
215
|
+
isPrimary = [bool]$screen.Primary
|
|
216
|
+
deviceName = [string]$screen.DeviceName
|
|
217
|
+
byteLength = $bytes.Length
|
|
218
|
+
})
|
|
219
|
+
exit 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if ([ChatRelayNative]::GetForegroundWindow() -eq [IntPtr]::Zero) {
|
|
223
|
+
Fail "session_unavailable"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ($Operation -eq "mouse_click") {
|
|
227
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
228
|
+
if ($null -eq $screen) { Fail "session_unavailable" }
|
|
229
|
+
$bounds = $screen.Bounds
|
|
230
|
+
$x = [int]$payload.x
|
|
231
|
+
$y = [int]$payload.y
|
|
232
|
+
if ($x -lt $bounds.Left -or $x -ge $bounds.Right -or $y -lt $bounds.Top -or $y -ge $bounds.Bottom) {
|
|
233
|
+
Fail "invalid_coordinates"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
switch ([string]$payload.button) {
|
|
237
|
+
"left" { $down = 0x0002; $up = 0x0004 }
|
|
238
|
+
"right" { $down = 0x0008; $up = 0x0010 }
|
|
239
|
+
"middle" { $down = 0x0020; $up = 0x0040 }
|
|
240
|
+
default { Fail "invalid_button" }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (-not [ChatRelayNative]::SetCursorPos($x, $y)) { Fail "input_failed" }
|
|
244
|
+
$clicks = [int]$payload.clicks
|
|
245
|
+
for ($i = 0; $i -lt $clicks; $i++) {
|
|
246
|
+
if (-not [ChatRelayNative]::MouseButton($down, $up)) { Fail "input_failed" }
|
|
247
|
+
if ($i + 1 -lt $clicks) { Start-Sleep -Milliseconds 100 }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
Write-Result ([pscustomobject]@{ ok = $true; action = "mouse_click" })
|
|
251
|
+
exit 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if ($Operation -eq "keyboard_input") {
|
|
255
|
+
if ($null -ne $payload.text) {
|
|
256
|
+
if (-not [ChatRelayNative]::UnicodeText([string]$payload.text)) { Fail "input_failed" }
|
|
257
|
+
} else {
|
|
258
|
+
$mods = New-Object System.Collections.Generic.List[uint16]
|
|
259
|
+
if ($payload.ctrl) { $mods.Add(0x11) }
|
|
260
|
+
if ($payload.alt) { $mods.Add(0x12) }
|
|
261
|
+
if ($payload.shift) { $mods.Add(0x10) }
|
|
262
|
+
if ($payload.win) { $mods.Add(0x5B) }
|
|
263
|
+
|
|
264
|
+
foreach ($vk in $mods) {
|
|
265
|
+
if (-not [ChatRelayNative]::Key($vk, $true)) { Fail "input_failed" }
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
$keyCode = [uint16]$payload.keyCode
|
|
269
|
+
if (-not [ChatRelayNative]::Key($keyCode, $true)) { Fail "input_failed" }
|
|
270
|
+
if (-not [ChatRelayNative]::Key($keyCode, $false)) { Fail "input_failed" }
|
|
271
|
+
} finally {
|
|
272
|
+
for ($i = $mods.Count - 1; $i -ge 0; $i--) {
|
|
273
|
+
[ChatRelayNative]::Key($mods[$i], $false) | Out-Null
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
Write-Result ([pscustomobject]@{ ok = $true; action = "keyboard_input" })
|
|
279
|
+
exit 0
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
Fail "unknown_desktop_operation"
|
|
283
|
+
} catch {
|
|
284
|
+
$message = [string]$_.Exception.Message
|
|
285
|
+
if ($message -match "session_unavailable") { Fail "session_unavailable" }
|
|
286
|
+
if ($Operation -eq "screenshot") { Fail "capture_failed" }
|
|
287
|
+
Fail "input_failed"
|
|
288
|
+
}
|
package/cli/config.mjs
CHANGED
|
@@ -1,113 +1,114 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export function configDir() {
|
|
6
|
-
if (process.env.CHAT_RELAY_HOME) return path.resolve(process.env.CHAT_RELAY_HOME);
|
|
7
|
-
if (process.platform === "win32") {
|
|
8
|
-
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
9
|
-
return path.join(base, "chat-relay");
|
|
10
|
-
}
|
|
11
|
-
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
12
|
-
return path.join(base, "chat-relay");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function configPath() {
|
|
16
|
-
return path.join(configDir(), "config.json");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function identityPath() {
|
|
20
|
-
return path.join(configDir(), "identity.json");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function loadIdentity() {
|
|
24
|
-
const file = identityPath();
|
|
25
|
-
if (!fs.existsSync(file)) return null;
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function saveIdentity(identity) {
|
|
34
|
-
const dir = configDir();
|
|
35
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
-
const file = identityPath();
|
|
37
|
-
fs.writeFileSync(file, JSON.stringify(identity, null, 2) + os.EOL, {
|
|
38
|
-
encoding: "utf8",
|
|
39
|
-
mode: 0o600,
|
|
40
|
-
});
|
|
41
|
-
try { fs.chmodSync(file, 0o600); } catch {}
|
|
42
|
-
return file;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseEnvFile(file) {
|
|
46
|
-
const result = {};
|
|
47
|
-
for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
48
|
-
const line = raw.trim();
|
|
49
|
-
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
50
|
-
const index = line.indexOf("=");
|
|
51
|
-
result[line.slice(0, index)] = line.slice(index + 1);
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function migrateLegacyConfig() {
|
|
57
|
-
const legacyPath = path.resolve(".dev.vars");
|
|
58
|
-
if (!fs.existsSync(legacyPath)) return null;
|
|
59
|
-
const legacy = parseEnvFile(legacyPath);
|
|
60
|
-
if (!legacy.RELAY_URL || !legacy.CALLER_TOKEN || !legacy.AGENT_TOKEN) return null;
|
|
61
|
-
|
|
62
|
-
const config = {
|
|
63
|
-
relayUrl: legacy.RELAY_URL.replace(/\/$/, ""),
|
|
64
|
-
user: { id: "owner", name: "Owner", login: null },
|
|
65
|
-
userToken: legacy.CALLER_TOKEN,
|
|
66
|
-
agentId: legacy.AGENT_ID || "default",
|
|
67
|
-
agentName: legacy.AGENT_NAME || "Primary PC",
|
|
68
|
-
agentToken: legacy.AGENT_TOKEN,
|
|
69
|
-
allowedRoots: legacy.ALLOWED_ROOTS || defaultAllowedRoot(),
|
|
70
|
-
terminalEnabled: legacy.TERMINAL_ENABLED !== "0",
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function configDir() {
|
|
6
|
+
if (process.env.CHAT_RELAY_HOME) return path.resolve(process.env.CHAT_RELAY_HOME);
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
9
|
+
return path.join(base, "chat-relay");
|
|
10
|
+
}
|
|
11
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
12
|
+
return path.join(base, "chat-relay");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function configPath() {
|
|
16
|
+
return path.join(configDir(), "config.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function identityPath() {
|
|
20
|
+
return path.join(configDir(), "identity.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadIdentity() {
|
|
24
|
+
const file = identityPath();
|
|
25
|
+
if (!fs.existsSync(file)) return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveIdentity(identity) {
|
|
34
|
+
const dir = configDir();
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
const file = identityPath();
|
|
37
|
+
fs.writeFileSync(file, JSON.stringify(identity, null, 2) + os.EOL, {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
});
|
|
41
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
42
|
+
return file;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseEnvFile(file) {
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
48
|
+
const line = raw.trim();
|
|
49
|
+
if (!line || line.startsWith("#") || !line.includes("=")) continue;
|
|
50
|
+
const index = line.indexOf("=");
|
|
51
|
+
result[line.slice(0, index)] = line.slice(index + 1);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function migrateLegacyConfig() {
|
|
57
|
+
const legacyPath = path.resolve(".dev.vars");
|
|
58
|
+
if (!fs.existsSync(legacyPath)) return null;
|
|
59
|
+
const legacy = parseEnvFile(legacyPath);
|
|
60
|
+
if (!legacy.RELAY_URL || !legacy.CALLER_TOKEN || !legacy.AGENT_TOKEN) return null;
|
|
61
|
+
|
|
62
|
+
const config = {
|
|
63
|
+
relayUrl: legacy.RELAY_URL.replace(/\/$/, ""),
|
|
64
|
+
user: { id: "owner", name: "Owner", login: null },
|
|
65
|
+
userToken: legacy.CALLER_TOKEN,
|
|
66
|
+
agentId: legacy.AGENT_ID || "default",
|
|
67
|
+
agentName: legacy.AGENT_NAME || "Primary PC",
|
|
68
|
+
agentToken: legacy.AGENT_TOKEN,
|
|
69
|
+
allowedRoots: legacy.ALLOWED_ROOTS || defaultAllowedRoot(),
|
|
70
|
+
terminalEnabled: legacy.TERMINAL_ENABLED !== "0",
|
|
71
|
+
desktopEnabled: legacy.DESKTOP_ENABLED === "1",
|
|
72
|
+
migratedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
saveConfig(config);
|
|
75
|
+
saveIdentity({
|
|
76
|
+
agentId: config.agentId,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function loadConfig() {
|
|
84
|
+
const file = configPath();
|
|
85
|
+
if (!fs.existsSync(file)) return migrateLegacyConfig();
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(`Invalid Chat Relay config: ${file}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function saveConfig(config) {
|
|
94
|
+
const dir = configDir();
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
const file = configPath();
|
|
97
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2) + os.EOL, {
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
mode: 0o600,
|
|
100
|
+
});
|
|
101
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
102
|
+
return file;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function clearConfig() {
|
|
106
|
+
const file = configPath();
|
|
107
|
+
if (fs.existsSync(file)) fs.rmSync(file, { force: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function defaultAllowedRoot() {
|
|
111
|
+
const projects = path.join(os.homedir(), "Projects");
|
|
112
|
+
if (fs.existsSync(projects)) return projects;
|
|
113
|
+
return os.homedir();
|
|
114
|
+
}
|