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 +0 -26
- package/apps/server/dist/main.mjs +53 -31
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/web/dist/assets/{dist-BRNbPpUx.js → dist-DITht1bT.js} +1 -1
- package/apps/web/dist/assets/{index-CEcok5lo.js → index-CHnreZja.js} +59 -59
- package/apps/web/dist/assets/index-D3NpzVu2.css +2 -0
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/sw.js +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/index-Bw3cJKrf.css +0 -2
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
|
|
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" ?
|
|
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 (
|
|
7125
|
-
|
|
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);
|