offsite 0.0.11 → 0.0.13

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 (91) hide show
  1. package/README.md +15 -15
  2. package/apps/server/dist/main.mjs +298 -143
  3. package/apps/server/dist/main.mjs.map +1 -1
  4. package/apps/server/drizzle/20261017120143_device_tokens/migration.sql +15 -0
  5. package/apps/server/drizzle/20261017120143_device_tokens/snapshot.json +305 -0
  6. package/apps/web/dist/apple-touch-icon.png +0 -0
  7. package/apps/web/dist/assets/{angular-html-DTNmgvth.js → angular-html-CtL9u31h.js} +1 -1
  8. package/apps/web/dist/assets/{angular-ts-DOOLa99A.js → angular-ts-D4IFBcAh.js} +1 -1
  9. package/apps/web/dist/assets/{apl-CSWmNqJI.js → apl-CqnJhScR.js} +1 -1
  10. package/apps/web/dist/assets/{astro-CK7We5bo.js → astro-Dn1_avL_.js} +1 -1
  11. package/apps/web/dist/assets/{blade-B9Q94PMc.js → blade-BKSa6Bs3.js} +1 -1
  12. package/apps/web/dist/assets/{c-D05zRIsd.js → c-C8sbMt_8.js} +1 -1
  13. package/apps/web/dist/assets/{chapel-CJbjuVPR.js → chapel-BWxXvR2-.js} +1 -1
  14. package/apps/web/dist/assets/{cobol-CE0VhU2R.js → cobol-DNoVp--S.js} +1 -1
  15. package/apps/web/dist/assets/{coffee-C0i-RL39.js → coffee-m1P3e2U1.js} +1 -1
  16. package/apps/web/dist/assets/{cpp-CBJhfD_V.js → cpp-CDvshXRJ.js} +1 -1
  17. package/apps/web/dist/assets/{crystal-DRdgVTW1.js → crystal-C4b65AQe.js} +1 -1
  18. package/apps/web/dist/assets/{css-DgMip4qW.js → css-CqQV9eEE.js} +1 -1
  19. package/apps/web/dist/assets/{edge-BCmeW_3H.js → edge-CEdbZQ1U.js} +1 -1
  20. package/apps/web/dist/assets/{elixir-BiRSxKnc.js → elixir-CYDX6OgA.js} +1 -1
  21. package/apps/web/dist/assets/{elm-zBiqCp9W.js → elm-10RuU89D.js} +1 -1
  22. package/apps/web/dist/assets/{erb-1QviMyT9.js → erb-BiMfBUaM.js} +1 -1
  23. package/apps/web/dist/assets/{git-rebase-TjFhp_Gn.js → git-rebase-D4Se-3SO.js} +1 -1
  24. package/apps/web/dist/assets/{glimmer-js-dThwC_hZ.js → glimmer-js-sKOVTqrH.js} +1 -1
  25. package/apps/web/dist/assets/{glimmer-ts-cCThNW17.js → glimmer-ts-DWMGXcHA.js} +1 -1
  26. package/apps/web/dist/assets/{glsl-BxmyIQYG.js → glsl-6ltM2JGZ.js} +1 -1
  27. package/apps/web/dist/assets/{graphql-DygyczJj.js → graphql-DwyBbitO.js} +1 -1
  28. package/apps/web/dist/assets/{hack-Bl1_oIjY.js → hack-DKDbxv34.js} +1 -1
  29. package/apps/web/dist/assets/{haml-CeOBocB0.js → haml-Bsfivw5f.js} +1 -1
  30. package/apps/web/dist/assets/{handlebars-Bw1KMTCv.js → handlebars-B3HW0h8N.js} +1 -1
  31. package/apps/web/dist/assets/{html-BaNo1IFo.js → html-D9EhGmAq.js} +1 -1
  32. package/apps/web/dist/assets/{html-derivative-f4LhlsLO.js → html-derivative-Cs1eIi3k.js} +1 -1
  33. package/apps/web/dist/assets/{http-Be2FOF-U.js → http-BG4vxnc2.js} +1 -1
  34. package/apps/web/dist/assets/{hurl-BrtPQQhS.js → hurl-OIF0UEfX.js} +1 -1
  35. package/apps/web/dist/assets/index-CEx2Hw3M.js +1203 -0
  36. package/apps/web/dist/assets/{java-Bum5bcia.js → java-2iLSH1xc.js} +1 -1
  37. package/apps/web/dist/assets/{javascript-C5oYhZKk.js → javascript-Di7CdqTh.js} +1 -1
  38. package/apps/web/dist/assets/{jinja-CNXoe8pn.js → jinja-2mjIAN_z.js} +1 -1
  39. package/apps/web/dist/assets/{jison-g1hzxo4W.js → jison-Ciw1rqtP.js} +1 -1
  40. package/apps/web/dist/assets/{json-CiAAuc5j.js → json-D7Dp9EFU.js} +1 -1
  41. package/apps/web/dist/assets/{jsx-Z0awY4ip.js → jsx-WEelr3xJ.js} +1 -1
  42. package/apps/web/dist/assets/{julia-DX3JjwcM.js → julia-DL3m48KZ.js} +1 -1
  43. package/apps/web/dist/assets/{just-DS6eCX0g.js → just-B-5HaJEz.js} +1 -1
  44. package/apps/web/dist/assets/{latex-CaBic_pJ.js → latex-DKmfapvy.js} +1 -1
  45. package/apps/web/dist/assets/{liquid-DMaOBf3v.js → liquid-Bqxp18sH.js} +1 -1
  46. package/apps/web/dist/assets/{lua-DeLz5pfA.js → lua-BSLGBVbN.js} +1 -1
  47. package/apps/web/dist/assets/{marko-DqlzpgnR.js → marko-yLzCBGNR.js} +1 -1
  48. package/apps/web/dist/assets/{mdc-CRcSVpSn.js → mdc-DSGSIXIk.js} +1 -1
  49. package/apps/web/dist/assets/{nginx-BvszHPa-.js → nginx-BF1hRuVH.js} +1 -1
  50. package/apps/web/dist/assets/{nim-B19-Hebo.js → nim-kAH67IJv.js} +1 -1
  51. package/apps/web/dist/assets/{org-Dx7yMTgG.js → org-DCuW6Ito.js} +1 -1
  52. package/apps/web/dist/assets/{perl-C-dISCJW.js → perl-ChYkJruC.js} +1 -1
  53. package/apps/web/dist/assets/{php-BHBxrwsl.js → php-Dy0o5CHL.js} +1 -1
  54. package/apps/web/dist/assets/{pug-4a3x4vRs.js → pug-C-vFGcyR.js} +1 -1
  55. package/apps/web/dist/assets/{qml-CGOhzFqi.js → qml-Bbhsm3bz.js} +1 -1
  56. package/apps/web/dist/assets/{r-8jr0rtQL.js → r-D6AOSU_n.js} +1 -1
  57. package/apps/web/dist/assets/{razor-BDrURPIA.js → razor-CsDTm1QE.js} +1 -1
  58. package/apps/web/dist/assets/{regexp-Cg-MfP18.js → regexp-Cxw4Sy2X.js} +1 -1
  59. package/apps/web/dist/assets/{rst-0OrP55QZ.js → rst-BdEa7eTr.js} +1 -1
  60. package/apps/web/dist/assets/{ruby-dAApWLhj.js → ruby-CrX6uYJq.js} +1 -1
  61. package/apps/web/dist/assets/{sas-3_hKTjL9.js → sas-Dq1DiCgf.js} +1 -1
  62. package/apps/web/dist/assets/{scss-D6ollEQv.js → scss-Cq-_Yezx.js} +1 -1
  63. package/apps/web/dist/assets/{shellscript-V3H5Rwnn.js → shellscript-N1TdW_Vo.js} +1 -1
  64. package/apps/web/dist/assets/{shellsession-TxydKEoU.js → shellsession-DGy-5gMa.js} +1 -1
  65. package/apps/web/dist/assets/{soy-t3rHc1tL.js → soy-BZmv_HHk.js} +1 -1
  66. package/apps/web/dist/assets/{sql-B-NkeM8R.js → sql-B0TfXN6P.js} +1 -1
  67. package/apps/web/dist/assets/{stata-BW6neeVL.js → stata-D0cL92SE.js} +1 -1
  68. package/apps/web/dist/assets/{surrealql-CRBKdx6K.js → surrealql-B9RWrsWf.js} +1 -1
  69. package/apps/web/dist/assets/{svelte-DTTmtyn-.js → svelte-Erfh9F_0.js} +1 -1
  70. package/apps/web/dist/assets/{templ-CCBYQttY.js → templ-C1hdk1C3.js} +1 -1
  71. package/apps/web/dist/assets/{tex-D9XxqR5N.js → tex-B6y5Z0Oe.js} +1 -1
  72. package/apps/web/dist/assets/{ts-tags-CI4pWHnF.js → ts-tags-B8Q3bBJx.js} +1 -1
  73. package/apps/web/dist/assets/{tsx-DlfK3Vuf.js → tsx-CA2zlxVX.js} +1 -1
  74. package/apps/web/dist/assets/{twig-D-C2s1f2.js → twig-xr1-p8qn.js} +1 -1
  75. package/apps/web/dist/assets/{typescript-BnqYTwqM.js → typescript-Bdd7ljtG.js} +1 -1
  76. package/apps/web/dist/assets/{typst-DdTyavvE.js → typst-BGU6c2s1.js} +1 -1
  77. package/apps/web/dist/assets/{vue-zbXTTw4n.js → vue-CxnbGcMK.js} +1 -1
  78. package/apps/web/dist/assets/{vue-html-CIXNcZez.js → vue-html-D3pkBH4b.js} +1 -1
  79. package/apps/web/dist/assets/{vue-vine-Czo_W6u3.js → vue-vine-0adNns8e.js} +1 -1
  80. package/apps/web/dist/assets/{xml-DWv3jkXo.js → xml-Bf5yulh9.js} +1 -1
  81. package/apps/web/dist/assets/{xsl-BiggF1J3.js → xsl-DnGCH2C6.js} +1 -1
  82. package/apps/web/dist/assets/{yaml-D9oC3m1d.js → yaml-ALFPjwWA.js} +1 -1
  83. package/apps/web/dist/icon-192.png +0 -0
  84. package/apps/web/dist/icon-512.png +0 -0
  85. package/apps/web/dist/icon-maskable-512.png +0 -0
  86. package/apps/web/dist/icon.svg +4 -2
  87. package/apps/web/dist/index.html +3 -3
  88. package/apps/web/dist/manifest.webmanifest +1 -0
  89. package/apps/web/dist/sw.js +1 -1
  90. package/package.json +1 -1
  91. package/apps/web/dist/assets/index-NWUHLKte.js +0 -1203
@@ -1,3 +1,4 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
1
2
  import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
2
3
  import { createServer } from "node:http";
3
4
  import path from "node:path";
@@ -5,7 +6,6 @@ import { consola } from "consola";
5
6
  import { WebSocketServer } from "ws";
6
7
  import os from "node:os";
7
8
  import { deleteSession, forkSession, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
8
- import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import { execFile, spawn } from "node:child_process";
10
10
  import readline from "node:readline";
11
11
  import { z } from "zod";
@@ -13,7 +13,7 @@ import { Command } from "@commander-js/extra-typings";
13
13
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
14
14
  import { OpenCode } from "@opencode/client";
15
15
  import { ensure, headers } from "@opencode/client/service";
16
- import { and, eq, inArray, sql } from "drizzle-orm";
16
+ import { and, eq, gt, inArray, lte, sql } from "drizzle-orm";
17
17
  import { sumBy } from "es-toolkit";
18
18
  import { DatabaseSync } from "node:sqlite";
19
19
  import { drizzle } from "drizzle-orm/node-sqlite";
@@ -127,7 +127,7 @@ var AppServer = class {
127
127
  this.ready = this.call("initialize", {
128
128
  clientInfo: {
129
129
  name: "offsite",
130
- title: "Offsite",
130
+ title: "OffSite",
131
131
  version: "0.0.0"
132
132
  },
133
133
  capabilities: {
@@ -698,12 +698,12 @@ function record$1(value) {
698
698
  //#region src/args.ts
699
699
  let command = "web";
700
700
  let name = null;
701
- const program = new Command("offsite").description("Starts the server and the web app").option("--host <host>", "Address to listen on", "127.0.0.1").option("--port <port>", "Port to listen on", "47841").option("--dir <dir>", "Folder for settings and accounts", path.join(os.homedir(), ".offsite")).option("--no-auth", "Accept connections without the pairing token").option("--no-pair", "Don't create a pairing token or print pairing links").option("--fallback <url>", "Address to try when this one is unreachable, e.g. a Cloudflare tunnel");
701
+ const program = new Command("offsite").description("Starts the server and the web app").option("--host <host>", "Address to listen on", "127.0.0.1").option("--port <port>", "Port to listen on", "47841").option("--dir <dir>", "Folder for settings and accounts", path.join(os.homedir(), ".offsite")).option("--no-auth", "Accept connections without pairing").option("--no-pair", "Don't create a pairing code or print pairing links").option("--pwa", "Serve the web app as an installable app that works offline");
702
702
  program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
703
703
  program.command("serve").description("Starts only the server, without the web app").action(() => {
704
704
  command = "serve";
705
705
  });
706
- program.command("pair").description("Creates a pairing token for another browser and prints its links").option("--name <name>", "Name to recognize this browser by").action((options) => {
706
+ program.command("pair").description("Creates a pairing code for another browser and prints its link").option("--name <name>", "Name to recognize this browser by").action((options) => {
707
707
  command = "pair";
708
708
  name = options.name ?? null;
709
709
  });
@@ -788,58 +788,31 @@ const modelSetting = z.strictObject({
788
788
  hidden: z.boolean(),
789
789
  effort: z.string().nullable()
790
790
  });
791
+ const posixDir = z.string().refine((value) => path.posix.isAbsolute(value), "must be an absolute path").nullable().default(null);
792
+ const platformDirs = z.strictObject({
793
+ macos: posixDir,
794
+ linux: posixDir,
795
+ windows: z.string().refine((value) => path.win32.isAbsolute(value), "must be an absolute path").nullable().default(null)
796
+ }).prefault({});
797
+ function accountSchema(provider) {
798
+ return z.strictObject({
799
+ id: z.string(),
800
+ provider: z.literal(provider),
801
+ name: z.string(),
802
+ configDir: platformDirs,
803
+ enabled: z.boolean().default(true),
804
+ hiddenModels: z.record(z.string(), z.boolean()).default({})
805
+ });
806
+ }
791
807
  const configSchema = z.strictObject({
792
- claudeBaseDir: z.string().refine((value) => path.isAbsolute(value), "must be an absolute path").default(path.join(os.homedir(), ".claude")),
808
+ claudeBaseDir: platformDirs,
809
+ claudeAccounts: z.array(accountSchema("claude")).default([]),
810
+ codexAccounts: z.array(accountSchema("codex")).default([]),
793
811
  enabledProviders: z.strictObject({
794
812
  claude: z.boolean().default(true),
795
813
  codex: z.boolean().default(true),
796
814
  opencode: z.boolean().default(true)
797
815
  }).prefault({}),
798
- composer: z.strictObject({
799
- provider: z.enum([
800
- "claude",
801
- "codex",
802
- "opencode"
803
- ]).default("claude"),
804
- claude: z.strictObject({
805
- accountId: z.string().nullable().default(null),
806
- model: z.string().nullable().default(null),
807
- effort: z.enum([
808
- "low",
809
- "medium",
810
- "high",
811
- "xhigh",
812
- "max"
813
- ]).nullable().default(null),
814
- mode: z.enum([
815
- "default",
816
- "acceptEdits",
817
- "bypassPermissions",
818
- "plan",
819
- "dontAsk",
820
- "auto"
821
- ]).nullable().default("bypassPermissions"),
822
- plan: z.boolean().default(false)
823
- }).prefault({}),
824
- codex: z.strictObject({
825
- accountId: z.string().nullable().default(null),
826
- model: z.string().nullable().default(null),
827
- effort: z.string().nullable().default(null),
828
- mode: z.enum([
829
- "read-only",
830
- "auto",
831
- "full-access"
832
- ]).nullable().default("auto"),
833
- plan: z.boolean().default(false)
834
- }).prefault({}),
835
- opencode: z.strictObject({
836
- accountId: z.null().default(null),
837
- model: z.string().nullable().default(null),
838
- effort: z.string().nullable().default(null),
839
- mode: z.string().nullable().default(null),
840
- plan: z.boolean().default(false)
841
- }).prefault({})
842
- }).prefault({}),
843
816
  commitMessage: z.strictObject({
844
817
  provider: z.enum([
845
818
  "claude",
@@ -876,13 +849,16 @@ const configSchema = z.strictObject({
876
849
  }).prefault({})
877
850
  }).prefault({})
878
851
  });
879
- const defaults = configSchema.parse({});
852
+ const defaults$1 = configSchema.parse({});
880
853
  function platform() {
881
854
  if (process.platform === "darwin") return "macos";
882
855
  if (process.platform === "linux") return "linux";
883
856
  if (process.platform === "win32") return "windows";
884
857
  throw new Error(`unsupported platform ${process.platform}`);
885
858
  }
859
+ function claudeBaseDir() {
860
+ return readConfig().claudeBaseDir[platform()] ?? path.join(os.homedir(), ".claude");
861
+ }
886
862
  function defaultShell() {
887
863
  if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
888
864
  return os.userInfo().shell ?? "/bin/sh";
@@ -890,9 +866,10 @@ function defaultShell() {
890
866
  function configInfo() {
891
867
  return {
892
868
  config: readConfig(),
893
- defaults,
869
+ defaults: defaults$1,
894
870
  platform: platform(),
895
- defaultShell: defaultShell()
871
+ defaultShell: defaultShell(),
872
+ defaultClaudeBaseDir: path.join(os.homedir(), ".claude")
896
873
  };
897
874
  }
898
875
  function readConfig() {
@@ -901,13 +878,13 @@ function readConfig() {
901
878
  user = readJson("config.json");
902
879
  } catch (error) {
903
880
  console.warn(`invalid ${configFile}, using the default config: ${error}`);
904
- return structuredClone(defaults);
881
+ return structuredClone(defaults$1);
905
882
  }
906
- if (user === null) return structuredClone(defaults);
883
+ if (user === null) return structuredClone(defaults$1);
907
884
  const result = configSchema.safeParse(user);
908
885
  if (!result.success) {
909
886
  console.warn(`invalid ${configFile}, using the default config:\n${z.prettifyError(result.error)}`);
910
- return structuredClone(defaults);
887
+ return structuredClone(defaults$1);
911
888
  }
912
889
  return result.data;
913
890
  }
@@ -915,7 +892,7 @@ function saveConfig(value) {
915
892
  const result = configSchema.safeParse(value);
916
893
  if (!result.success) throw new Error(z.prettifyError(result.error));
917
894
  writeJson("config.json", result.data);
918
- process.env.CLAUDE_CONFIG_DIR = result.data.claudeBaseDir;
895
+ process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
919
896
  broadcast({
920
897
  type: "config",
921
898
  config: result.data
@@ -2423,10 +2400,16 @@ const sessions$1 = snakeCase.table("sessions", {
2423
2400
  "minimized"
2424
2401
  ] }).notNull()
2425
2402
  });
2426
- const tokens = snakeCase.table("tokens", {
2403
+ const pairingCodes = snakeCase.table("pairing_codes", {
2404
+ hash: text().primaryKey(),
2405
+ name: text(),
2406
+ expiresAt: integer().notNull()
2407
+ });
2408
+ const devices = snakeCase.table("devices", {
2427
2409
  hash: text().primaryKey(),
2428
2410
  name: text(),
2429
2411
  createdAt: integer().notNull(),
2412
+ expiresAt: integer().notNull(),
2430
2413
  lastUsedAt: integer()
2431
2414
  });
2432
2415
  const pushSubscriptions = snakeCase.table("push_subscriptions", {
@@ -2717,7 +2700,7 @@ async function find(provider, paths) {
2717
2700
  }
2718
2701
  function watchSessions() {
2719
2702
  for (const watcher of watchers) watcher.close();
2720
- watchers = [["claude", path.join(readConfig().claudeBaseDir, "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
2703
+ watchers = [["claude", path.join(claudeBaseDir(), "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
2721
2704
  if (!readConfig().enabledProviders[provider]) return [];
2722
2705
  try {
2723
2706
  const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
@@ -2913,14 +2896,15 @@ function providerName(provider) {
2913
2896
  return providerNames[provider];
2914
2897
  }
2915
2898
  function storedAccounts() {
2916
- return (readJson("accounts.json") ?? []).map((item) => ({
2917
- id: item.id,
2918
- provider: item.provider,
2919
- name: item.name,
2920
- configDir: item.configDir,
2921
- enabled: item.enabled ?? true,
2922
- hiddenModels: item.hiddenModels ?? {}
2923
- }));
2899
+ const config = readConfig();
2900
+ return [...config.claudeAccounts, ...config.codexAccounts].flatMap((item) => {
2901
+ const configDir = item.configDir[platform()];
2902
+ if (configDir === null) return [];
2903
+ return [{
2904
+ ...item,
2905
+ configDir
2906
+ }];
2907
+ });
2924
2908
  }
2925
2909
  function assertEnabled(provider, accountId) {
2926
2910
  if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
@@ -3019,29 +3003,50 @@ function providerCapabilities(provider, accountId, cwd) {
3019
3003
  }
3020
3004
  }
3021
3005
  function addAccount(provider, name, configDir) {
3022
- saveAccounts([...storedAccounts(), {
3023
- id: randomUUID(),
3024
- provider,
3025
- ...validAccount(name, configDir),
3026
- enabled: true,
3027
- hiddenModels: {}
3028
- }]);
3006
+ const account = validAccount(name, configDir);
3007
+ const config = readConfig();
3008
+ const key = `${provider}Accounts`;
3009
+ saveConfig({
3010
+ ...config,
3011
+ [key]: [...config[key], {
3012
+ id: randomUUID(),
3013
+ provider,
3014
+ name: account.name,
3015
+ configDir: { [platform()]: account.configDir },
3016
+ enabled: true,
3017
+ hiddenModels: {}
3018
+ }]
3019
+ });
3029
3020
  }
3030
3021
  async function updateAccount(update) {
3031
3022
  const before = storedAccounts().find((item) => item.id === update.id);
3032
3023
  if (before === void 0) throw new Error("That account was removed");
3033
3024
  const account = validAccount(update.name, update.configDir);
3034
- saveAccounts(storedAccounts().map((item) => item.id === update.id ? {
3035
- ...item,
3036
- ...account,
3037
- enabled: update.enabled,
3038
- hiddenModels: update.hiddenModels
3039
- } : item));
3025
+ const config = readConfig();
3026
+ const key = `${before.provider}Accounts`;
3027
+ saveConfig({
3028
+ ...config,
3029
+ [key]: config[key].map((item) => item.id === update.id ? {
3030
+ ...item,
3031
+ name: account.name,
3032
+ configDir: {
3033
+ ...item.configDir,
3034
+ [platform()]: account.configDir
3035
+ },
3036
+ enabled: update.enabled,
3037
+ hiddenModels: update.hiddenModels
3038
+ } : item)
3039
+ });
3040
3040
  forgetClaude(update.id);
3041
3041
  if (before.enabled && !update.enabled) await stopAccount(before);
3042
3042
  }
3043
3043
  function removeAccount(id) {
3044
- saveAccounts(storedAccounts().filter((item) => item.id !== id));
3044
+ const config = readConfig();
3045
+ saveConfig({
3046
+ ...config,
3047
+ claudeAccounts: config.claudeAccounts.filter((item) => item.id !== id),
3048
+ codexAccounts: config.codexAccounts.filter((item) => item.id !== id)
3049
+ });
3045
3050
  forgetClaude(id);
3046
3051
  }
3047
3052
  function validAccount(name, configDir) {
@@ -3057,9 +3062,6 @@ function validAccount(name, configDir) {
3057
3062
  };
3058
3063
  throw new Error("Use an absolute config directory or one starting with ~/");
3059
3064
  }
3060
- function saveAccounts(items) {
3061
- writeJson("accounts.json", items);
3062
- }
3063
3065
  //#endregion
3064
3066
  //#region src/lib/claude/claude.ts
3065
3067
  const efforts = [
@@ -3233,7 +3235,7 @@ async function recordUsage(accountId, probe) {
3233
3235
  }
3234
3236
  }
3235
3237
  function isLinked(configDir) {
3236
- const base = readConfig().claudeBaseDir;
3238
+ const base = claudeBaseDir();
3237
3239
  try {
3238
3240
  if (realpathSync(configDir) === realpathSync(base)) return true;
3239
3241
  return realpathSync(path.join(configDir, "projects")) === realpathSync(path.join(base, "projects"));
@@ -3689,7 +3691,7 @@ function text$1(value) {
3689
3691
  //#endregion
3690
3692
  //#region src/lib/claude/history.ts
3691
3693
  async function sessionFile(sessionId) {
3692
- const projectsDir = path.join(readConfig().claudeBaseDir, "projects");
3694
+ const projectsDir = path.join(claudeBaseDir(), "projects");
3693
3695
  for (const dir of await readdir(projectsDir)) {
3694
3696
  const file = path.join(projectsDir, dir, `${sessionId}.jsonl`);
3695
3697
  try {
@@ -5618,6 +5620,81 @@ function send(connection, event) {
5618
5620
  });
5619
5621
  }
5620
5622
  //#endregion
5623
+ //#region src/storage/state.ts
5624
+ const stateFile = path.join(home, "state.json");
5625
+ const stateSchema = z.strictObject({ composer: z.strictObject({
5626
+ provider: z.enum([
5627
+ "claude",
5628
+ "codex",
5629
+ "opencode"
5630
+ ]).default("claude"),
5631
+ claude: z.strictObject({
5632
+ accountId: z.string().nullable().default(null),
5633
+ model: z.string().nullable().default(null),
5634
+ effort: z.enum([
5635
+ "low",
5636
+ "medium",
5637
+ "high",
5638
+ "xhigh",
5639
+ "max"
5640
+ ]).nullable().default(null),
5641
+ mode: z.enum([
5642
+ "default",
5643
+ "acceptEdits",
5644
+ "bypassPermissions",
5645
+ "plan",
5646
+ "dontAsk",
5647
+ "auto"
5648
+ ]).nullable().default("bypassPermissions"),
5649
+ plan: z.boolean().default(false)
5650
+ }).prefault({}),
5651
+ codex: z.strictObject({
5652
+ accountId: z.string().nullable().default(null),
5653
+ model: z.string().nullable().default(null),
5654
+ effort: z.string().nullable().default(null),
5655
+ mode: z.enum([
5656
+ "read-only",
5657
+ "auto",
5658
+ "full-access"
5659
+ ]).nullable().default("auto"),
5660
+ plan: z.boolean().default(false)
5661
+ }).prefault({}),
5662
+ opencode: z.strictObject({
5663
+ accountId: z.null().default(null),
5664
+ model: z.string().nullable().default(null),
5665
+ effort: z.string().nullable().default(null),
5666
+ mode: z.string().nullable().default(null),
5667
+ plan: z.boolean().default(false)
5668
+ }).prefault({})
5669
+ }).prefault({}) });
5670
+ const defaults = stateSchema.parse({});
5671
+ function readComposer() {
5672
+ let user;
5673
+ try {
5674
+ user = readJson("state.json");
5675
+ } catch (error) {
5676
+ console.warn(`invalid ${stateFile}, using the default state: ${error}`);
5677
+ return structuredClone(defaults.composer);
5678
+ }
5679
+ if (user === null) return structuredClone(defaults.composer);
5680
+ const result = stateSchema.safeParse(user);
5681
+ if (!result.success) {
5682
+ console.warn(`invalid ${stateFile}, using the default state:\n${z.prettifyError(result.error)}`);
5683
+ return structuredClone(defaults.composer);
5684
+ }
5685
+ return result.data.composer;
5686
+ }
5687
+ function saveComposer(value) {
5688
+ const result = stateSchema.safeParse({ composer: value });
5689
+ if (!result.success) throw new Error(z.prettifyError(result.error));
5690
+ writeJson("state.json", result.data);
5691
+ broadcast({
5692
+ type: "composer",
5693
+ composer: result.data.composer
5694
+ });
5695
+ return result.data.composer;
5696
+ }
5697
+ //#endregion
5621
5698
  //#region src/api/socket.ts
5622
5699
  const handlers = {
5623
5700
  ping: () => null,
@@ -5625,6 +5702,7 @@ const handlers = {
5625
5702
  refreshAll();
5626
5703
  return {
5627
5704
  ...configInfo(),
5705
+ composer: readComposer(),
5628
5706
  hostname: os.hostname(),
5629
5707
  projects: projects$1(),
5630
5708
  sessions: sessions(),
@@ -5633,6 +5711,8 @@ const handlers = {
5633
5711
  };
5634
5712
  },
5635
5713
  "config.get": () => configInfo(),
5714
+ "composer.get": () => readComposer(),
5715
+ "composer.save": (params) => saveComposer(params),
5636
5716
  "config.save": async (params) => {
5637
5717
  const before = readConfig();
5638
5718
  const saved = saveConfig(params);
@@ -5641,7 +5721,7 @@ const handlers = {
5641
5721
  "codex",
5642
5722
  "opencode"
5643
5723
  ]) if (before.enabledProviders[provider] && !saved.enabledProviders[provider]) await stopProvider(provider);
5644
- if (saved.claudeBaseDir === before.claudeBaseDir && JSON.stringify(saved.enabledProviders) === JSON.stringify(before.enabledProviders)) return saved;
5724
+ if (JSON.stringify(saved.claudeBaseDir) === JSON.stringify(before.claudeBaseDir) && JSON.stringify(saved.enabledProviders) === JSON.stringify(before.enabledProviders)) return saved;
5645
5725
  watchSessions();
5646
5726
  refreshAll();
5647
5727
  publishAccounts();
@@ -5821,38 +5901,58 @@ async function answer(client, text) {
5821
5901
  }
5822
5902
  }
5823
5903
  //#endregion
5824
- //#region src/storage/token.ts
5825
- function readTokens() {
5826
- return db.select().from(tokens).orderBy(tokens.createdAt).all();
5904
+ //#region src/storage/auth.ts
5905
+ const CODE_LIFETIME = 3e5;
5906
+ const DEVICE_LIFETIME = 2592e6;
5907
+ function addCode(name) {
5908
+ const code = randomBytes(16).toString("base64url");
5909
+ db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
5910
+ db.insert(pairingCodes).values({
5911
+ hash: hash(code),
5912
+ name,
5913
+ expiresAt: Date.now() + CODE_LIFETIME
5914
+ }).run();
5915
+ return code;
5827
5916
  }
5828
- function addToken(name) {
5917
+ function redeemCode(code) {
5918
+ const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
5919
+ if (found === void 0 || found.expiresAt <= Date.now()) return null;
5829
5920
  const token = randomBytes(32).toString("base64url");
5830
- db.insert(tokens).values({
5831
- hash: hashToken(token),
5832
- name,
5833
- createdAt: Date.now()
5921
+ const now = Date.now();
5922
+ db.insert(devices).values({
5923
+ hash: hash(token),
5924
+ name: found.name,
5925
+ createdAt: now,
5926
+ expiresAt: now + DEVICE_LIFETIME
5834
5927
  }).run();
5835
5928
  return token;
5836
5929
  }
5837
- function removeTokens(hashes) {
5838
- db.delete(tokens).where(inArray(tokens.hash, hashes)).run();
5930
+ function findDevice(token) {
5931
+ const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
5932
+ return found === void 0 ? null : found.hash;
5933
+ }
5934
+ function isPaired(device) {
5935
+ return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
5839
5936
  }
5840
- function hasTokens() {
5841
- return db.select().from(tokens).limit(1).get() !== void 0;
5937
+ function hasDevices() {
5938
+ return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
5842
5939
  }
5843
- function matchesToken(given) {
5844
- if (given === null) return false;
5845
- return db.select().from(tokens).where(eq(tokens.hash, hashToken(given))).get() !== void 0;
5940
+ function readDevices() {
5941
+ return db.select().from(devices).orderBy(devices.createdAt).all();
5846
5942
  }
5847
- function recordUse(token) {
5848
- db.update(tokens).set({ lastUsedAt: Date.now() }).where(eq(tokens.hash, hashToken(token))).run();
5943
+ function removeDevices(hashes) {
5944
+ db.delete(devices).where(inArray(devices.hash, hashes)).run();
5849
5945
  }
5850
- function hashToken(token) {
5851
- return createHash("sha256").update(token).digest("hex");
5946
+ function recordUse(device) {
5947
+ db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
5948
+ }
5949
+ function hash(secret) {
5950
+ return createHash("sha256").update(secret).digest("hex");
5852
5951
  }
5853
5952
  //#endregion
5854
5953
  //#region src/main.ts
5855
5954
  const UNPAIR_CHECK = 5e3;
5955
+ const TICKET_LIFETIME = 6e4;
5856
5956
  const contentTypes = {
5857
5957
  ".html": "text/html; charset=utf-8",
5858
5958
  ".js": "text/javascript; charset=utf-8",
@@ -5870,12 +5970,10 @@ if (!Number.isInteger(port) || port < 1 || port > 65535) {
5870
5970
  process.exit(1);
5871
5971
  }
5872
5972
  const address = `http://${args.host}:${port}`;
5873
- const fallback = args.fallback === void 0 ? null : readFallback(args.fallback);
5874
5973
  if (args.command === "pair") {
5875
- const links = pairingLinks(addToken(args.name));
5876
- console.log("Open this link in the browser to pair:");
5974
+ const links = pairingLinks(addCode(args.name));
5975
+ console.log("Open this link in a browser within 5 minutes to pair it:");
5877
5976
  console.log(` ${links.local}`);
5878
- if (fallback !== null) console.log(`Fallback: ${fallback}`);
5879
5977
  process.exit(0);
5880
5978
  }
5881
5979
  if (args.command === "unpair") {
@@ -5883,7 +5981,7 @@ if (args.command === "unpair") {
5883
5981
  process.exit(0);
5884
5982
  }
5885
5983
  lockHome();
5886
- process.env.CLAUDE_CONFIG_DIR = readConfig().claudeBaseDir;
5984
+ process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
5887
5985
  watchSessions();
5888
5986
  refreshAll();
5889
5987
  if (findClaude() === null) {
@@ -5891,9 +5989,14 @@ if (findClaude() === null) {
5891
5989
  process.exit(1);
5892
5990
  }
5893
5991
  const publicDir = path.join(import.meta.dirname, "..", "..", "web", "dist");
5894
- const token = args.auth && args.pair && !hasTokens() ? addToken(null) : null;
5992
+ const code = args.auth && args.pair && !hasDevices() ? addCode(null) : null;
5993
+ const tickets = /* @__PURE__ */ new Map();
5895
5994
  const server = createServer((request, response) => {
5896
5995
  const url = new URL(request.url ?? "/", "http://localhost");
5996
+ if (url.pathname.startsWith("/api/")) {
5997
+ answerApi(url.pathname, request, response);
5998
+ return;
5999
+ }
5897
6000
  if (args.command === "serve") {
5898
6001
  response.statusCode = 404;
5899
6002
  response.end("offsite serve has no web app");
@@ -5905,26 +6008,27 @@ const sockets = new WebSocketServer({ noServer: true });
5905
6008
  const paired = /* @__PURE__ */ new Map();
5906
6009
  server.on("upgrade", (request, socket, head) => {
5907
6010
  const url = new URL(request.url ?? "/", "http://localhost");
5908
- const given = url.searchParams.get("token");
5909
- if (url.pathname !== "/ws" || !authorized(url)) {
6011
+ const device = args.auth ? redeemTicket(url.searchParams.get("ticket")) : null;
6012
+ if (url.pathname !== "/ws" || args.auth && device === null) {
5910
6013
  socket.destroy();
5911
6014
  return;
5912
6015
  }
5913
6016
  sockets.handleUpgrade(request, socket, head, (client) => {
5914
6017
  connectClient(client);
5915
- if (!args.auth || given === null) return;
5916
- paired.set(client, given);
6018
+ if (device === null) return;
6019
+ paired.set(client, device);
5917
6020
  client.on("close", () => paired.delete(client));
5918
- recordUse(given);
6021
+ recordUse(device);
5919
6022
  });
5920
6023
  });
5921
6024
  if (args.auth) setInterval(() => {
5922
- for (const [client, given] of paired) if (!matchesToken(given)) client.close();
6025
+ for (const [client, device] of paired) if (!isPaired(device)) client.close();
6026
+ for (const [ticket, item] of tickets) if (item.expiresAt <= Date.now()) tickets.delete(ticket);
5923
6027
  }, UNPAIR_CHECK);
5924
6028
  server.listen(port, args.host, () => {
5925
6029
  startPush(`${args.host}:${port}`);
5926
- if (args.pair && (token !== null || !args.auth)) {
5927
- const links = pairingLinks(token);
6030
+ if (args.pair && (code !== null || !args.auth)) {
6031
+ const links = pairingLinks(code);
5928
6032
  if (args.command === "web") console.log(`offsite is running at ${links.local}`);
5929
6033
  } else {
5930
6034
  console.log(`offsite is running at ${address}`);
@@ -5938,12 +6042,67 @@ function stop() {
5938
6042
  stopAll();
5939
6043
  process.exit(0);
5940
6044
  }
5941
- function authorized(url) {
5942
- return !args.auth || matchesToken(url.searchParams.get("token"));
6045
+ function answerApi(pathname, request, response) {
6046
+ response.setHeader("access-control-allow-origin", "*");
6047
+ if (request.method === "OPTIONS") {
6048
+ response.setHeader("access-control-allow-methods", "POST");
6049
+ response.setHeader("access-control-allow-headers", "authorization");
6050
+ response.statusCode = 204;
6051
+ response.end();
6052
+ return;
6053
+ }
6054
+ if (request.method !== "POST") {
6055
+ response.statusCode = 405;
6056
+ response.end("use POST");
6057
+ return;
6058
+ }
6059
+ const given = request.headers.authorization?.match(/^Bearer (.+)$/)?.[1] ?? null;
6060
+ if (pathname === "/api/pair") {
6061
+ const token = given === null ? null : redeemCode(given);
6062
+ if (token === null) {
6063
+ response.statusCode = 401;
6064
+ response.end("That pairing code expired or was already used");
6065
+ return;
6066
+ }
6067
+ answerJson(response, { token });
6068
+ return;
6069
+ }
6070
+ if (pathname === "/api/ticket") {
6071
+ if (!args.auth) {
6072
+ answerJson(response, { ticket: null });
6073
+ return;
6074
+ }
6075
+ const device = given === null ? null : findDevice(given);
6076
+ if (device === null) {
6077
+ response.statusCode = 401;
6078
+ response.end("This browser needs pairing");
6079
+ return;
6080
+ }
6081
+ const ticket = randomBytes(32).toString("base64url");
6082
+ tickets.set(ticket, {
6083
+ device,
6084
+ expiresAt: Date.now() + TICKET_LIFETIME
6085
+ });
6086
+ answerJson(response, { ticket });
6087
+ return;
6088
+ }
6089
+ response.statusCode = 404;
6090
+ response.end("not found");
6091
+ }
6092
+ function answerJson(response, body) {
6093
+ response.setHeader("content-type", "application/json");
6094
+ response.end(JSON.stringify(body));
6095
+ }
6096
+ function redeemTicket(ticket) {
6097
+ if (ticket === null) return null;
6098
+ const found = tickets.get(ticket);
6099
+ tickets.delete(ticket);
6100
+ if (found === void 0 || found.expiresAt <= Date.now()) return null;
6101
+ return found.device;
5943
6102
  }
5944
6103
  async function unpair() {
5945
- const tokens = readTokens();
5946
- if (tokens.length === 0) {
6104
+ const devices = readDevices();
6105
+ if (devices.length === 0) {
5947
6106
  console.log("No browsers are paired");
5948
6107
  return;
5949
6108
  }
@@ -5955,11 +6114,12 @@ async function unpair() {
5955
6114
  type: "multiselect",
5956
6115
  required: false,
5957
6116
  cancel: "null",
5958
- options: tokens.map((item) => ({
6117
+ options: devices.map((item) => ({
5959
6118
  value: item.hash,
5960
6119
  label: [
5961
6120
  item.name ?? "Unnamed",
5962
6121
  `paired ${new Date(item.createdAt).toLocaleString()}`,
6122
+ `${item.expiresAt <= Date.now() ? "expired" : "expires"} ${new Date(item.expiresAt).toLocaleString()}`,
5963
6123
  item.lastUsedAt === null ? "never used" : `last used ${new Date(item.lastUsedAt).toLocaleString()}`
5964
6124
  ].join(" · ")
5965
6125
  }))
@@ -5968,30 +6128,21 @@ async function unpair() {
5968
6128
  console.log("Nothing unpaired");
5969
6129
  return;
5970
6130
  }
5971
- removeTokens(picked);
6131
+ removeDevices(picked);
5972
6132
  console.log(`Unpaired ${picked.length} ${picked.length === 1 ? "browser" : "browsers"}; a running offsite disconnects them within seconds`);
5973
6133
  }
5974
- function readFallback(link) {
5975
- let url;
5976
- try {
5977
- url = new URL(link);
5978
- } catch {
5979
- console.error(`invalid fallback URL ${link}`);
5980
- process.exit(1);
5981
- }
5982
- if (url.protocol !== "http:" && url.protocol !== "https:") {
5983
- console.error(`the fallback URL ${link} needs http:// or https://`);
5984
- process.exit(1);
5985
- }
5986
- return url.origin;
5987
- }
5988
- function pairingLinks(token) {
6134
+ function pairingLinks(code) {
5989
6135
  return { local: `${address}/pair#${new URLSearchParams({
5990
6136
  machine: address,
5991
- ...token !== null && { token }
6137
+ ...code !== null && { code }
5992
6138
  })}` };
5993
6139
  }
5994
6140
  function serveFile(pathname, response) {
6141
+ if (!args.pwa && pathname === "/manifest.webmanifest") {
6142
+ response.statusCode = 404;
6143
+ response.end("offsite runs without --pwa");
6144
+ return;
6145
+ }
5995
6146
  const requested = path.join(publicDir, path.normalize(pathname));
5996
6147
  const file = requested.startsWith(publicDir + path.sep) && statSync(requested, { throwIfNoEntry: false })?.isFile() ? requested : path.join(publicDir, "index.html");
5997
6148
  if (!existsSync(file)) {
@@ -6000,6 +6151,10 @@ function serveFile(pathname, response) {
6000
6151
  return;
6001
6152
  }
6002
6153
  response.setHeader("content-type", contentTypes[path.extname(file)] ?? "application/octet-stream");
6154
+ if (!args.pwa && path.basename(file) === "index.html") {
6155
+ response.end(readFileSync(file, "utf8").replace(/<link rel="manifest"[^>]*>/, ""));
6156
+ return;
6157
+ }
6003
6158
  createReadStream(file).pipe(response);
6004
6159
  }
6005
6160
  //#endregion