offsite 0.0.42 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,32 +10,6 @@ Claude Code, Codex and OpenCode in your browser. It uses the same sessions as th
10
10
  npx offsite
11
11
  ```
12
12
 
13
- Then open the pairing link it prints. It pairs this browser with the server, so only browsers that opened the link can use it.
14
-
15
- | Command | What it does |
16
- | ---------------- | --------------------------------------------------------------------------------- |
17
- | `offsite` | Starts the server and the web app |
18
- | `offsite web` | Same as `offsite` |
19
- | `offsite serve` | Starts only the server |
20
- | `offsite pair` | Makes a pairing token for another browser and prints its link. `--name` labels it |
21
- | `offsite unpair` | Lists paired browsers so you can pick which to unpair |
22
-
23
- | Flag | Default | What it sets |
24
- | ----------- | ------------ | ------------------------------------------------------------ |
25
- | `--host` | `127.0.0.1` | Address to listen on |
26
- | `--port` | `47841` | Port to listen on |
27
- | `--dir` | `~/.offsite` | Folder for settings and accounts |
28
- | `--no-auth` | auth on | Accept connections without the pairing token |
29
- | `--no-pair` | pair on | Don't create a pairing token or print pairing links on start |
30
- | `--pwa` | off | Serve the web app as an installable app that works offline |
31
-
32
- Pairing tokens stay in `tokens.json` inside `--dir`. A running OffSite disconnects an unpaired browser within seconds.
33
-
34
- > [!WARNING]
35
- > With `--no-auth`, anyone who can reach its port, including any website open in your browser, can run your coding agents and open a terminal on your machine.
36
-
37
- Don't use the same session in OffSite and the CLI at the same time. Switching between them is fine.
38
-
39
13
  ## Separate instances
40
14
 
41
15
  Give each instance its own port and `--dir`.
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { consola } from "consola";
6
6
  import { WebSocketServer } from "ws";
7
7
  import { z } from "zod";
8
- import os, { homedir } from "node:os";
8
+ import os from "node:os";
9
9
  import { deleteSession, forkSession, getSessionInfo, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
10
10
  import { execFile, spawn } from "node:child_process";
11
11
  import readline from "node:readline";
@@ -85,7 +85,7 @@ function broadcast(event) {
85
85
  //#region src/args.ts
86
86
  let command = "web";
87
87
  let name = null;
88
- 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").configureHelp({ showGlobalOptions: true });
88
+ 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").option("--connect <addresses>", "Add this machine to the web app with these comma separated addresses; needs --no-auth").configureHelp({ showGlobalOptions: true });
89
89
  program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
90
90
  program.command("serve").description("Starts only the server, without the web app").action(() => {
91
91
  command = "serve";
@@ -100,11 +100,19 @@ program.command("unpair").description("Picks paired browsers to unpair").action(
100
100
  program.parse();
101
101
  const options = program.opts();
102
102
  if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
103
+ if (options.connect !== void 0 && options.auth) program.error("error: --connect needs --no-auth");
104
+ if (options.connect !== void 0 && command === "serve") program.error("error: --connect can't be used with offsite serve");
105
+ const connect$1 = options.connect === void 0 ? null : options.connect.split(",").map((address) => {
106
+ const url = URL.parse(address);
107
+ if (url === null || url.protocol !== "http:" && url.protocol !== "https:") return program.error(`error: --connect address ${address} needs http:// or https://, like https://example.com`);
108
+ return url.origin;
109
+ });
103
110
  const args = {
104
111
  ...options,
105
112
  dir: path.resolve(options.dir),
106
113
  command,
107
- name
114
+ name,
115
+ connect: connect$1
108
116
  };
109
117
  const serverDir = path.join(import.meta.dirname, "..");
110
118
  //#endregion
@@ -955,6 +963,10 @@ function platform() {
955
963
  function claudeBaseDir() {
956
964
  return readConfig().claudeBaseDir[platform()] ?? path.join(os.homedir(), ".claude");
957
965
  }
966
+ function workingDirectory() {
967
+ const dir = readConfig().workingDir[platform()] ?? "~";
968
+ return dir.startsWith("~") ? path.join(os.homedir(), dir.slice(1)) : dir;
969
+ }
958
970
  function defaultShell() {
959
971
  if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
960
972
  return os.userInfo().shell ?? "/bin/sh";
@@ -3314,6 +3326,7 @@ const CAN = 24;
3314
3326
  const SUB = 26;
3315
3327
  var Scanner = class {
3316
3328
  title = "";
3329
+ directory = null;
3317
3330
  state = "ground";
3318
3331
  payload = [];
3319
3332
  scan(bytes) {
@@ -3390,6 +3403,10 @@ var Scanner = class {
3390
3403
  this.title = text.slice(2);
3391
3404
  return null;
3392
3405
  }
3406
+ if (text.startsWith("9;9;")) {
3407
+ this.directory = text.slice(4).replace(/^"(.*)"$/, "$1");
3408
+ return null;
3409
+ }
3393
3410
  if (text.startsWith("9;")) {
3394
3411
  const body = text.slice(2);
3395
3412
  const digits = /^\d*/.exec(body)?.[0].length ?? 0;
@@ -3710,7 +3727,7 @@ function startShell(cwd) {
3710
3727
  encoding: null
3711
3728
  });
3712
3729
  }
3713
- async function currentDirectory(pid) {
3730
+ async function currentDirectory(pid, reported) {
3714
3731
  if (process.platform === "linux") return readlink(`/proc/${pid}/cwd`);
3715
3732
  if (process.platform === "darwin") {
3716
3733
  const { stdout } = await run("lsof", [
@@ -3725,6 +3742,10 @@ async function currentDirectory(pid) {
3725
3742
  if (line === void 0) throw new Error(`lsof found no folder for ${pid}`);
3726
3743
  return line.slice(1);
3727
3744
  }
3745
+ if (process.platform === "win32") {
3746
+ if (reported === null || !statSync(reported, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`the shell reported no existing folder, it reported ${reported}`);
3747
+ return reported;
3748
+ }
3728
3749
  throw new Error("reading the directory of another process is not supported on this platform");
3729
3750
  }
3730
3751
  async function busyProgram(pid) {
@@ -3835,12 +3856,6 @@ const OUTPUT_LIMIT$1 = 1048576;
3835
3856
  const STALL_LIMIT = 1e4;
3836
3857
  const SILENCE_LIMIT$1 = 9e4;
3837
3858
  const KILL_DELAY = 3e3;
3838
- const pasteTypes = {
3839
- "image/png": "png",
3840
- "image/jpeg": "jpg",
3841
- "image/gif": "gif",
3842
- "image/webp": "webp"
3843
- };
3844
3859
  const instance = `${Date.now()}-${randomUUID()}`;
3845
3860
  const projects = /* @__PURE__ */ new Map();
3846
3861
  const terminals = /* @__PURE__ */ new Map();
@@ -3894,15 +3909,6 @@ function closeProjectTabs(projectId) {
3894
3909
  for (const tab of [...project.tabs]) closeTab(tab.id);
3895
3910
  publish();
3896
3911
  }
3897
- function savePastedImage(type, data) {
3898
- const extension = pasteTypes[type];
3899
- if (extension === void 0) throw new Error("pasted image must be a PNG, JPEG, GIF or WebP image");
3900
- const directory = path.join(home, "paste");
3901
- mkdirSync(directory, { recursive: true });
3902
- const file = path.join(directory, `${Date.now()}-${randomUUID()}.${extension}`);
3903
- writeFileSync(file, data);
3904
- return file;
3905
- }
3906
3912
  function attach(client) {
3907
3913
  detachTerminal(client);
3908
3914
  const connection = {
@@ -3965,7 +3971,7 @@ async function request(connection, message) {
3965
3971
  tab.minimized = false;
3966
3972
  project.activeTab = tab.id;
3967
3973
  }
3968
- if (project.tabs.length === 0) await createTab(message.projectId);
3974
+ if (project.tabs.length === 0 && message.projectId !== "global") await createTab(message.projectId);
3969
3975
  break;
3970
3976
  }
3971
3977
  }
@@ -3999,7 +4005,7 @@ function input(connection, bytes) {
3999
4005
  terminal.pty.write(Buffer.from(frame.data));
4000
4006
  }
4001
4007
  async function createTab(projectId) {
4002
- const cwd = projectId === "global" ? homedir() : projectPath(projectId);
4008
+ const cwd = projectId === "global" ? workingDirectory() : projectPath(projectId);
4003
4009
  if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
4004
4010
  console.error(`${cwd} is not a folder, terminal not created`);
4005
4011
  return;
@@ -4012,7 +4018,7 @@ async function createTab(projectId) {
4012
4018
  const active = project.activeTab === null ? void 0 : terminals.get(project.activeTab);
4013
4019
  let directory = cwd;
4014
4020
  if (active !== void 0) try {
4015
- directory = await currentDirectory(active.pty.pid);
4021
+ directory = await currentDirectory(active.pty.pid, active.scanner.directory);
4016
4022
  } catch (error) {
4017
4023
  console.warn(`starting in the project folder, could not read the folder of the active terminal: ${error}`);
4018
4024
  }
@@ -4111,12 +4117,8 @@ function drain() {
4111
4117
  }, 10);
4112
4118
  }
4113
4119
  function scan(id, terminal, data) {
4114
- if (!pushActive()) {
4115
- terminal.scanner = new Scanner();
4116
- return;
4117
- }
4118
4120
  const alert = terminal.scanner.scan(data);
4119
- if (alert === null || notifierWatching()) return;
4121
+ if (!pushActive() || alert === null || notifierWatching()) return;
4120
4122
  if (!(alert.type === "bell" ? terminal.alerts.startBell() : terminal.alerts.startNotification())) return;
4121
4123
  const windowTitle = terminal.scanner.title;
4122
4124
  setTimeout(() => push(id, alert, windowTitle), 500);
@@ -6404,6 +6406,23 @@ async function changedElsewhere(run, cwd) {
6404
6406
  return last !== void 0 && !run.messages.some((item) => item.uuid === last);
6405
6407
  }
6406
6408
  //#endregion
6409
+ //#region src/services/terminal/paste.ts
6410
+ const pasteTypes = {
6411
+ "image/png": "png",
6412
+ "image/jpeg": "jpg",
6413
+ "image/gif": "gif",
6414
+ "image/webp": "webp"
6415
+ };
6416
+ function savePastedImage(type, data) {
6417
+ const extension = pasteTypes[type];
6418
+ if (extension === void 0) throw new Error("pasted image must be a PNG, JPEG, GIF or WebP image");
6419
+ const directory = path.join(home, "paste");
6420
+ mkdirSync(directory, { recursive: true });
6421
+ const file = path.join(directory, `${Date.now()}-${randomUUID()}.${extension}`);
6422
+ writeFileSync(file, data);
6423
+ return file;
6424
+ }
6425
+ //#endregion
6407
6426
  //#region src/storage/state.ts
6408
6427
  const stateFile = path.join(home, "state.json");
6409
6428
  const stateSchema = z.strictObject({ composer: z.strictObject({
@@ -6866,12 +6885,12 @@ if (args.auth) setInterval(() => {
6866
6885
  }, UNPAIR_CHECK);
6867
6886
  server.listen(port, args.host, () => {
6868
6887
  startPush(`${args.host}:${port}`);
6869
- if (args.pair && (code !== null || !args.auth)) {
6888
+ if (args.connect === null && args.pair && (code !== null || !args.auth)) {
6870
6889
  const links = pairingLinks(code);
6871
6890
  if (args.command === "web") console.log(`offsite is running at ${links.local}`);
6872
6891
  } else {
6873
6892
  console.log(`offsite is running at ${address}`);
6874
- if (args.pair) console.log("Run offsite pair to pair another browser");
6893
+ if (args.auth && args.pair) console.log("Run offsite pair to pair another browser");
6875
6894
  }
6876
6895
  if (!args.auth) console.warn("auth is off; anyone who can reach this port can control this machine");
6877
6896
  });
@@ -7121,8 +7140,11 @@ function serveFile(pathname, response) {
7121
7140
  return;
7122
7141
  }
7123
7142
  response.setHeader("content-type", contentTypes[path.extname(file)] ?? "application/octet-stream");
7124
- if (!args.pwa && path.basename(file) === "index.html") {
7125
- response.end(readFileSync(file, "utf8").replace(/<link rel="manifest"[^>]*>/, ""));
7143
+ if (path.basename(file) === "index.html") {
7144
+ let page = readFileSync(file, "utf8");
7145
+ if (!args.pwa) page = page.replace(/<link rel="manifest"[^>]*>/, "");
7146
+ if (args.connect !== null) page = page.replace("</head>", `<meta name="offsite-connect" content="${args.connect.join(" ")}"></head>`);
7147
+ response.end(page);
7126
7148
  return;
7127
7149
  }
7128
7150
  createReadStream(file).pipe(response);