relay-companion 0.1.25 → 0.1.26

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.
@@ -23,8 +23,12 @@ function isRelayBundle(bundle) {
23
23
  }
24
24
 
25
25
  // Cold-start choice when NEITHER agent is running: pick by what's installed.
26
- // Spec (preserved from the original): prefer Codex if installed, else Claude Code.
27
- function defaultClickHost({ codexInstalled = false, claudeInstalled = false } = {}) {
26
+ // macOS preserves the original preference (Codex, then Claude). Windows is
27
+ // Claude-only for now: the Codex materializer still depends on macOS Codex
28
+ // Desktop paths/app-server behavior, while Claude's claude:// resume handler is
29
+ // registered by Claude Desktop on Windows.
30
+ function defaultClickHost({ codexInstalled = false, claudeInstalled = false, platform = process.platform } = {}) {
31
+ if (platform === "win32") return "claude";
28
32
  if (codexInstalled) return "codex";
29
33
  if (claudeInstalled) return "claude";
30
34
  return "codex";
@@ -50,17 +54,22 @@ function chooseClickHost({
50
54
  codexRunning = false,
51
55
  codexInstalled = false,
52
56
  claudeInstalled = false,
57
+ platform = process.platform,
53
58
  } = {}) {
59
+ const codexSupported = platform !== "win32";
60
+ const effectiveCodexRunning = codexSupported && codexRunning;
61
+ const effectiveCodexInstalled = codexSupported && codexInstalled;
62
+
54
63
  if (frontHost) return frontHost;
55
64
 
56
65
  const capturedHostIsFresh = lastHost && now - lastHostSeenAt < 2500;
57
66
  if ((!bundle || isRelayBundle(bundle)) && capturedHostIsFresh) return lastHost;
58
67
 
59
- if (claudeRunning && !codexRunning) return "claude";
60
- if (codexRunning && !claudeRunning) return "codex";
61
- if (claudeRunning && codexRunning && lastHost) return lastHost;
68
+ if (claudeRunning && !effectiveCodexRunning) return "claude";
69
+ if (effectiveCodexRunning && !claudeRunning) return "codex";
70
+ if (claudeRunning && effectiveCodexRunning && lastHost) return lastHost;
62
71
 
63
- return defaultClickHost({ codexInstalled, claudeInstalled });
72
+ return defaultClickHost({ codexInstalled: effectiveCodexInstalled, claudeInstalled, platform });
64
73
  }
65
74
 
66
75
  module.exports = { hostFromBundle, isRelayBundle, defaultClickHost, chooseClickHost };
package/overlay/main.cjs CHANGED
@@ -24,10 +24,10 @@ const { app, BrowserWindow, Menu, Tray, ipcMain, nativeImage, shell, screen } =
24
24
  const fs = require("node:fs");
25
25
  const os = require("node:os");
26
26
  const path = require("node:path");
27
- const { execFile, spawn, pathToFileURL } = (() => {
27
+ const { execFile, execFileSync, spawn, pathToFileURL } = (() => {
28
28
  const cp = require("node:child_process");
29
29
  const url = require("node:url");
30
- return { execFile: cp.execFile, spawn: cp.spawn, pathToFileURL: url.pathToFileURL };
30
+ return { execFile: cp.execFile, execFileSync: cp.execFileSync, spawn: cp.spawn, pathToFileURL: url.pathToFileURL };
31
31
  })();
32
32
  const { hostFromBundle, chooseClickHost } = require("./host-select.cjs");
33
33
 
@@ -531,6 +531,11 @@ function rememberForegroundHost(host) {
531
531
  function appInstalled(appName) {
532
532
  const name = String(appName || "").replace(/[^A-Za-z0-9 ._-]/g, "");
533
533
  if (!name) return false;
534
+ if (process.platform === "win32" && /^claude$/i.test(name)) {
535
+ if (windowsUriSchemeRegistered("claude")) return true;
536
+ const appDataClaude = process.env.APPDATA && path.join(process.env.APPDATA, "Claude");
537
+ if (appDataClaude && fs.existsSync(appDataClaude)) return true;
538
+ }
534
539
  let candidates = [];
535
540
  if (process.platform === "darwin") {
536
541
  candidates = [
@@ -548,6 +553,23 @@ function appInstalled(appName) {
548
553
  return candidates.some((candidate) => fs.existsSync(candidate));
549
554
  }
550
555
 
556
+ function windowsUriSchemeRegistered(scheme) {
557
+ if (process.platform !== "win32") return false;
558
+ const clean = String(scheme || "").replace(/[^A-Za-z0-9+.-]/g, "");
559
+ if (!clean) return false;
560
+ for (const hive of ["HKCU\\Software\\Classes", "HKCR"]) {
561
+ try {
562
+ const out = execFileSync("reg.exe", ["query", `${hive}\\${clean}\\shell\\open\\command`, "/ve"], {
563
+ encoding: "utf8",
564
+ timeout: 1000,
565
+ windowsHide: true,
566
+ });
567
+ if (String(out || "").trim()) return true;
568
+ } catch {}
569
+ }
570
+ return false;
571
+ }
572
+
551
573
  // Resolve which host a click opens into from the current frontmost bundle, folding
552
574
  // in the polled running-process state (claudeRunning/codexRunning) so a host sitting
553
575
  // on another macOS Space still wins over cold-starting the other. Shared by every
@@ -565,6 +587,7 @@ function resolveClickHost(bundle) {
565
587
  codexRunning,
566
588
  codexInstalled: appInstalled("Codex"),
567
589
  claudeInstalled: appInstalled("Claude"),
590
+ platform: process.platform,
568
591
  });
569
592
  }
570
593
 
@@ -610,11 +633,20 @@ function claudeLogSawResume(sessionId, fromOffset) {
610
633
  // local_<uuid>.json within ~1s of actually focusing it — and keeps doing so
611
634
  // even when main.log has gone dead (observed live: log frozen for over an hour
612
635
  // while resumes worked, which made a log-only check fire false fallbacks).
613
- const CLAUDE_SESSIONS_DIR = path.join(os.homedir(), "Library", "Application Support", "Claude", "claude-code-sessions");
636
+ function claudeDesktopBaseDir() {
637
+ if (process.platform === "win32") {
638
+ return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude");
639
+ }
640
+ return path.join(os.homedir(), "Library", "Application Support", "Claude");
641
+ }
642
+ function claudeDesktopSessionsDir() {
643
+ return process.env.CLAUDE_DESKTOP_SESSIONS_DIR || path.join(claudeDesktopBaseDir(), "claude-code-sessions");
644
+ }
614
645
  function claudeSessionMetaPath(sessionId) {
646
+ const sessionsDir = claudeDesktopSessionsDir();
615
647
  try {
616
- for (const org of fs.readdirSync(CLAUDE_SESSIONS_DIR)) {
617
- const orgDir = path.join(CLAUDE_SESSIONS_DIR, org);
648
+ for (const org of fs.readdirSync(sessionsDir)) {
649
+ const orgDir = path.join(sessionsDir, org);
618
650
  let orgStat;
619
651
  try {
620
652
  orgStat = fs.statSync(orgDir);
@@ -640,6 +672,7 @@ function claudeLastFocusedAt(metaPath) {
640
672
  function openClaudeDeepLinkVerified(url, fallbackUrl, label) {
641
673
  const fire = () =>
642
674
  shell.openExternal(url).catch((error) => console.error("[overlay] openExternal failed:", error && error.message));
675
+ if (process.platform !== "darwin") return fire();
643
676
  const sessionId = claudeSessionIdFromUrl(url);
644
677
  if (!sessionId) return fire();
645
678
  const metaPath = claudeSessionMetaPath(sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Relay companion: connects local coding agents to Relay tasks, approvals, and connector tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -268,16 +268,20 @@ export function importClaudeDesktopSession({ sessionId, activate = process.env.R
268
268
  if (process.env.RELAY_IMPORT_CLAUDE_DESKTOP === "0") {
269
269
  return { attempted: false, reason: "disabled", deepLink };
270
270
  }
271
- if (process.platform !== "darwin") {
271
+ if (process.platform !== "darwin" && process.platform !== "win32") {
272
272
  return { attempted: false, reason: "unsupported-platform", deepLink };
273
273
  }
274
274
 
275
- const args = activate ? [deepLink] : ["-g", deepLink];
276
- const result = spawnSync("open", args, { encoding: "utf8", timeout: 5000 });
275
+ const command = process.platform === "win32" ? "cmd.exe" : "open";
276
+ const args =
277
+ process.platform === "win32"
278
+ ? ["/c", "start", "", deepLink]
279
+ : activate ? [deepLink] : ["-g", deepLink];
280
+ const result = spawnSync(command, args, { encoding: "utf8", timeout: 5000, windowsHide: true });
277
281
  return {
278
282
  attempted: true,
279
283
  deepLink,
280
- command: "open",
284
+ command,
281
285
  args,
282
286
  status: result.status,
283
287
  signal: result.signal,
package/src/host-paths.js CHANGED
@@ -12,20 +12,29 @@ export const DEFAULT_CODEX_HOME = path.join(os.homedir(), ".codex");
12
12
  export const DEFAULT_CODEX_GLOBAL_STATE = path.join(DEFAULT_CODEX_HOME, ".codex-global-state.json");
13
13
  export const DEFAULT_CODEX_STATE_DB = path.join(DEFAULT_CODEX_HOME, "state_5.sqlite");
14
14
  export const DEFAULT_CLAUDE_HOME = path.join(os.homedir(), ".claude");
15
- export const DEFAULT_CLAUDE_DESKTOP_CONFIG = path.join(
16
- os.homedir(),
17
- "Library",
18
- "Application Support",
19
- "Claude",
20
- "claude_desktop_config.json",
21
- );
22
- export const DEFAULT_CLAUDE_DESKTOP_SESSIONS = path.join(
23
- os.homedir(),
24
- "Library",
25
- "Application Support",
26
- "Claude",
27
- "claude-code-sessions",
28
- );
15
+
16
+ function defaultWindowsAppData({ env = process.env, homedir = os.homedir() } = {}) {
17
+ return env.APPDATA || path.win32.join(homedir, "AppData", "Roaming");
18
+ }
19
+
20
+ export function defaultClaudeDesktopBaseDir({ platform = process.platform, env = process.env, homedir = os.homedir() } = {}) {
21
+ if (platform === "win32") return path.win32.join(defaultWindowsAppData({ env, homedir }), "Claude");
22
+ if (platform === "darwin") return path.join(homedir, "Library", "Application Support", "Claude");
23
+ return path.join(env.XDG_CONFIG_HOME || path.join(homedir, ".config"), "Claude");
24
+ }
25
+
26
+ export function defaultClaudeDesktopConfigPath(options = {}) {
27
+ const pathLib = options.platform === "win32" ? path.win32 : path;
28
+ return pathLib.join(defaultClaudeDesktopBaseDir(options), "claude_desktop_config.json");
29
+ }
30
+
31
+ export function defaultClaudeDesktopSessionsDir(options = {}) {
32
+ const pathLib = options.platform === "win32" ? path.win32 : path;
33
+ return pathLib.join(defaultClaudeDesktopBaseDir(options), "claude-code-sessions");
34
+ }
35
+
36
+ export const DEFAULT_CLAUDE_DESKTOP_CONFIG = defaultClaudeDesktopConfigPath();
37
+ export const DEFAULT_CLAUDE_DESKTOP_SESSIONS = defaultClaudeDesktopSessionsDir();
29
38
 
30
39
  // Durable companion store dir. The cloud companion stages packets + state under
31
40
  // RELAY_HOME/RELAY_COMPANION_HOME (see notifications.companionHome()); the engine
@@ -58,10 +67,12 @@ export function claudeProjectsDir() {
58
67
  return process.env.CLAUDE_PROJECTS_DIR || path.join(claudeHome(), "projects");
59
68
  }
60
69
 
61
- export function claudeDesktopConfigPath() {
62
- return process.env.CLAUDE_DESKTOP_CONFIG || DEFAULT_CLAUDE_DESKTOP_CONFIG;
70
+ export function claudeDesktopConfigPath(options = {}) {
71
+ const env = options.env || process.env;
72
+ return env.CLAUDE_DESKTOP_CONFIG || defaultClaudeDesktopConfigPath({ env, ...options });
63
73
  }
64
74
 
65
- export function claudeDesktopSessionsDir() {
66
- return process.env.CLAUDE_DESKTOP_SESSIONS_DIR || DEFAULT_CLAUDE_DESKTOP_SESSIONS;
75
+ export function claudeDesktopSessionsDir(options = {}) {
76
+ const env = options.env || process.env;
77
+ return env.CLAUDE_DESKTOP_SESSIONS_DIR || defaultClaudeDesktopSessionsDir({ env, ...options });
67
78
  }