@walkhi/code-relax 0.1.0-beta.51 → 0.1.0-beta.59
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/dist/bin/set-relay-admin-password.mjs +11 -0
- package/dist/build-platform.json +1 -1
- package/dist/shared/release-version.cjs +31 -0
- package/dist/src/dots-room-client.mjs +2 -0
- package/dist/src/file-upload-session.mjs +18 -2
- package/dist/src/lan-socket-server.mjs +81 -9
- package/dist/src/platform/desktop-project-store.mjs +20 -9
- package/dist/src/self-relay/admin-auth.mjs +59 -0
- package/dist/src/self-relay/admin-login-page.mjs +7 -0
- package/dist/src/self-relay/admin-page.mjs +15 -8
- package/dist/src/self-relay/admin-server.mjs +49 -18
- package/dist/src/self-relay/admin-style.mjs +44 -0
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -0
- package/dist/src/self-relay/admin-workstations.mjs +143 -0
- package/dist/src/self-relay/connection-reporting.mjs +38 -0
- package/dist/src/self-relay/connector.mjs +8 -3
- package/dist/src/self-relay/lifecycle.mjs +11 -0
- package/dist/src/self-relay/phone-connections.mjs +55 -0
- package/dist/src/self-relay/rtc-file-bridge.mjs +8 -3
- package/dist/src/self-relay/server.mjs +63 -3
- package/dist/src/server.mjs +18 -5
- package/dist/src/service-update.mjs +6 -3
- package/dist/src/shared-app-server.mjs +17 -0
- package/package.json +4 -4
|
@@ -19,6 +19,8 @@ import { accountConnections } from './account-connections.mjs';
|
|
|
19
19
|
import { usageMeter, usageReport } from './usage-meter.mjs';
|
|
20
20
|
import { relayDiagnostics } from './diagnostics.mjs';
|
|
21
21
|
import { connectionInfo, deviceInfo } from './device-info.mjs';
|
|
22
|
+
import { listWorkstations, workstationDetail, workstationSummary, resolveWorkstationAccounts, resolveWorkstationConnections } from './admin-workstations.mjs';
|
|
23
|
+
import { phoneConnectionEvents, validPhoneConnection } from './phone-connections.mjs';
|
|
22
24
|
|
|
23
25
|
// Always loopback; a TLS reverse proxy supplies the private-trial Internet endpoint.
|
|
24
26
|
export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlMs = 120000,
|
|
@@ -57,6 +59,7 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
57
59
|
const diagnosticKey = usageDiagnosticKeyFile ? fs.readFileSync(usageDiagnosticKeyFile, 'utf8').trim() : '';
|
|
58
60
|
if (diagnosticKey && !/^[A-Za-z0-9_-]{43}$/.test(diagnosticKey)) throw new Error('Invalid usage diagnostic key');
|
|
59
61
|
const store = database?.devices || deviceStore(stateFile);
|
|
62
|
+
const phoneEvents = phoneConnectionEvents(database?.pool);
|
|
60
63
|
const debugAllowlist = readDebugAllowlist(debugAllowlistFile);
|
|
61
64
|
const adminState = database?.admin || relayAdminState(adminStateFile, maxRelaySessions);
|
|
62
65
|
let activeRelaySessions = 0;
|
|
@@ -295,6 +298,21 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
295
298
|
const diagnosticAccess = diagnosticRoute && diagnosticKey && token &&
|
|
296
299
|
timingSafeEqual(Buffer.from(token), Buffer.from(diagnosticKey));
|
|
297
300
|
if (!internalAccess && !diagnosticAccess) { adminReply(response, 404, { error: 'Not found' }); return; }
|
|
301
|
+
if (request.method === 'GET' && target.pathname === '/admin/api/workstations') {
|
|
302
|
+
const workstations = await resolveWorkstationAccounts(listWorkstations(rooms, { details: true }), database?.pool);
|
|
303
|
+
await resolveWorkstationConnections(workstations, phoneEvents);
|
|
304
|
+
adminReply(response, 200, { sampledAt: new Date().toISOString(), workstations: workstations.map(workstationSummary) }); return;
|
|
305
|
+
}
|
|
306
|
+
if (request.method === 'GET' && target.pathname.startsWith('/admin/api/workstations/')) {
|
|
307
|
+
const id = target.pathname.slice('/admin/api/workstations/'.length);
|
|
308
|
+
const room = validId(id) && rooms.get(id);
|
|
309
|
+
if (!room) { adminReply(response, 404, { error: '未找到工作站' }); return; }
|
|
310
|
+
const [workstation] = await resolveWorkstationAccounts([
|
|
311
|
+
workstationDetail(room, debugAllowlist.devices.some(entry => entry.deviceId === room.id))], database?.pool);
|
|
312
|
+
await resolveWorkstationConnections([workstation], phoneEvents);
|
|
313
|
+
workstation.connectionEvents = await phoneEvents.list(room.id);
|
|
314
|
+
adminReply(response, 200, { sampledAt: new Date().toISOString(), workstation }); return;
|
|
315
|
+
}
|
|
298
316
|
if (request.method === 'GET' && target.pathname === '/admin/api/status') {
|
|
299
317
|
const state = adminState.read();
|
|
300
318
|
adminReply(response, 200, { maxRelaySessions: state.maxRelaySessions, activeRelaySessions,
|
|
@@ -402,10 +420,14 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
402
420
|
if (wss.clients.size >= maxConnections || ++admissions > 240) {
|
|
403
421
|
socket.end('HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\n\r\n'); return;
|
|
404
422
|
}
|
|
405
|
-
wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws));
|
|
423
|
+
wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request));
|
|
406
424
|
});
|
|
407
|
-
wss.on('connection', ws => {
|
|
425
|
+
wss.on('connection', (ws, request) => {
|
|
426
|
+
const address = request.headers['x-real-ip'];
|
|
427
|
+
ws.adminInfo = { connectedAt: new Date().toISOString(),
|
|
428
|
+
address: typeof address === 'string' && isIP(address) ? address : request.socket.remoteAddress };
|
|
408
429
|
let role, room, session, clientId, alive = true;
|
|
430
|
+
const phoneEventIds = new Set();
|
|
409
431
|
let accountConnection;
|
|
410
432
|
function invalidateAccountConnection() {
|
|
411
433
|
if (role === 'client' && room?.connections.get(clientId)?.ws === ws) {
|
|
@@ -533,6 +555,7 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
533
555
|
const user = await accounts.current(message.accountToken);
|
|
534
556
|
if (!accountConnection.activate(user.expiresAt)) return;
|
|
535
557
|
userId = user.id;
|
|
558
|
+
ws.adminInfo.account = { id: user.id, email: user.email };
|
|
536
559
|
}
|
|
537
560
|
catch { return fail(4431, 'Account session invalid'); }
|
|
538
561
|
if (ws.readyState !== ws.OPEN) return;
|
|
@@ -591,6 +614,8 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
591
614
|
room.usageTasks?.clear(); room.usageArms?.clear();
|
|
592
615
|
}
|
|
593
616
|
room.host = ws; room.hostName = hostName; room.lanCandidates = lanCandidates;
|
|
617
|
+
room.adminPhoneConnections = undefined;
|
|
618
|
+
room.adminLastOnlineAt = ws.adminInfo.connectedAt; room.adminLastHostName = hostName;
|
|
594
619
|
room.usageTasks ||= new Map(); room.usageArms ||= new Map();
|
|
595
620
|
if (oldHost && oldHost !== ws) room.uploadHost?.close(4409, 'Computer connection replaced');
|
|
596
621
|
room.p2pProbe = message.p2pProbe === 1; room.p2pProbeRepeat = message.p2pProbeRepeat === true;
|
|
@@ -682,7 +707,8 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
682
707
|
if (room.lanCandidates?.length) send(ws, { type: 'lan-candidates', candidates: room.lanCandidates });
|
|
683
708
|
const announcement = adminState.read().announcement;
|
|
684
709
|
if (announcement) send(ws, { type: 'announcement', announcement });
|
|
685
|
-
send(room.host, { type: 'attach', session, clientId,
|
|
710
|
+
send(room.host, { type: 'attach', session, clientId, chat: Boolean(userId),
|
|
711
|
+
p2pProbe: room.connections.get(clientId).p2pProbe ? 1 : 0 }); return;
|
|
686
712
|
}
|
|
687
713
|
if (role === 'device-manager') {
|
|
688
714
|
if (!room.clients.some(item => item.id === clientId)) return fail(4401, 'Device authorization revoked');
|
|
@@ -690,12 +716,44 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
690
716
|
await manageDevices(room, ws, message, clientId); return;
|
|
691
717
|
}
|
|
692
718
|
if (role === 'host') {
|
|
719
|
+
if (['phone-connection', 'phone-connections'].includes(message.type)) {
|
|
720
|
+
if (room.host !== ws) return;
|
|
721
|
+
if (message.type === 'phone-connections') {
|
|
722
|
+
if (!Array.isArray(message.active) || message.active.length > 128 ||
|
|
723
|
+
message.active.some(item => !validPhoneConnection(item))) return fail(1008, 'Invalid phone connection snapshot');
|
|
724
|
+
room.adminPhoneConnections = new Map(message.active.filter(item => room.clients.some(client => client.id === item.clientId))
|
|
725
|
+
.map(item => [item.connectionId, { connectionId: item.connectionId, clientId: item.clientId, mode: item.mode }]));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const event = message.event;
|
|
729
|
+
if (!validPhoneConnection(event)) return fail(1008, 'Invalid phone connection event');
|
|
730
|
+
// Revocation can overtake the workstation's final close event.
|
|
731
|
+
if (!room.clients.some(client => client.id === event.clientId)) {
|
|
732
|
+
send(ws, { type: 'phone-connection-ack', id: event.id }); return;
|
|
733
|
+
}
|
|
734
|
+
try { await phoneEvents.record(room.id, event); }
|
|
735
|
+
catch (error) { log({ event: 'phone-connection-error', message: error.message }); return; }
|
|
736
|
+
if (room.host !== ws) return;
|
|
737
|
+
if (phoneEventIds.has(event.id)) { send(ws, { type: 'phone-connection-ack', id: event.id }); return; }
|
|
738
|
+
phoneEventIds.add(event.id);
|
|
739
|
+
if (phoneEventIds.size > 256) phoneEventIds.delete(phoneEventIds.values().next().value);
|
|
740
|
+
room.adminPhoneConnections ||= new Map();
|
|
741
|
+
if (event.state === 'connected') room.adminPhoneConnections.set(event.connectionId,
|
|
742
|
+
{ connectionId: event.connectionId, clientId: event.clientId, mode: event.mode });
|
|
743
|
+
else room.adminPhoneConnections.delete(event.connectionId);
|
|
744
|
+
send(ws, { type: 'phone-connection-ack', id: event.id }); return;
|
|
745
|
+
}
|
|
693
746
|
if (room.host !== ws) return;
|
|
694
747
|
if (['devices-list', 'device-revoke'].includes(message.type)) { await manageDevices(room, ws, message); return; }
|
|
695
748
|
if (message.type === 'device-connected') {
|
|
696
749
|
const at = new Date(Math.min(Date.parse(message.at), Date.now())).toISOString();
|
|
697
750
|
if (!validId(message.clientId)) return fail(1008, 'Invalid mobile identity');
|
|
698
751
|
await recordConnection(room, message.clientId, at, deviceInfo(message));
|
|
752
|
+
room.adminLanConnections ||= new Map();
|
|
753
|
+
if (room.clients.some(client => client.id === message.clientId) &&
|
|
754
|
+
(!room.adminLanConnections.has(message.clientId) || room.adminLanConnections.get(message.clientId) < at)) {
|
|
755
|
+
room.adminLanConnections.set(message.clientId, at);
|
|
756
|
+
}
|
|
699
757
|
return;
|
|
700
758
|
}
|
|
701
759
|
if (meter && ['usage-arm', 'usage-message', 'usage-cancel', 'usage-turn', 'usage-observer-lost'].includes(message.type)) {
|
|
@@ -874,12 +932,14 @@ export async function startSelfRelay({ port = 0, hostKey = secret(), pairingTtlM
|
|
|
874
932
|
if (diagnostic && message.kind === 'close') connection.diagnosticProbeId = undefined;
|
|
875
933
|
} else if (room.connections.get(clientId)?.ws === ws && message.type === 'p2p-route' && message.direct === 1) {
|
|
876
934
|
releaseRelay(room.connections.get(clientId));
|
|
935
|
+
room.connections.get(clientId).adminRoute = 'direct';
|
|
877
936
|
} else if (room.connections.get(clientId)?.ws === ws && message.type === 'data' && typeof message.payload === 'string') {
|
|
878
937
|
const connection = room.connections.get(clientId);
|
|
879
938
|
if (!connection.userId && !managementPayload(message.payload)) return fail(4431, 'Account login required');
|
|
880
939
|
if (connection.userId && !controlPayload(message.payload) && !admitRelay(connection, room)) {
|
|
881
940
|
ws.close(4428, 'Server busy'); return;
|
|
882
941
|
}
|
|
942
|
+
if (connection.userId && !controlPayload(message.payload)) connection.adminRoute = 'relay';
|
|
883
943
|
send(room.host, { type: 'data', session, payload: message.payload });
|
|
884
944
|
} else fail();
|
|
885
945
|
} catch (error) { log({ event: 'request-error', role, message: 'Relay operation failed' }); ws.close(1011, 'Relay operation failed'); }
|
package/dist/src/server.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
import vm from 'node:vm';
|
|
10
10
|
import sharp from 'sharp';
|
|
11
11
|
import { attachLanSocket } from './lan-socket-server.mjs';
|
|
12
|
+
import { connectionReporting } from './self-relay/connection-reporting.mjs';
|
|
12
13
|
import fileMentions from '../shared/file-mentions.cjs';
|
|
13
14
|
import fileCitations from '../shared/file-citations.cjs';
|
|
14
15
|
import { validateInlineImage } from './message-images.mjs';
|
|
@@ -393,7 +394,10 @@ async function handleRequest(request, response) {
|
|
|
393
394
|
return;
|
|
394
395
|
}
|
|
395
396
|
const localPath = url.searchParams.get('path') || '';
|
|
396
|
-
if (localFileRequest &&
|
|
397
|
+
if (localFileRequest && (/[\x00-\x1f]/.test(localPath)
|
|
398
|
+
|| !(process.platform === 'win32' ? /^[a-z]:[\\/]/i : /^\/(?!\/)/).test(localPath))) {
|
|
399
|
+
throw new RequestError(400, '无效的工作站文件路径。');
|
|
400
|
+
}
|
|
397
401
|
const file = localFileRequest ? describeLocalFile(localPath) : fileCache.get(fileMatch[1]);
|
|
398
402
|
if (!file) throw new RequestError(404, '电脑上的文件不存在或无法读取。');
|
|
399
403
|
if (file.externalIdentity) await dotsImages.authorization(file.externalIdentity);
|
|
@@ -515,11 +519,16 @@ async function handleRequest(request, response) {
|
|
|
515
519
|
|
|
516
520
|
if (request.method === 'GET' && url.pathname === '/api/file-local-copy') {
|
|
517
521
|
const value = url.searchParams.get('source') || '';
|
|
518
|
-
const
|
|
519
|
-
if (
|
|
522
|
+
const match = /^([a-f0-9]{64})(?:\?recovery=([A-Za-z0-9_-]{1,4096}))?$/.exec(value);
|
|
523
|
+
if (!match) throw new RequestError(400, '无效的文件标识。');
|
|
524
|
+
const asset = attachmentCache.get(match[1])
|
|
525
|
+
|| dotsImages.restore(match[1], match[2] || url.searchParams.get('recovery') || '');
|
|
526
|
+
// A missing process cache does not make a local image an external file.
|
|
527
|
+
// The actual image endpoint owns recovery and download authorization.
|
|
528
|
+
if (!asset?.dotsFile) {
|
|
520
529
|
sendJson(response, 200, { external: false, localCopy: null }); return;
|
|
521
530
|
}
|
|
522
|
-
|
|
531
|
+
await dotsImages.authorization(asset.identity);
|
|
523
532
|
const copy = await workstationDownloads.copy(asset.id);
|
|
524
533
|
sendJson(response, 200, { external: true, localCopy: copy ? workstationCopyReference(copy, asset) : null,
|
|
525
534
|
cachedContent: asset.buffer ? { remotePath: asset.id, name: asset.name, size: asset.buffer.length, mimeType: asset.mimeType } : null });
|
|
@@ -760,7 +769,9 @@ async function handleRequest(request, response) {
|
|
|
760
769
|
}
|
|
761
770
|
const { input } = appServerTurnArguments('', body);
|
|
762
771
|
messageSubmissionStarted = true;
|
|
763
|
-
const
|
|
772
|
+
const createRpc = (method, params) => method === 'thread/start'
|
|
773
|
+
? execution.startThread(params) : rpc(method, params);
|
|
774
|
+
const created = await appServerEvents.createThread(createRpc, target, input, body, undefined, bindCreation);
|
|
764
775
|
const prompt = input.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
765
776
|
void generateAndSetThreadTitle({
|
|
766
777
|
request: rpc,
|
|
@@ -1022,6 +1033,8 @@ const lanSocket = attachLanSocket(server, { token: options.token, dispatch: hand
|
|
|
1022
1033
|
uploadsRoot,
|
|
1023
1034
|
authorizeDevice: (clientId, credential) => authorizeLanDevice(stateRoot, clientId, credential),
|
|
1024
1035
|
onDeviceAuthorizationsChanged: listener => onLanAuthorizationsChanged(stateRoot, listener),
|
|
1036
|
+
onPhoneConnected: (...args) => connectionReporting(stateRoot).connected(...args),
|
|
1037
|
+
onPhoneDisconnected: id => connectionReporting(stateRoot).disconnected(id),
|
|
1025
1038
|
onDeviceConnected: (clientId, info) => {
|
|
1026
1039
|
try { recordLanConnection(stateRoot, clientId, info); }
|
|
1027
1040
|
catch (error) { console.error('记录局域网设备连接失败:', error.message); }
|
|
@@ -5,6 +5,7 @@ import { startBackground } from './background-process.mjs';
|
|
|
5
5
|
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
6
6
|
import { runtimePaths } from './platform/runtime-paths.mjs';
|
|
7
7
|
import { desktopHost } from './platform/desktop-host.mjs';
|
|
8
|
+
import releaseVersion from '../shared/release-version.cjs';
|
|
8
9
|
|
|
9
10
|
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
10
11
|
const statusPath = root => path.join(root, 'service-update.json');
|
|
@@ -27,8 +28,10 @@ export async function checkServiceUpdate(root) {
|
|
|
27
28
|
const result = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax/latest', { signal: AbortSignal.timeout(12000) });
|
|
28
29
|
if (!result.ok) throw new Error(`查询桌面服务版本失败:HTTP ${result.status}`);
|
|
29
30
|
const latest = await result.json();
|
|
30
|
-
if (!versionPattern.test(latest.version || '')) throw new Error('npm 返回的版本号无效');
|
|
31
|
-
|
|
31
|
+
if (!versionPattern.test(latest.version || '') || releaseVersion.compare(latest.version, latest.version) !== 0) throw new Error('npm 返回的版本号无效');
|
|
32
|
+
const status = serviceUpdateStatus(root);
|
|
33
|
+
return { ...status, latestVersion: latest.version,
|
|
34
|
+
updateAvailable: releaseVersion.compare(latest.version, status.currentVersion) > 0 };
|
|
32
35
|
}
|
|
33
36
|
let scheduling = false;
|
|
34
37
|
export async function requestServiceUpdate(root, body) {
|
|
@@ -50,7 +53,7 @@ export async function requestServiceUpdate(root, body) {
|
|
|
50
53
|
const latest = await checkServiceUpdate(root);
|
|
51
54
|
if (!latest.supported) throw new Error('当前安装缺少 npm 路径,请先安装支持手机更新的过渡版。');
|
|
52
55
|
if (latest.latestVersion !== body.targetVersion) throw new Error('最新版本已变化,请重新检查后确认。');
|
|
53
|
-
if (latest.
|
|
56
|
+
if (!latest.updateAvailable) throw new Error('没有高于当前安装版本的桌面服务更新。');
|
|
54
57
|
const installed = installation();
|
|
55
58
|
const directory = path.join(root, 'service-updates', body.operationId);
|
|
56
59
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -173,6 +173,23 @@ export class SharedAppServer extends EventEmitter {
|
|
|
173
173
|
return events.readProjects((method, params) => this.request(method, params));
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
startThread(target) {
|
|
177
|
+
return this.request('thread/start', target, result => {
|
|
178
|
+
// thread/start already subscribes this connection. Retain its authoritative
|
|
179
|
+
// empty snapshot before turn/start can emit events; immediately resuming
|
|
180
|
+
// would unnecessarily read history that may not have been persisted yet.
|
|
181
|
+
const thread = result.thread;
|
|
182
|
+
const record = { thread: { ...thread, turns: (thread.turns || []).map(turn => events.compactTurn(turn)) },
|
|
183
|
+
nextCursor: null, sequence: 0, recoverAfter: 0, changes: new Map(), changeBytes: 0,
|
|
184
|
+
tokens: new Map([[randomUUID(), { sequence: 0, expires: Date.now() + syncTokenTtlMs }]]) };
|
|
185
|
+
this.threads.set(thread.id, record);
|
|
186
|
+
this.contexts.set(thread.id, { model: result.model, thinking: result.reasoningEffort,
|
|
187
|
+
serviceTier: result.serviceTier ?? null, cwd: result.cwd, ...events.permissionState(result) });
|
|
188
|
+
this.scheduleCleanup(thread.id, this.slot(thread.id));
|
|
189
|
+
return result;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
176
193
|
async send(args, { assertCurrent = () => {} } = {}) {
|
|
177
194
|
let submitted = false;
|
|
178
195
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkhi/code-relax",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.59",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"tag": "beta"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.
|
|
45
|
-
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.
|
|
46
|
-
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.
|
|
44
|
+
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.59",
|
|
45
|
+
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.59",
|
|
46
|
+
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.59"
|
|
47
47
|
}
|
|
48
48
|
}
|