cicy-desktop 2.1.78 → 2.1.80

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.
Files changed (54) hide show
  1. package/bin/cicy-desktop +7 -7
  2. package/package.json +6 -6
  3. package/src/backends/homepage-preload.js +22 -0
  4. package/src/backends/homepage-react/assets/index-CKpaMBKz.css +1 -0
  5. package/src/backends/homepage-react/assets/index-CSsNZgC5.js +365 -0
  6. package/src/backends/homepage-react/index.html +2 -2
  7. package/src/backends/homepage-window.js +52 -7
  8. package/src/backends/ipc.js +57 -0
  9. package/src/backends/local-teams.js +73 -26
  10. package/src/backends/sidecar-ipc.js +11 -0
  11. package/src/backends/webview-preload.js +5 -3
  12. package/src/backends/window-manager.js +13 -3
  13. package/src/chrome/chrome-launcher.js +5 -4
  14. package/src/chrome/debugger-port-resolver.js +1 -1
  15. package/src/cloud/cloud-client.js +237 -41
  16. package/src/cluster/types.js +0 -5
  17. package/src/extension/inject.js +1 -1
  18. package/src/main.js +282 -88
  19. package/src/master/chrome-config.js +2 -2
  20. package/src/preload-rpc.js +1 -1
  21. package/src/profiles/profile-store.js +321 -0
  22. package/src/profiles/trusted-origins-store.js +95 -0
  23. package/src/server/worker-observability-routes.js +0 -2
  24. package/src/sidecar/cicy-code.js +84 -23
  25. package/src/sidecar/localbin.js +20 -3
  26. package/src/sidecar/native.js +3 -3
  27. package/src/sidecar/version.js +45 -0
  28. package/src/tabbrowser/newtab-protocol.js +54 -0
  29. package/src/tabbrowser/tab-browser.html +151 -0
  30. package/src/tabbrowser/tab-shell-preload.js +28 -0
  31. package/src/tabbrowser/tab-shell.html +227 -0
  32. package/src/tools/account-tools.js +191 -25
  33. package/src/tools/chrome-tools.js +173 -37
  34. package/src/tools/device-tools.js +25 -0
  35. package/src/tools/index.js +2 -0
  36. package/src/tools/tab-browser-tools.js +453 -0
  37. package/src/tools/window-tools.js +64 -7
  38. package/src/utils/brand-host-electron.js +25 -0
  39. package/src/utils/context-menu-options.js +80 -0
  40. package/src/utils/cookie-logins.js +58 -0
  41. package/src/utils/ip-probe.js +50 -0
  42. package/src/utils/rpc-audit.js +53 -0
  43. package/src/utils/rpc-guard.js +189 -0
  44. package/src/utils/window-monitor.js +5 -15
  45. package/src/utils/window-registry.js +210 -0
  46. package/src/utils/window-thumbnails.js +126 -0
  47. package/src/utils/window-utils.js +146 -109
  48. package/workers/render/package-lock.json +6 -6
  49. package/workers/render/src/App.css +36 -2
  50. package/workers/render/src/App.jsx +587 -103
  51. package/src/backends/artifact-ipc.js +0 -142
  52. package/src/backends/homepage-react/assets/index-DE9m6JTn.css +0 -1
  53. package/src/backends/homepage-react/assets/index-DLYMzgf5.js +0 -365
  54. package/src/cluster/artifact-registry.js +0 -61
@@ -29,9 +29,46 @@ const GATEWAY_URL = process.env.CICY_GATEWAY_URL || "https://gateway.cicy-ai.com
29
29
  const GLOBAL_JSON = path.join(os.homedir(), "cicy-ai", "global.json");
30
30
 
31
31
  // The two provider slots the gateway key must land in, per the contract.
32
- const GATEWAY_PROVIDER_KEYS = {
33
- defaultAnthropic: "anthropic",
34
- defaultOpenAi: "openai",
32
+ // Full item templates (主人 spec): cicy-code needs the complete provider
33
+ // entries — protocol, model list, defaultModel — not just a bare key, or the
34
+ // CLIs can't pick a model. apiKey/url are filled in at injection time.
35
+ const GATEWAY_PROVIDER_TEMPLATES = {
36
+ defaultAnthropic: {
37
+ key: "defaultAnthropic",
38
+ name: "CiCyAi",
39
+ protocol: "anthropic",
40
+ defaultModel: "deepseek-v4-pro",
41
+ defaultModels: {},
42
+ modelMapping: {},
43
+ models: [
44
+ "claude-opus-4-8",
45
+ "claude-opus-4-7",
46
+ "claude-opus-4-6",
47
+ "claude-haiku-4-5-20251001",
48
+ "claude-sonnet-4-6",
49
+ "deepseek-v4-pro",
50
+ "deepseek-v4-flash",
51
+ ],
52
+ statusLabel: "Opus 4.8 (1M context)",
53
+ },
54
+ defaultOpenAi: {
55
+ key: "defaultOpenAi",
56
+ name: "CiCyAi",
57
+ protocol: "openai",
58
+ defaultModel: "deepseek-v4-pro",
59
+ modelMapping: {},
60
+ models: ["deepseek-v4-pro", "deepseek-v4-flash", "gpt-5.5"],
61
+ },
62
+ };
63
+
64
+ // Which provider slot each CLI routes to (providers.default in global.json).
65
+ // Filled in only where missing so a user's own routing override survives.
66
+ const GATEWAY_DEFAULT_ROUTING = {
67
+ cicy: "defaultAnthropic",
68
+ claude: "defaultAnthropic",
69
+ codex: "defaultOpenAi",
70
+ opencode: "defaultOpenAi",
71
+ stt: "defaultOpenAi",
35
72
  };
36
73
 
37
74
  // ── token / identity ────────────────────────────────────────────────────────
@@ -47,13 +84,46 @@ function loginToken() {
47
84
  }
48
85
  }
49
86
 
50
- // Stable per-machine UUID. Generated once and persisted in global.json so the
51
- // SAME machine keeps one identity across restarts; a win box and a mac box each
52
- // get their own (the `platform` field reported alongside makes that explicit).
87
+ // A STABLE machine-level id, derived from the OS hardware/install UUID so it
88
+ // survives a wipe of ~/cicy-ai (deviceId lived there before every wipe minted
89
+ // a NEW device in the cloud = zombie devices). Hashed so we never ship the raw
90
+ // machine id, and normalized to UUID shape across platforms. "" if unreadable.
91
+ function machineStableId() {
92
+ try {
93
+ const cp = require("child_process");
94
+ const fs = require("fs");
95
+ let raw = "";
96
+ if (process.platform === "darwin") {
97
+ const out = cp.execSync("ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
98
+ const m = out.match(/IOPlatformUUID"\s*=\s*"([^"]+)"/);
99
+ raw = m ? m[1] : "";
100
+ } else if (process.platform === "win32") {
101
+ const out = cp.execSync('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid', { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
102
+ const m = out.match(/MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]+)/);
103
+ raw = m ? m[1] : "";
104
+ } else {
105
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
106
+ try { raw = fs.readFileSync(p, "utf8").trim(); if (raw) break; } catch {}
107
+ }
108
+ }
109
+ raw = (raw || "").trim();
110
+ if (!raw) return "";
111
+ const h = crypto.createHash("sha256").update("cicy-device:" + raw).digest("hex");
112
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
113
+ } catch {
114
+ return "";
115
+ }
116
+ }
117
+
118
+ // Stable per-machine device id, persisted in global.json so the SAME machine
119
+ // keeps one identity across restarts. Derived from machineStableId() so a wipe
120
+ // of global.json RE-DERIVES the same id (no zombie devices); a random UUID is
121
+ // only a last-resort fallback when the OS machine id is unreadable. A win box
122
+ // and a mac box each get their own (reported `platform` makes that explicit).
53
123
  function getDeviceId() {
54
124
  const c = readGlobalConfig(GLOBAL_JSON);
55
125
  if (c && typeof c.deviceId === "string" && c.deviceId) return c.deviceId;
56
- const id = crypto.randomUUID();
126
+ const id = machineStableId() || crypto.randomUUID();
57
127
  updateGlobalConfig(GLOBAL_JSON, (cfg) => {
58
128
  if (!cfg.deviceId) cfg.deviceId = id;
59
129
  return cfg;
@@ -62,29 +132,105 @@ function getDeviceId() {
62
132
  return readGlobalConfig(GLOBAL_JSON).deviceId || id;
63
133
  }
64
134
 
65
- // Best-effort public IP. Optional in the contract (cloud falls back to the peer
66
- // IP), so a failure here is non-fatal we just send no publicIp.
67
- async function getPublicIp({ timeoutMs = 4000 } = {}) {
68
- const services = [
69
- "https://api.ipify.org?format=json", // { ip }
70
- "https://ipinfo.io/json", // { ip, ... }
71
- ];
72
- for (const url of services) {
135
+ // Detect egress public IP + that IP's geo region. Runs in the Electron MAIN
136
+ // process where global `fetch` goes DIRECTit does NOT use the Electron
137
+ // session proxy (主人令: 出口 IP 探测不能走 proxy). Each request has its own
138
+ // timeout via AbortController; never throws (returns empty/partial on failure).
139
+ async function detectIpGeo({ timeoutMs = 4000 } = {}) {
140
+ const tryFetch = async (url) => {
141
+ const ctrl = new AbortController();
142
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
73
143
  try {
74
- const ctrl = new AbortController();
75
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
76
144
  const r = await fetch(url, { signal: ctrl.signal, cache: "no-store" });
77
- clearTimeout(t);
78
- if (!r.ok) continue;
79
- const j = await r.json();
80
- if (j && typeof j.ip === "string" && j.ip) return j.ip;
145
+ if (!r.ok) return null;
146
+ return await r.json();
81
147
  } catch (_) {
82
- /* try next */
148
+ return null;
149
+ } finally {
150
+ clearTimeout(t);
83
151
  }
152
+ };
153
+ const empty = { country: "", region: "", city: "" };
154
+ // ipinfo.io (https, no token for ip/city/region/country)
155
+ let j = await tryFetch("https://ipinfo.io/json");
156
+ if (j && typeof j.ip === "string" && j.ip) {
157
+ return { publicIp: j.ip, ipRegion: { country: j.country || "", region: j.region || "", city: j.city || "" } };
158
+ }
159
+ // ip-api.com fallback ({ query, country, regionName, city })
160
+ j = await tryFetch("http://ip-api.com/json");
161
+ if (j && typeof j.query === "string" && j.query) {
162
+ return { publicIp: j.query, ipRegion: { country: j.country || "", region: j.regionName || "", city: j.city || "" } };
163
+ }
164
+ // ipify last resort (ip only)
165
+ j = await tryFetch("https://api.ipify.org?format=json");
166
+ if (j && typeof j.ip === "string" && j.ip) {
167
+ return { publicIp: j.ip, ipRegion: { ...empty } };
168
+ }
169
+ return { publicIp: "", ipRegion: { ...empty } };
170
+ }
171
+
172
+ // Back-compat thin wrapper — some callers only want the IP string.
173
+ async function getPublicIp(opts) {
174
+ return (await detectIpGeo(opts)).publicIp;
175
+ }
176
+
177
+ // The persisted deviceInfo block in global.json (single source of truth).
178
+ function readDeviceInfo() {
179
+ const c = readGlobalConfig(GLOBAL_JSON) || {};
180
+ return c.deviceInfo && typeof c.deviceInfo === "object" ? c.deviceInfo : {};
181
+ }
182
+
183
+ // Flatten ipRegion ({country,region,city}) → "US / California / San Jose" for a
184
+ // clean, human-readable cloud device record (matches the SPA's representation).
185
+ // Pass-through if already a string.
186
+ function flattenIpRegion(r) {
187
+ if (!r) return "";
188
+ if (typeof r === "string") return r;
189
+ if (typeof r === "object") {
190
+ return [r.country, r.region, r.city].map((x) => String(x || "").trim()).filter(Boolean).join(" / ");
84
191
  }
85
192
  return "";
86
193
  }
87
194
 
195
+ // Instant, no-network: what the get_device_info RPC + cloud report read.
196
+ function getDeviceInfo() {
197
+ const di = readDeviceInfo();
198
+ return {
199
+ deviceId: getDeviceId(),
200
+ publicIp: di.publicIp || "",
201
+ ipRegion: di.ipRegion && typeof di.ipRegion === "object" ? di.ipRegion : { country: "", region: "", city: "" },
202
+ systemLanguage: di.systemLanguage || "",
203
+ detectedAt: di.detectedAt || "",
204
+ };
205
+ }
206
+
207
+ // Detect (network, no proxy, timeout) + merge systemLanguage (passed from the
208
+ // main process via electronApp.getLocale()) + persist to global.json. Never
209
+ // throws. Returns the persisted deviceInfo (incl. deviceId).
210
+ async function detectAndPersistDeviceInfo({ systemLanguage = "", timeoutMs = 4000 } = {}) {
211
+ let geo = { publicIp: "", ipRegion: { country: "", region: "", city: "" } };
212
+ try {
213
+ geo = await detectIpGeo({ timeoutMs });
214
+ } catch (_) {
215
+ /* non-fatal */
216
+ }
217
+ try {
218
+ updateGlobalConfig(GLOBAL_JSON, (cfg) => {
219
+ const prev = cfg.deviceInfo && typeof cfg.deviceInfo === "object" ? cfg.deviceInfo : {};
220
+ cfg.deviceInfo = {
221
+ publicIp: geo.publicIp || prev.publicIp || "",
222
+ ipRegion: geo.publicIp ? geo.ipRegion : prev.ipRegion || { country: "", region: "", city: "" },
223
+ systemLanguage: String(systemLanguage || "") || prev.systemLanguage || "",
224
+ detectedAt: new Date().toISOString(),
225
+ };
226
+ return cfg;
227
+ });
228
+ } catch (e) {
229
+ log.warn(`[cloud] persist deviceInfo failed: ${e.message}`);
230
+ }
231
+ return getDeviceInfo();
232
+ }
233
+
88
234
  // ── HTTP helper ─────────────────────────────────────────────────────────────
89
235
 
90
236
  async function cloudFetch(endpoint, { method = "GET", body = null } = {}) {
@@ -120,13 +266,21 @@ async function registerDevice() {
120
266
  const token = loginToken();
121
267
  if (!token) return { ok: false, reason: "not_logged_in" };
122
268
  const deviceId = getDeviceId();
123
- const publicIp = await getPublicIp();
269
+ // Prefer the persisted deviceInfo (written by the startup task, which also has
270
+ // the OS locale). If nothing detected yet, detect now (no syslang available here).
271
+ let di = readDeviceInfo();
272
+ if (!di || !di.publicIp) {
273
+ di = await detectAndPersistDeviceInfo({ systemLanguage: (di && di.systemLanguage) || "" });
274
+ }
124
275
  const body = {
125
276
  deviceId,
126
277
  platform: process.platform, // "win32" | "darwin" | "linux"
127
278
  arch: process.arch, // "x64" | "arm64"
128
279
  };
129
- if (publicIp) body.publicIp = publicIp;
280
+ if (di.publicIp) body.publicIp = di.publicIp;
281
+ const region = flattenIpRegion(di.ipRegion);
282
+ if (region) body.ipRegion = region;
283
+ if (di.systemLanguage) body.systemLanguage = di.systemLanguage;
130
284
  const res = await cloudFetch("/api/device/register", { method: "POST", body });
131
285
  if (res.ok) {
132
286
  log.info(`[cloud] device registered deviceId=${deviceId} platform=${body.platform}/${body.arch}`);
@@ -142,13 +296,18 @@ async function registerDevice() {
142
296
  // teamId omitted → cloud creates a new team + key.
143
297
  // teamId given → cloud returns that team's existing key (no rotation).
144
298
  // Returns { ok, teamId, apiKey, gatewayUrl } on success.
145
- async function registerTeam({ teamId = null, title = "" } = {}) {
299
+ async function registerTeam({ teamId = null, title = "", titleVersion = 0 } = {}) {
146
300
  const token = loginToken();
147
301
  if (!token) return { ok: false, reason: "not_logged_in" };
148
302
  const deviceId = getDeviceId();
149
303
  const body = { deviceId };
150
304
  if (teamId != null) body.teamId = teamId;
151
305
  if (title) body.title = title;
306
+ // 服务端权威版本号(w-10032 契约,commit 06698533;旧 titleUpdatedAt 时间戳契约已废)。
307
+ // 带上「最后一次从云端看到的」版本作为 base(没有就 0)。服务端乐观并发裁决:title 有变
308
+ // 且 base>=云端当前版本 → 采用本端 title、版本=当前+1;否则忽略(base 落后=云端在我们
309
+ // 上次同步后改过名 → 云端赢);相同 title 不 bump。客户端不再用自己的时钟。
310
+ body.titleVersion = Number(titleVersion) || 0;
152
311
  const res = await cloudFetch("/api/team/register", { method: "POST", body });
153
312
  if (res.ok && res.json) {
154
313
  return {
@@ -157,6 +316,8 @@ async function registerTeam({ teamId = null, title = "" } = {}) {
157
316
  apiKey: res.json.apiKey,
158
317
  gatewayUrl: res.json.gatewayUrl || GATEWAY_URL,
159
318
  protocols: res.json.protocols || ["anthropic", "openai"],
319
+ title: res.json.title, // cloud's authoritative title
320
+ titleVersion: Number(res.json.titleVersion) || 0, // cloud's authoritative version
160
321
  };
161
322
  }
162
323
  log.warn(`[cloud] team register failed status=${res.status} reason=${res.reason || ""}`);
@@ -182,30 +343,62 @@ async function listTeams({ deviceId = null, kind = null } = {}) {
182
343
 
183
344
  // ── gateway-key injection ─────────────────────────────────────────────────────
184
345
 
185
- // Write the per-team gateway apiKey + url into the team's global.json
186
- // providers.items entries keyed defaultAnthropic / defaultOpenAi. Existing
187
- // entries are updated in place (preserving model lists etc.); missing ones are
188
- // created minimally. `globalJsonPath` defaults to the user-global config, which
189
- // is also the local team's config home on this machine.
346
+ // Write the per-team gateway apiKey + url into the team's global.json:
347
+ // providers.default CLI routing (cicy/claude→anthropic slot, codex/opencode/
348
+ // stt→openai slot) plus full provider items keyed defaultAnthropic /
349
+ // defaultOpenAi (protocol, model list, defaultModel cicy-code can't drive an
350
+ // LLM from a bare key). Existing entries keep user-tuned fields (model lists,
351
+ // routing overrides); only apiKey/url are forced and missing fields filled.
352
+ // No-ops (no write) when everything is already in place, so calling this on
353
+ // every launch is free. `globalJsonPath` defaults to the user-global config,
354
+ // which is also the local team's config home on this machine.
190
355
  function injectGatewayKey(apiKey, gatewayUrl = GATEWAY_URL, globalJsonPath = GLOBAL_JSON) {
191
356
  if (!apiKey) throw new Error("injectGatewayKey: apiKey required");
192
- return updateGlobalConfig(globalJsonPath, (cfg) => {
357
+ // Pre-check (updateGlobalConfig always rewrites the file): skip the write
358
+ // when routing + both items are already exactly in place.
359
+ try {
360
+ const cur = readGlobalConfig(globalJsonPath);
361
+ const p = cur.providers;
362
+ // stt must specifically be on the gateway slot (defaultOpenAi), overriding
363
+ // cicy-code's seeded "cloudflare-ai" — so require the exact value here, not
364
+ // just "any truthy routing", or the pre-check would short-circuit the fix.
365
+ const routed = p && p.default && Object.keys(GATEWAY_DEFAULT_ROUTING).every((cli) => p.default[cli]) && p.default.stt === "defaultOpenAi";
366
+ const itemsOk = p && Array.isArray(p.items) && Object.entries(GATEWAY_PROVIDER_TEMPLATES).every(([key, tpl]) => {
367
+ const it = p.items.find((x) => x && x.key === key);
368
+ return it && it.apiKey === apiKey && it.url === gatewayUrl && Object.keys(tpl).every((f) => it[f] !== undefined);
369
+ });
370
+ if (routed && itemsOk) return { changed: false, config: cur };
371
+ } catch {}
372
+ let changed = false;
373
+ const next = updateGlobalConfig(globalJsonPath, (cfg) => {
193
374
  if (!cfg.providers || typeof cfg.providers !== "object") cfg.providers = {};
194
- if (!Array.isArray(cfg.providers.items)) cfg.providers.items = [];
195
- const items = cfg.providers.items;
196
- for (const [key, protocol] of Object.entries(GATEWAY_PROVIDER_KEYS)) {
197
- let item = items.find((it) => it && it.key === key);
375
+ const p = cfg.providers;
376
+ if (!p.default || typeof p.default !== "object") p.default = {};
377
+ for (const [cli, slot] of Object.entries(GATEWAY_DEFAULT_ROUTING)) {
378
+ if (!p.default[cli]) { p.default[cli] = slot; changed = true; }
379
+ }
380
+ // Force STT onto the gateway slot. cicy-code seeds default.stt="cloudflare-ai"
381
+ // (direct-to-Cloudflare, needs .cf.prod creds), which the only-if-absent loop
382
+ // above never overwrites. Route it through the gateway instead so STT is
383
+ // metered via the team key and doesn't depend on local CF credentials.
384
+ if (p.default.stt !== "defaultOpenAi") { p.default.stt = "defaultOpenAi"; changed = true; }
385
+ if (!Array.isArray(p.items)) p.items = [];
386
+ for (const [key, tpl] of Object.entries(GATEWAY_PROVIDER_TEMPLATES)) {
387
+ let item = p.items.find((it) => it && it.key === key);
198
388
  if (!item) {
199
- item = { key, protocol, name: "CiCyAi", url: gatewayUrl, apiKey };
200
- items.push(item);
201
- } else {
202
- item.apiKey = apiKey;
203
- item.url = gatewayUrl;
204
- if (!item.protocol) item.protocol = protocol;
389
+ p.items.push({ ...tpl, url: gatewayUrl, apiKey });
390
+ changed = true;
391
+ continue;
392
+ }
393
+ if (item.apiKey !== apiKey) { item.apiKey = apiKey; changed = true; }
394
+ if (item.url !== gatewayUrl) { item.url = gatewayUrl; changed = true; }
395
+ for (const [f, v] of Object.entries(tpl)) {
396
+ if (item[f] === undefined) { item[f] = Array.isArray(v) ? [...v] : (v && typeof v === "object" ? { ...v } : v); changed = true; }
205
397
  }
206
398
  }
207
399
  return cfg;
208
400
  });
401
+ return { changed, config: next };
209
402
  }
210
403
 
211
404
  // Convenience: register a team and immediately wire its key into the local
@@ -231,6 +424,9 @@ module.exports = {
231
424
  loginToken,
232
425
  getDeviceId,
233
426
  getPublicIp,
427
+ detectIpGeo,
428
+ getDeviceInfo,
429
+ detectAndPersistDeviceInfo,
234
430
  registerDevice,
235
431
  registerTeam,
236
432
  listTeams,
@@ -18,14 +18,9 @@ function createRuntimeSessionId(workerId, partition, accountIdx) {
18
18
  return `${workerId}:runtime:${partition || `account-${accountIdx}`}`;
19
19
  }
20
20
 
21
- function createArtifactId(workerId, kind, localId) {
22
- return `${workerId}:artifact:${kind}:${localId}`;
23
- }
24
-
25
21
  module.exports = {
26
22
  toIsoString,
27
23
  createWindowRef,
28
24
  createAgentId,
29
25
  createRuntimeSessionId,
30
- createArtifactId,
31
26
  };
@@ -11,7 +11,7 @@ window._g = window._g || {};
11
11
  // ========================================
12
12
  try {
13
13
  const { ipcRenderer } = require('electron');
14
- window.electronRPC = (tool, args) => ipcRenderer.invoke('rpc', tool, args || {});
14
+ window.electronRPC = (tool, args) => ipcRenderer.invoke('rpc:guarded', tool, args || {});
15
15
  window._g.rpc = window.electronRPC;
16
16
  console.log('[RPC] electronRPC ready');
17
17
  } catch(e) {