background-agents 0.1.1 → 1.0.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.
Files changed (90) hide show
  1. package/README.md +45 -364
  2. package/app/assets/icon.png +0 -0
  3. package/app/assets/tray-icon.png +0 -0
  4. package/app/assets/tray-icon.svg +4 -0
  5. package/app/dist/main.js +932 -0
  6. package/app/dist/preload.cjs +70 -0
  7. package/bin/background-agents.js +298 -0
  8. package/lib/ui.js +110 -0
  9. package/package.json +32 -47
  10. package/dist/debug.d.ts +0 -7
  11. package/dist/debug.d.ts.map +0 -1
  12. package/dist/debug.js +0 -19
  13. package/dist/debug.js.map +0 -1
  14. package/dist/factory.d.ts +0 -31
  15. package/dist/factory.d.ts.map +0 -1
  16. package/dist/factory.js +0 -47
  17. package/dist/factory.js.map +0 -1
  18. package/dist/index.d.ts +0 -30
  19. package/dist/index.d.ts.map +0 -1
  20. package/dist/index.js +0 -35
  21. package/dist/index.js.map +0 -1
  22. package/dist/providers/base.d.ts +0 -160
  23. package/dist/providers/base.d.ts.map +0 -1
  24. package/dist/providers/base.js +0 -656
  25. package/dist/providers/base.js.map +0 -1
  26. package/dist/providers/claude.d.ts +0 -14
  27. package/dist/providers/claude.d.ts.map +0 -1
  28. package/dist/providers/claude.js +0 -122
  29. package/dist/providers/claude.js.map +0 -1
  30. package/dist/providers/codex.d.ts +0 -14
  31. package/dist/providers/codex.d.ts.map +0 -1
  32. package/dist/providers/codex.js +0 -160
  33. package/dist/providers/codex.js.map +0 -1
  34. package/dist/providers/gemini.d.ts +0 -16
  35. package/dist/providers/gemini.d.ts.map +0 -1
  36. package/dist/providers/gemini.js +0 -91
  37. package/dist/providers/gemini.js.map +0 -1
  38. package/dist/providers/index.d.ts +0 -6
  39. package/dist/providers/index.d.ts.map +0 -1
  40. package/dist/providers/index.js +0 -6
  41. package/dist/providers/index.js.map +0 -1
  42. package/dist/providers/opencode.d.ts +0 -14
  43. package/dist/providers/opencode.d.ts.map +0 -1
  44. package/dist/providers/opencode.js +0 -100
  45. package/dist/providers/opencode.js.map +0 -1
  46. package/dist/sandbox/daytona-ssh.d.ts +0 -9
  47. package/dist/sandbox/daytona-ssh.d.ts.map +0 -1
  48. package/dist/sandbox/daytona-ssh.js +0 -113
  49. package/dist/sandbox/daytona-ssh.js.map +0 -1
  50. package/dist/sandbox/daytona.d.ts +0 -4
  51. package/dist/sandbox/daytona.d.ts.map +0 -1
  52. package/dist/sandbox/daytona.js +0 -238
  53. package/dist/sandbox/daytona.js.map +0 -1
  54. package/dist/sandbox/index.d.ts +0 -14
  55. package/dist/sandbox/index.d.ts.map +0 -1
  56. package/dist/sandbox/index.js +0 -15
  57. package/dist/sandbox/index.js.map +0 -1
  58. package/dist/session.d.ts +0 -64
  59. package/dist/session.d.ts.map +0 -1
  60. package/dist/session.js +0 -113
  61. package/dist/session.js.map +0 -1
  62. package/dist/types/events.d.ts +0 -114
  63. package/dist/types/events.d.ts.map +0 -1
  64. package/dist/types/events.js +0 -50
  65. package/dist/types/events.js.map +0 -1
  66. package/dist/types/index.d.ts +0 -3
  67. package/dist/types/index.d.ts.map +0 -1
  68. package/dist/types/index.js +0 -3
  69. package/dist/types/index.js.map +0 -1
  70. package/dist/types/provider.d.ts +0 -124
  71. package/dist/types/provider.d.ts.map +0 -1
  72. package/dist/types/provider.js +0 -2
  73. package/dist/types/provider.js.map +0 -1
  74. package/dist/utils/index.d.ts +0 -4
  75. package/dist/utils/index.d.ts.map +0 -1
  76. package/dist/utils/index.js +0 -4
  77. package/dist/utils/index.js.map +0 -1
  78. package/dist/utils/install.d.ts +0 -27
  79. package/dist/utils/install.d.ts.map +0 -1
  80. package/dist/utils/install.js +0 -86
  81. package/dist/utils/install.js.map +0 -1
  82. package/dist/utils/json.d.ts +0 -8
  83. package/dist/utils/json.d.ts.map +0 -1
  84. package/dist/utils/json.js +0 -15
  85. package/dist/utils/json.js.map +0 -1
  86. package/dist/utils/session.d.ts +0 -17
  87. package/dist/utils/session.d.ts.map +0 -1
  88. package/dist/utils/session.js +0 -59
  89. package/dist/utils/session.js.map +0 -1
  90. package/next.config.codeagentsdk.cjs +0 -22
@@ -0,0 +1,932 @@
1
+ // src/main.ts
2
+ import {
3
+ app as app5,
4
+ BrowserWindow as BrowserWindow5,
5
+ ipcMain as ipcMain3,
6
+ Notification as Notification2,
7
+ shell as shell2,
8
+ nativeTheme
9
+ } from "electron";
10
+ import path4 from "path";
11
+ import { fileURLToPath as fileURLToPath2 } from "url";
12
+
13
+ // src/tray.ts
14
+ import { app, Menu, nativeImage, Tray } from "electron";
15
+ import path from "path";
16
+ import { fileURLToPath } from "url";
17
+ var __filename = fileURLToPath(import.meta.url);
18
+ var __dirname = path.dirname(__filename);
19
+ var tray = null;
20
+ function createTray(mainWindow2) {
21
+ const iconPath = path.join(__dirname, "../assets/tray-icon.png");
22
+ let icon;
23
+ try {
24
+ icon = nativeImage.createFromPath(iconPath);
25
+ if (icon.isEmpty()) {
26
+ icon = nativeImage.createEmpty();
27
+ }
28
+ } catch {
29
+ icon = nativeImage.createEmpty();
30
+ }
31
+ if (process.platform === "darwin") {
32
+ icon.setTemplateImage(true);
33
+ }
34
+ tray = new Tray(icon);
35
+ tray.setToolTip("Background Agents");
36
+ const contextMenu = buildContextMenu(mainWindow2, false);
37
+ tray.setContextMenu(contextMenu);
38
+ tray.on("click", () => {
39
+ if (mainWindow2.isVisible() && mainWindow2.isFocused()) {
40
+ mainWindow2.hide();
41
+ } else {
42
+ mainWindow2.show();
43
+ mainWindow2.focus();
44
+ }
45
+ });
46
+ tray.on("double-click", () => {
47
+ mainWindow2.show();
48
+ mainWindow2.focus();
49
+ });
50
+ return tray;
51
+ }
52
+ function buildContextMenu(mainWindow2, isHidden) {
53
+ return Menu.buildFromTemplate([
54
+ {
55
+ label: isHidden ? "Show Background Agents" : "Hide Background Agents",
56
+ click: () => {
57
+ if (isHidden) {
58
+ mainWindow2.show();
59
+ mainWindow2.focus();
60
+ } else {
61
+ mainWindow2.hide();
62
+ }
63
+ }
64
+ },
65
+ { type: "separator" },
66
+ {
67
+ label: "New Chat",
68
+ accelerator: "CmdOrCtrl+N",
69
+ click: () => {
70
+ mainWindow2.show();
71
+ mainWindow2.focus();
72
+ mainWindow2.webContents.send("shortcut", "new-chat");
73
+ }
74
+ },
75
+ {
76
+ label: "Search",
77
+ accelerator: "CmdOrCtrl+K",
78
+ click: () => {
79
+ mainWindow2.show();
80
+ mainWindow2.focus();
81
+ mainWindow2.webContents.send("shortcut", "search");
82
+ }
83
+ },
84
+ { type: "separator" },
85
+ {
86
+ label: "Settings",
87
+ click: () => {
88
+ mainWindow2.show();
89
+ mainWindow2.focus();
90
+ mainWindow2.webContents.send("shortcut", "settings");
91
+ }
92
+ },
93
+ { type: "separator" },
94
+ {
95
+ label: "Quit Background Agents",
96
+ accelerator: "CmdOrCtrl+Q",
97
+ click: () => {
98
+ app.quit();
99
+ }
100
+ }
101
+ ]);
102
+ }
103
+ function updateTrayMenu(mainWindow2, isHidden) {
104
+ if (tray) {
105
+ const contextMenu = buildContextMenu(mainWindow2, isHidden);
106
+ tray.setContextMenu(contextMenu);
107
+ }
108
+ }
109
+
110
+ // src/shortcuts.ts
111
+ import { globalShortcut } from "electron";
112
+ var DEFAULT_SHORTCUTS = {
113
+ "toggle-window": "CmdOrCtrl+Shift+A",
114
+ "new-chat": "CmdOrCtrl+Shift+N",
115
+ search: "CmdOrCtrl+Shift+K"
116
+ };
117
+ var registeredShortcuts = [];
118
+ var mainWindowRef = null;
119
+ function registerShortcuts(mainWindow2) {
120
+ mainWindowRef = mainWindow2;
121
+ const toggleRet = globalShortcut.register(
122
+ DEFAULT_SHORTCUTS["toggle-window"],
123
+ () => {
124
+ if (mainWindow2.isVisible() && mainWindow2.isFocused()) {
125
+ mainWindow2.hide();
126
+ } else {
127
+ mainWindow2.show();
128
+ mainWindow2.focus();
129
+ }
130
+ }
131
+ );
132
+ if (toggleRet) {
133
+ registeredShortcuts.push(DEFAULT_SHORTCUTS["toggle-window"]);
134
+ console.log(
135
+ `Registered global shortcut: ${DEFAULT_SHORTCUTS["toggle-window"]} (toggle window)`
136
+ );
137
+ } else {
138
+ console.warn(
139
+ `Failed to register shortcut: ${DEFAULT_SHORTCUTS["toggle-window"]}`
140
+ );
141
+ }
142
+ const newChatRet = globalShortcut.register(
143
+ DEFAULT_SHORTCUTS["new-chat"],
144
+ () => {
145
+ mainWindow2.show();
146
+ mainWindow2.focus();
147
+ mainWindow2.webContents.send("shortcut", "new-chat");
148
+ }
149
+ );
150
+ if (newChatRet) {
151
+ registeredShortcuts.push(DEFAULT_SHORTCUTS["new-chat"]);
152
+ console.log(
153
+ `Registered global shortcut: ${DEFAULT_SHORTCUTS["new-chat"]} (new chat)`
154
+ );
155
+ } else {
156
+ console.warn(
157
+ `Failed to register shortcut: ${DEFAULT_SHORTCUTS["new-chat"]}`
158
+ );
159
+ }
160
+ const searchRet = globalShortcut.register(DEFAULT_SHORTCUTS["search"], () => {
161
+ mainWindow2.show();
162
+ mainWindow2.focus();
163
+ mainWindow2.webContents.send("shortcut", "search");
164
+ });
165
+ if (searchRet) {
166
+ registeredShortcuts.push(DEFAULT_SHORTCUTS["search"]);
167
+ console.log(
168
+ `Registered global shortcut: ${DEFAULT_SHORTCUTS["search"]} (search)`
169
+ );
170
+ } else {
171
+ console.warn(`Failed to register shortcut: ${DEFAULT_SHORTCUTS["search"]}`);
172
+ }
173
+ }
174
+ function unregisterShortcuts() {
175
+ for (const shortcut of registeredShortcuts) {
176
+ globalShortcut.unregister(shortcut);
177
+ }
178
+ registeredShortcuts = [];
179
+ mainWindowRef = null;
180
+ }
181
+
182
+ // src/deeplinks.ts
183
+ import { app as app2 } from "electron";
184
+ var PROTOCOL = "background-agents";
185
+ function setupDeepLinks(handler) {
186
+ if (process.defaultApp) {
187
+ if (process.argv.length >= 2) {
188
+ app2.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [
189
+ process.argv[1]
190
+ ]);
191
+ }
192
+ } else {
193
+ app2.setAsDefaultProtocolClient(PROTOCOL);
194
+ }
195
+ const deepLinkArg = process.argv.find(
196
+ (arg) => arg.startsWith(`${PROTOCOL}://`)
197
+ );
198
+ if (deepLinkArg) {
199
+ app2.whenReady().then(() => {
200
+ setTimeout(() => handler(deepLinkArg), 100);
201
+ });
202
+ }
203
+ }
204
+
205
+ // src/git-sync.ts
206
+ import { ipcMain, Notification, app as app3, dialog, shell } from "electron";
207
+ import git from "isomorphic-git";
208
+ import http from "isomorphic-git/http/node";
209
+ import path2 from "path";
210
+ import fs from "fs";
211
+ var SETTINGS_FILE = "git-sync-settings.json";
212
+ function defaultRootDirectory() {
213
+ return path2.join(app3.getPath("home"), "Backgrounder");
214
+ }
215
+ function settingsPath() {
216
+ return path2.join(app3.getPath("userData"), SETTINGS_FILE);
217
+ }
218
+ var settings = { rootDirectory: defaultRootDirectory() };
219
+ var opts = null;
220
+ function loadSettings() {
221
+ try {
222
+ const raw = fs.readFileSync(settingsPath(), "utf8");
223
+ const parsed = JSON.parse(raw);
224
+ if (parsed.rootDirectory && typeof parsed.rootDirectory === "string") {
225
+ settings.rootDirectory = parsed.rootDirectory;
226
+ }
227
+ } catch {
228
+ }
229
+ }
230
+ function persistSettings() {
231
+ try {
232
+ fs.mkdirSync(app3.getPath("userData"), { recursive: true });
233
+ fs.writeFileSync(settingsPath(), JSON.stringify(settings, null, 2), "utf8");
234
+ } catch (error) {
235
+ console.error("[git-sync] Failed to persist settings:", error);
236
+ }
237
+ }
238
+ function emitStatus(repo, status, message) {
239
+ opts?.getWindow()?.webContents.send("sync-status", { repo, status, message });
240
+ }
241
+ function emitError(repo, branch, message) {
242
+ opts?.getWindow()?.webContents.send("sync-error", { repo, branch, message });
243
+ new Notification({ title: "Local sync error", body: message }).show();
244
+ }
245
+ var repoQueues = /* @__PURE__ */ new Map();
246
+ function enqueue(repo, task) {
247
+ const prev = repoQueues.get(repo) ?? Promise.resolve();
248
+ const next = prev.then(task, task);
249
+ repoQueues.set(
250
+ repo,
251
+ next.then(
252
+ () => void 0,
253
+ () => void 0
254
+ )
255
+ );
256
+ return next;
257
+ }
258
+ function repoDir(repo) {
259
+ const name = repo.split("/").pop() || repo;
260
+ return path2.join(settings.rootDirectory, name);
261
+ }
262
+ function repoUrl(repo) {
263
+ return `https://github.com/${repo}.git`;
264
+ }
265
+ function isCloned(repo) {
266
+ return fs.existsSync(path2.join(repoDir(repo), ".git"));
267
+ }
268
+ var DivergenceError = class extends Error {
269
+ constructor(branch) {
270
+ super(
271
+ `Your local copy of "${branch}" has uncommitted changes that differ from the cloud. Sync was skipped to avoid overwriting your work.`
272
+ );
273
+ this.branch = branch;
274
+ this.name = "DivergenceError";
275
+ }
276
+ branch;
277
+ };
278
+ async function getToken() {
279
+ if (!opts) throw new Error("git-sync not initialized");
280
+ const win = opts.getWindow();
281
+ if (!win) throw new Error("No window available to fetch token");
282
+ const res = await win.webContents.session.fetch(
283
+ `${opts.backendUrl}/api/auth/github-sync-token`,
284
+ { method: "POST" }
285
+ );
286
+ if (!res.ok) {
287
+ throw new Error(`Could not get GitHub token (HTTP ${res.status})`);
288
+ }
289
+ const data = await res.json();
290
+ if (!data.token) throw new Error("No GitHub token returned");
291
+ return data.token;
292
+ }
293
+ function authCallback(token) {
294
+ return () => ({ username: token, password: "x-oauth-basic" });
295
+ }
296
+ async function isWorkingTreeClean(dir) {
297
+ const matrix = await git.statusMatrix({ fs, dir });
298
+ return !matrix.some(([, head, workdir]) => head === 1 && workdir !== 1);
299
+ }
300
+ async function currentBranch(dir) {
301
+ return await git.currentBranch({ fs, dir, fullname: false }) || void 0;
302
+ }
303
+ function isNotFound(error) {
304
+ return error?.code === "NotFoundError";
305
+ }
306
+ async function remoteOid(dir, branch) {
307
+ try {
308
+ return await git.resolveRef({ fs, dir, ref: `refs/remotes/origin/${branch}` });
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ async function hasLocalBranch(dir, branch) {
314
+ try {
315
+ await git.resolveRef({ fs, dir, ref: `refs/heads/${branch}` });
316
+ return true;
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+ async function reconcileBranch(dir, branch, target, checkout) {
322
+ let local = null;
323
+ try {
324
+ local = await git.resolveRef({ fs, dir, ref: `refs/heads/${branch}` });
325
+ } catch {
326
+ local = null;
327
+ }
328
+ const checkedOut = await currentBranch(dir) === branch;
329
+ const touchesWorkingTree = checkout || checkedOut;
330
+ if (local === target) {
331
+ if (checkout && !checkedOut) await safeCheckout(dir, branch);
332
+ return;
333
+ }
334
+ if (touchesWorkingTree && !await isWorkingTreeClean(dir)) {
335
+ throw new DivergenceError(branch);
336
+ }
337
+ await git.writeRef({ fs, dir, ref: `refs/heads/${branch}`, value: target, force: true });
338
+ if (touchesWorkingTree) {
339
+ await git.checkout({ fs, dir, ref: branch, force: true });
340
+ }
341
+ }
342
+ async function fetchAndReconcile(dir, url, token, branch, checkout) {
343
+ let fetchHead = null;
344
+ try {
345
+ const res = await git.fetch({
346
+ fs,
347
+ http,
348
+ dir,
349
+ url,
350
+ ref: branch,
351
+ singleBranch: true,
352
+ tags: false,
353
+ onAuth: authCallback(token)
354
+ });
355
+ fetchHead = res.fetchHead ?? null;
356
+ } catch (error) {
357
+ if (isNotFound(error)) return false;
358
+ throw error;
359
+ }
360
+ if (!fetchHead) return false;
361
+ await reconcileBranch(dir, branch, fetchHead, checkout);
362
+ return true;
363
+ }
364
+ async function safeCheckout(dir, branch) {
365
+ if (await currentBranch(dir) === branch) return;
366
+ if (!await isWorkingTreeClean(dir)) {
367
+ throw new DivergenceError(branch);
368
+ }
369
+ await git.checkout({ fs, dir, ref: branch, force: true });
370
+ }
371
+ async function doOpenRepoFolder(repo, branches, activeBranch) {
372
+ const dir = repoDir(repo);
373
+ const url = repoUrl(repo);
374
+ fs.mkdirSync(settings.rootDirectory, { recursive: true });
375
+ if (isCloned(repo)) {
376
+ emitStatus(repo, "syncing");
377
+ try {
378
+ if (activeBranch && await hasLocalBranch(dir, activeBranch)) {
379
+ await safeCheckout(dir, activeBranch);
380
+ }
381
+ } catch (error) {
382
+ if (error instanceof DivergenceError) emitError(repo, activeBranch ?? "", error.message);
383
+ else throw error;
384
+ }
385
+ emitStatus(repo, "ready");
386
+ await shell.openPath(dir);
387
+ return;
388
+ }
389
+ const token = await getToken();
390
+ emitStatus(repo, "cloning");
391
+ fs.mkdirSync(dir, { recursive: true });
392
+ await git.clone({ fs, http, dir, url, singleBranch: false, onAuth: authCallback(token) });
393
+ emitStatus(repo, "syncing");
394
+ const wanted = Array.from(new Set(branches.filter(Boolean)));
395
+ for (const branch of wanted) {
396
+ const target = await remoteOid(dir, branch);
397
+ if (!target) continue;
398
+ try {
399
+ await reconcileBranch(dir, branch, target, branch === activeBranch);
400
+ } catch (error) {
401
+ if (error instanceof DivergenceError) {
402
+ emitError(repo, branch, error.message);
403
+ } else {
404
+ console.warn(`[git-sync] Could not update branch "${branch}" for ${repo}:`, error);
405
+ }
406
+ }
407
+ }
408
+ emitStatus(repo, "ready");
409
+ await shell.openPath(dir);
410
+ }
411
+ async function doSetActiveChat(repo, branch) {
412
+ if (!branch || !isCloned(repo)) return;
413
+ const dir = repoDir(repo);
414
+ const url = repoUrl(repo);
415
+ emitStatus(repo, "syncing");
416
+ try {
417
+ if (await hasLocalBranch(dir, branch)) {
418
+ await safeCheckout(dir, branch);
419
+ } else {
420
+ const token = await getToken();
421
+ await fetchAndReconcile(dir, url, token, branch, true);
422
+ }
423
+ emitStatus(repo, "ready");
424
+ } catch (error) {
425
+ handleOpError(repo, branch, error);
426
+ }
427
+ }
428
+ async function doSyncBranch(repo, branch) {
429
+ if (!branch || !isCloned(repo)) return;
430
+ const dir = repoDir(repo);
431
+ const url = repoUrl(repo);
432
+ emitStatus(repo, "syncing");
433
+ try {
434
+ const token = await getToken();
435
+ await fetchAndReconcile(dir, url, token, branch, false);
436
+ emitStatus(repo, "ready");
437
+ } catch (error) {
438
+ handleOpError(repo, branch, error);
439
+ }
440
+ }
441
+ function handleOpError(repo, branch, error) {
442
+ if (error instanceof DivergenceError) {
443
+ emitStatus(repo, "ready");
444
+ emitError(repo, branch, error.message);
445
+ return;
446
+ }
447
+ const message = error instanceof Error ? error.message : "Unknown error";
448
+ emitStatus(repo, "error", message);
449
+ emitError(repo, branch, `Failed to sync ${repo}: ${message}`);
450
+ }
451
+ function setupGitSync(setupOptions) {
452
+ opts = setupOptions;
453
+ loadSettings();
454
+ ipcMain.handle("git-sync:get-settings", () => settings);
455
+ ipcMain.handle(
456
+ "git-sync:set-settings",
457
+ (_event, next) => {
458
+ if (next.rootDirectory && typeof next.rootDirectory === "string") {
459
+ settings.rootDirectory = next.rootDirectory;
460
+ }
461
+ persistSettings();
462
+ return settings;
463
+ }
464
+ );
465
+ ipcMain.handle("git-sync:pick-directory", async () => {
466
+ const win = opts?.getWindow();
467
+ const result = win ? await dialog.showOpenDialog(win, {
468
+ title: "Choose Backgrounder folder",
469
+ defaultPath: settings.rootDirectory,
470
+ properties: ["openDirectory", "createDirectory"]
471
+ }) : await dialog.showOpenDialog({
472
+ properties: ["openDirectory", "createDirectory"]
473
+ });
474
+ if (result.canceled || result.filePaths.length === 0) return null;
475
+ settings.rootDirectory = result.filePaths[0];
476
+ persistSettings();
477
+ return settings.rootDirectory;
478
+ });
479
+ ipcMain.handle("git-sync:get-repo-state", (_event, repo) => {
480
+ return { cloned: isCloned(repo) };
481
+ });
482
+ ipcMain.handle(
483
+ "git-sync:open-repo-folder",
484
+ async (_event, data) => {
485
+ try {
486
+ await enqueue(
487
+ data.repo,
488
+ () => doOpenRepoFolder(data.repo, data.branches ?? [], data.activeBranch ?? null)
489
+ );
490
+ return { success: true };
491
+ } catch (error) {
492
+ const message = error instanceof Error ? error.message : "Unknown error";
493
+ emitStatus(data.repo, "error", message);
494
+ emitError(data.repo, data.activeBranch ?? void 0, message);
495
+ return { success: false, error: message };
496
+ }
497
+ }
498
+ );
499
+ ipcMain.handle(
500
+ "git-sync:set-active-chat",
501
+ async (_event, data) => {
502
+ if (!isCloned(data.repo)) return { success: true };
503
+ await enqueue(data.repo, () => doSetActiveChat(data.repo, data.branch));
504
+ return { success: true };
505
+ }
506
+ );
507
+ ipcMain.handle(
508
+ "git-sync:sync-branch",
509
+ async (_event, data) => {
510
+ if (!isCloned(data.repo)) return { success: true };
511
+ await enqueue(data.repo, () => doSyncBranch(data.repo, data.branch));
512
+ return { success: true };
513
+ }
514
+ );
515
+ }
516
+
517
+ // src/autoupdate.ts
518
+ import { dialog as dialog2 } from "electron";
519
+ import pkg from "electron-updater";
520
+ var { autoUpdater } = pkg;
521
+ function setupAutoUpdater(mainWindow2) {
522
+ autoUpdater.autoDownload = false;
523
+ autoUpdater.autoInstallOnAppQuit = true;
524
+ setTimeout(() => {
525
+ autoUpdater.checkForUpdates().catch((err) => {
526
+ console.log("Auto-update check failed:", err.message);
527
+ });
528
+ }, 1e4);
529
+ setInterval(
530
+ () => {
531
+ autoUpdater.checkForUpdates().catch((err) => {
532
+ console.log("Auto-update check failed:", err.message);
533
+ });
534
+ },
535
+ 60 * 60 * 1e3
536
+ );
537
+ autoUpdater.on("update-available", (info) => {
538
+ dialog2.showMessageBox(mainWindow2, {
539
+ type: "info",
540
+ title: "Update Available",
541
+ message: `A new version (${info.version}) is available. Would you like to download it now?`,
542
+ buttons: ["Download", "Later"],
543
+ defaultId: 0,
544
+ cancelId: 1
545
+ }).then((result) => {
546
+ if (result.response === 0) {
547
+ autoUpdater.downloadUpdate();
548
+ }
549
+ });
550
+ });
551
+ autoUpdater.on("update-not-available", () => {
552
+ console.log("No updates available");
553
+ });
554
+ autoUpdater.on("download-progress", (progress) => {
555
+ mainWindow2.webContents.send("update-progress", {
556
+ percent: progress.percent,
557
+ transferred: progress.transferred,
558
+ total: progress.total
559
+ });
560
+ });
561
+ autoUpdater.on("update-downloaded", (info) => {
562
+ dialog2.showMessageBox(mainWindow2, {
563
+ type: "info",
564
+ title: "Update Ready",
565
+ message: `Version ${info.version} has been downloaded. Restart now to install?`,
566
+ buttons: ["Restart Now", "Later"],
567
+ defaultId: 0,
568
+ cancelId: 1
569
+ }).then((result) => {
570
+ if (result.response === 0) {
571
+ autoUpdater.quitAndInstall(false, true);
572
+ }
573
+ });
574
+ });
575
+ autoUpdater.on("error", (err) => {
576
+ console.error("Auto-updater error:", err);
577
+ });
578
+ }
579
+
580
+ // src/license-detect.ts
581
+ import { ipcMain as ipcMain2, app as app4 } from "electron";
582
+ import fs2 from "fs";
583
+ import path3 from "path";
584
+ import { execSync } from "child_process";
585
+ var settings2 = {
586
+ autoDetectEnabled: true
587
+ };
588
+ var getSettingsPath = () => path3.join(app4.getPath("userData"), "license-settings.json");
589
+ function loadSettings2() {
590
+ try {
591
+ const settingsPath2 = getSettingsPath();
592
+ if (fs2.existsSync(settingsPath2)) {
593
+ const data = fs2.readFileSync(settingsPath2, "utf-8");
594
+ const loaded = JSON.parse(data);
595
+ settings2 = { ...settings2, ...loaded };
596
+ }
597
+ } catch (error) {
598
+ console.error("Failed to load license detection settings:", error);
599
+ }
600
+ }
601
+ function saveSettings() {
602
+ try {
603
+ const settingsPath2 = getSettingsPath();
604
+ fs2.writeFileSync(settingsPath2, JSON.stringify(settings2, null, 2), "utf-8");
605
+ } catch (error) {
606
+ console.error("Failed to save license detection settings:", error);
607
+ }
608
+ }
609
+ function getCredentialsFilePath() {
610
+ const homeDir = app4.getPath("home");
611
+ return path3.join(homeDir, ".claude", ".credentials.json");
612
+ }
613
+ function readFromKeychain() {
614
+ try {
615
+ const output = execSync(
616
+ 'security find-generic-password -s "Claude Code-credentials" -w',
617
+ {
618
+ encoding: "utf-8",
619
+ timeout: 5e3,
620
+ stdio: ["pipe", "pipe", "pipe"]
621
+ // Suppress stderr
622
+ }
623
+ );
624
+ const credentials = output.trim();
625
+ if (credentials) {
626
+ try {
627
+ JSON.parse(credentials);
628
+ return {
629
+ found: true,
630
+ credentials,
631
+ source: "keychain"
632
+ };
633
+ } catch {
634
+ return {
635
+ found: false,
636
+ credentials: null,
637
+ source: null,
638
+ error: "Keychain contains invalid JSON"
639
+ };
640
+ }
641
+ }
642
+ return {
643
+ found: false,
644
+ credentials: null,
645
+ source: null,
646
+ error: "No credentials found in Keychain"
647
+ };
648
+ } catch (error) {
649
+ const message = error instanceof Error ? error.message : "Unknown keychain error";
650
+ if (message.includes("could not be found") || message.includes("SecKeychainSearchCopyNext")) {
651
+ return {
652
+ found: false,
653
+ credentials: null,
654
+ source: null,
655
+ error: "Claude Code credentials not found in Keychain"
656
+ };
657
+ }
658
+ return {
659
+ found: false,
660
+ credentials: null,
661
+ source: null,
662
+ error: `Keychain access error: ${message}`
663
+ };
664
+ }
665
+ }
666
+ function readFromFile() {
667
+ try {
668
+ const credentialsPath = getCredentialsFilePath();
669
+ if (!fs2.existsSync(credentialsPath)) {
670
+ return {
671
+ found: false,
672
+ credentials: null,
673
+ source: null,
674
+ error: `Credentials file not found at ${credentialsPath}`
675
+ };
676
+ }
677
+ const content = fs2.readFileSync(credentialsPath, "utf-8");
678
+ try {
679
+ JSON.parse(content);
680
+ return {
681
+ found: true,
682
+ credentials: content.trim(),
683
+ source: "file"
684
+ };
685
+ } catch {
686
+ return {
687
+ found: false,
688
+ credentials: null,
689
+ source: null,
690
+ error: "Credentials file contains invalid JSON"
691
+ };
692
+ }
693
+ } catch (error) {
694
+ const message = error instanceof Error ? error.message : "Unknown file read error";
695
+ return {
696
+ found: false,
697
+ credentials: null,
698
+ source: null,
699
+ error: `Failed to read credentials file: ${message}`
700
+ };
701
+ }
702
+ }
703
+ function detectClaudeCredentials() {
704
+ if (!settings2.autoDetectEnabled) {
705
+ return {
706
+ found: false,
707
+ credentials: null,
708
+ source: null,
709
+ error: "Auto-detection is disabled"
710
+ };
711
+ }
712
+ switch (process.platform) {
713
+ case "darwin":
714
+ return readFromKeychain();
715
+ case "linux":
716
+ case "win32":
717
+ return readFromFile();
718
+ default:
719
+ return {
720
+ found: false,
721
+ credentials: null,
722
+ source: null,
723
+ error: `Unsupported platform: ${process.platform}`
724
+ };
725
+ }
726
+ }
727
+ function setupLicenseDetect() {
728
+ loadSettings2();
729
+ ipcMain2.handle("get-claude-license-auto-detect", () => {
730
+ return detectClaudeCredentials();
731
+ });
732
+ ipcMain2.handle("get-license-detect-settings", () => {
733
+ return { autoDetectEnabled: settings2.autoDetectEnabled };
734
+ });
735
+ ipcMain2.handle(
736
+ "set-license-detect-settings",
737
+ (_event, newSettings) => {
738
+ settings2 = { ...settings2, ...newSettings };
739
+ saveSettings();
740
+ return true;
741
+ }
742
+ );
743
+ }
744
+
745
+ // src/main.ts
746
+ var __filename2 = fileURLToPath2(import.meta.url);
747
+ var __dirname2 = path4.dirname(__filename2);
748
+ var DEFAULT_BACKEND_URL = app5.isPackaged ? "https://backgrounder.dev" : "http://localhost:4000";
749
+ var BACKEND_URL = process.env.BACKGROUND_AGENTS_URL || DEFAULT_BACKEND_URL;
750
+ var mainWindow = null;
751
+ var isQuitting = false;
752
+ function createWindow() {
753
+ mainWindow = new BrowserWindow5({
754
+ width: 1400,
755
+ height: 900,
756
+ minWidth: 800,
757
+ minHeight: 600,
758
+ title: "Background Agents",
759
+ icon: path4.join(__dirname2, "../assets/icon.png"),
760
+ webPreferences: {
761
+ preload: path4.join(__dirname2, "preload.cjs"),
762
+ contextIsolation: true,
763
+ nodeIntegration: false,
764
+ sandbox: false
765
+ },
766
+ titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
767
+ trafficLightPosition: { x: 15, y: 15 },
768
+ backgroundColor: nativeTheme.shouldUseDarkColors ? "#0a0a0a" : "#ffffff",
769
+ show: false
770
+ });
771
+ mainWindow.loadURL(BACKEND_URL);
772
+ mainWindow.once("ready-to-show", () => {
773
+ mainWindow?.show();
774
+ console.log("background-agents:ready");
775
+ });
776
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
777
+ shell2.openExternal(url);
778
+ return { action: "deny" };
779
+ });
780
+ mainWindow.webContents.on("will-navigate", (event, url) => {
781
+ if (url.includes("/api/auth/signin")) {
782
+ event.preventDefault();
783
+ const baseUrl = new URL(BACKEND_URL);
784
+ shell2.openExternal(`${baseUrl.origin}/auth/electron-start`);
785
+ }
786
+ });
787
+ mainWindow.on("close", (event) => {
788
+ if (!isQuitting) {
789
+ event.preventDefault();
790
+ mainWindow?.hide();
791
+ }
792
+ });
793
+ mainWindow.on("show", () => {
794
+ updateTrayMenu(mainWindow, false);
795
+ });
796
+ mainWindow.on("hide", () => {
797
+ updateTrayMenu(mainWindow, true);
798
+ });
799
+ return mainWindow;
800
+ }
801
+ var gotTheLock = app5.requestSingleInstanceLock();
802
+ if (!gotTheLock) {
803
+ app5.quit();
804
+ } else {
805
+ app5.on("second-instance", (_event, commandLine) => {
806
+ if (mainWindow) {
807
+ if (mainWindow.isMinimized()) mainWindow.restore();
808
+ mainWindow.show();
809
+ mainWindow.focus();
810
+ }
811
+ const deepLink = commandLine.find(
812
+ (arg) => arg.startsWith("background-agents://")
813
+ );
814
+ if (deepLink) {
815
+ handleDeepLink(deepLink);
816
+ }
817
+ });
818
+ }
819
+ async function handleDeepLink(url) {
820
+ if (!mainWindow) return;
821
+ try {
822
+ const parsed = new URL(url);
823
+ const action = parsed.hostname;
824
+ const params = Object.fromEntries(parsed.searchParams);
825
+ if (action === "auth" && params.token) {
826
+ console.log("Auth token received, setting session cookie...");
827
+ try {
828
+ const backendUrl = new URL(BACKEND_URL);
829
+ await mainWindow.webContents.session.cookies.set({
830
+ url: BACKEND_URL,
831
+ name: backendUrl.protocol === "https:" ? "__Secure-next-auth.session-token" : "next-auth.session-token",
832
+ value: params.token,
833
+ httpOnly: true,
834
+ secure: backendUrl.protocol === "https:",
835
+ sameSite: "lax",
836
+ // Expire in 30 days (NextAuth default)
837
+ expirationDate: Math.floor(Date.now() / 1e3) + 30 * 24 * 60 * 60
838
+ });
839
+ console.log("Session cookie set successfully");
840
+ } catch (cookieError) {
841
+ console.error("Failed to set session cookie:", cookieError);
842
+ }
843
+ mainWindow.loadURL(BACKEND_URL);
844
+ mainWindow.show();
845
+ mainWindow.focus();
846
+ return;
847
+ }
848
+ if (action === "auth-callback") {
849
+ console.log("Legacy auth callback received, reloading app...");
850
+ mainWindow.loadURL(BACKEND_URL);
851
+ mainWindow.show();
852
+ mainWindow.focus();
853
+ return;
854
+ }
855
+ mainWindow.webContents.send("deep-link", { action, params });
856
+ mainWindow.show();
857
+ mainWindow.focus();
858
+ } catch (error) {
859
+ console.error("Failed to parse deep link:", error);
860
+ }
861
+ }
862
+ app5.whenReady().then(async () => {
863
+ if (process.platform === "win32") {
864
+ app5.setAppUserModelId("com.background-agents.desktop");
865
+ }
866
+ const window = createWindow();
867
+ createTray(window);
868
+ registerShortcuts(window);
869
+ setupDeepLinks(handleDeepLink);
870
+ setupGitSync({ getWindow: () => mainWindow, backendUrl: BACKEND_URL });
871
+ if (app5.isPackaged) {
872
+ setupAutoUpdater(window);
873
+ }
874
+ setupLicenseDetect();
875
+ app5.on("activate", () => {
876
+ if (BrowserWindow5.getAllWindows().length === 0) {
877
+ createWindow();
878
+ } else {
879
+ mainWindow?.show();
880
+ }
881
+ });
882
+ });
883
+ app5.on("before-quit", () => {
884
+ isQuitting = true;
885
+ });
886
+ app5.on("will-quit", () => {
887
+ unregisterShortcuts();
888
+ });
889
+ app5.on("open-url", (event, url) => {
890
+ event.preventDefault();
891
+ handleDeepLink(url);
892
+ });
893
+ ipcMain3.on(
894
+ "show-notification",
895
+ (_event, { title, body, chatId }) => {
896
+ const notification = new Notification2({
897
+ title,
898
+ body,
899
+ icon: path4.join(__dirname2, "../assets/icon.png")
900
+ });
901
+ notification.on("click", () => {
902
+ mainWindow?.show();
903
+ mainWindow?.focus();
904
+ if (chatId) {
905
+ mainWindow?.webContents.send("navigate-to-chat", chatId);
906
+ }
907
+ });
908
+ notification.show();
909
+ }
910
+ );
911
+ ipcMain3.on("update-badge", (_event, count) => {
912
+ if (process.platform === "darwin") {
913
+ app5.dock?.setBadge(count > 0 ? count.toString() : "");
914
+ }
915
+ });
916
+ ipcMain3.on("toggle-window", () => {
917
+ if (mainWindow?.isVisible() && mainWindow?.isFocused()) {
918
+ mainWindow.hide();
919
+ } else {
920
+ mainWindow?.show();
921
+ mainWindow?.focus();
922
+ }
923
+ });
924
+ ipcMain3.handle("get-auth-token", async () => {
925
+ return null;
926
+ });
927
+ ipcMain3.handle("set-auth-token", async (_event, _token) => {
928
+ return true;
929
+ });
930
+ ipcMain3.on("open-external", (_event, url) => {
931
+ shell2.openExternal(url);
932
+ });