offsite 0.0.71 → 0.0.73

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.
@@ -8,10 +8,10 @@ import { WebSocketServer } from "ws";
8
8
  import { z } from "zod";
9
9
  import os, { tmpdir } from "node:os";
10
10
  import { deleteSession, forkSession, getSessionInfo, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
11
- import { execFile, spawn } from "node:child_process";
11
+ import { execFile, spawn, spawnSync } from "node:child_process";
12
12
  import readline from "node:readline";
13
13
  import { Command } from "@commander-js/extra-typings";
14
- import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rm, stat, writeFile } from "node:fs/promises";
14
+ import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rm, rmdir, stat, writeFile } from "node:fs/promises";
15
15
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
16
16
  import { OpenCode, isSessionNotFoundError } from "@opencode/client";
17
17
  import { ensure, headers } from "@opencode/client/service";
@@ -127,6 +127,7 @@ const home = args.dir;
127
127
  const lockFile = path.join(home, "server.pid");
128
128
  const savedServer = z.object({ port: z.number() }).nullable();
129
129
  mkdirSync(home, { recursive: true });
130
+ const scratchAvailable = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: home }).status !== 0;
130
131
  function listenPort() {
131
132
  if (args.command !== "pair" || args.portGiven) return args.port;
132
133
  const saved = readSettings("server.json", savedServer, null);
@@ -1050,7 +1051,8 @@ function configInfo() {
1050
1051
  platform: platform(),
1051
1052
  defaultShell: defaultShell(),
1052
1053
  defaultClaudeBaseDir: path.join(os.homedir(), ".claude"),
1053
- homeDir: os.homedir()
1054
+ homeDir: os.homedir(),
1055
+ scratchAvailable
1054
1056
  };
1055
1057
  }
1056
1058
  function readConfig() {
@@ -2795,7 +2797,8 @@ const projects$1 = snakeCase.table("projects", {
2795
2797
  name: text().notNull(),
2796
2798
  logo: text(),
2797
2799
  position: integer().notNull().default(0),
2798
- hidden: integer({ mode: "boolean" }).notNull().default(false)
2800
+ hidden: integer({ mode: "boolean" }).notNull().default(false),
2801
+ scratch: integer({ mode: "boolean" }).notNull().default(false)
2799
2802
  });
2800
2803
  const sessions$1 = snakeCase.table("sessions", {
2801
2804
  sessionId: text().primaryKey(),
@@ -3640,13 +3643,15 @@ function logoRank(file) {
3640
3643
  return -1;
3641
3644
  }
3642
3645
  // src/services/projects.ts
3646
+ const scratchRoot = path.join(home, "scratch");
3643
3647
  function projects() {
3644
3648
  return db.select({
3645
3649
  id: projects$1.id,
3646
3650
  path: projects$1.path,
3647
3651
  name: projects$1.name,
3648
3652
  logo: projects$1.logo,
3649
- hidden: projects$1.hidden
3653
+ hidden: projects$1.hidden,
3654
+ scratch: projects$1.scratch
3650
3655
  }).from(projects$1).orderBy(projects$1.position).all();
3651
3656
  }
3652
3657
  function projectPath(id) {
@@ -3657,19 +3662,58 @@ function projectPath(id) {
3657
3662
  async function addProject(input) {
3658
3663
  const folder = path.resolve(expandHome(input.trim()));
3659
3664
  if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
3660
- const last = db.select({ position: max(projects$1.position) }).from(projects$1).get();
3661
3665
  db.insert(projects$1).values({
3662
3666
  id: randomUUID(),
3663
3667
  path: folder,
3664
3668
  name: path.basename(folder) || folder,
3665
3669
  logo: await findLogo(folder),
3666
- position: last === void 0 || last.position === null ? 0 : last.position + 1
3670
+ position: nextPosition()
3667
3671
  }).onConflictDoNothing({ target: projects$1.path }).run();
3668
3672
  publish$1();
3669
3673
  const project = projects().find((item) => item.path === folder);
3670
3674
  if (project === void 0) throw new Error(`Could not add ${folder}`);
3671
3675
  return project;
3672
3676
  }
3677
+ async function addScratchProject(text) {
3678
+ if (!scratchAvailable) throw new Error(`Chats without a project are unavailable because ${home} is inside a Git repository`);
3679
+ await mkdir(scratchRoot, { recursive: true });
3680
+ const words = text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).slice(0, 5).join("-").slice(0, 48).replace(/-+$/, "");
3681
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA");
3682
+ while (true) {
3683
+ const folder = path.join(scratchRoot, [
3684
+ date,
3685
+ words,
3686
+ randomBytes(4).toString("hex")
3687
+ ].filter(Boolean).join("-"));
3688
+ try {
3689
+ await mkdir(folder);
3690
+ } catch (error) {
3691
+ if (error.code === "EEXIST") continue;
3692
+ throw error;
3693
+ }
3694
+ const id = randomUUID();
3695
+ db.insert(projects$1).values({
3696
+ id,
3697
+ path: folder,
3698
+ name: "No Project",
3699
+ scratch: true
3700
+ }).run();
3701
+ publish$1();
3702
+ return {
3703
+ id,
3704
+ path: folder
3705
+ };
3706
+ }
3707
+ }
3708
+ async function removeScratchProject(id, folder) {
3709
+ db.delete(projects$1).where(eq(projects$1.id, id)).run();
3710
+ publish$1();
3711
+ try {
3712
+ await rmdir(folder);
3713
+ } catch (error) {
3714
+ if (error.code !== "ENOTEMPTY") throw error;
3715
+ }
3716
+ }
3673
3717
  async function updateProject(id, patch) {
3674
3718
  if (patch.name !== void 0) {
3675
3719
  const name = patch.name.trim();
@@ -3677,6 +3721,10 @@ async function updateProject(id, patch) {
3677
3721
  db.update(projects$1).set({ name }).where(eq(projects$1.id, id)).run();
3678
3722
  }
3679
3723
  if (patch.logo !== void 0) db.update(projects$1).set({ logo: patch.logo === null ? null : await prepareLogo(patch.logo) }).where(eq(projects$1.id, id)).run();
3724
+ if (patch.scratch === false) db.update(projects$1).set({
3725
+ scratch: false,
3726
+ position: nextPosition()
3727
+ }).where(eq(projects$1.id, id)).run();
3680
3728
  if (patch.hidden !== void 0) db.update(projects$1).set({ hidden: patch.hidden }).where(eq(projects$1.id, id)).run();
3681
3729
  publish$1();
3682
3730
  return null;
@@ -3686,7 +3734,7 @@ function removeProject(id) {
3686
3734
  publish$1();
3687
3735
  }
3688
3736
  function reorderProjects(ids) {
3689
- const current = projects().map((project) => project.id);
3737
+ const current = projects().filter((project) => !project.scratch).map((project) => project.id);
3690
3738
  if (ids.length !== current.length || !current.every((id) => ids.includes(id))) throw new Error("The projects changed, try again");
3691
3739
  db.transaction((transaction) => {
3692
3740
  ids.forEach((id, position) => {
@@ -3709,6 +3757,10 @@ async function directories(input) {
3709
3757
  })).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
3710
3758
  };
3711
3759
  }
3760
+ function nextPosition() {
3761
+ const last = db.select({ position: max(projects$1.position) }).from(projects$1).get();
3762
+ return last === void 0 || last.position === null ? 0 : last.position + 1;
3763
+ }
3712
3764
  function publish$1() {
3713
3765
  broadcast({
3714
3766
  type: "projects",
@@ -3848,7 +3900,7 @@ function sessions() {
3848
3900
  return withStatus(allProviders.flatMap((provider) => listed[provider]).filter((item) => known.has(item.projectId))).filter((session) => session.status !== "inactive");
3849
3901
  }
3850
3902
  async function inactiveSessions(projectId) {
3851
- return (await everything(projectPaths().filter(([, id]) => projectId === null || id === projectId))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
3903
+ return (await everything(projects().filter((project) => projectId === null || project.id === projectId || projectId === "no-project" && project.scratch).map((project) => [project.path, project.id]))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
3852
3904
  }
3853
3905
  async function searchableSessions() {
3854
3906
  const paths = projectPaths();
@@ -4036,11 +4088,17 @@ async function tracked(provider, paths) {
4036
4088
  if (provider === "codex" && !codexInstalled() || provider === "opencode" && !openCodeInstalled()) return null;
4037
4089
  return (await Promise.all(db.select().from(sessions$1).all().map((record) => {
4038
4090
  const owner = ownerOf(record.sessionId);
4039
- if (owner === provider) return info(provider, record.sessionId, paths);
4040
- if (owner === void 0) return info(provider, record.sessionId, paths).catch(() => null);
4091
+ if (owner === provider) return lookup(provider, record.sessionId, paths);
4092
+ if (owner === void 0) return lookup(provider, record.sessionId, paths).catch(() => null);
4041
4093
  return null;
4042
4094
  }))).filter((item) => item !== null);
4043
4095
  }
4096
+ async function lookup(provider, sessionId, paths) {
4097
+ const known = listed[provider].find((item) => item.id === sessionId);
4098
+ const knownPath = known === void 0 ? void 0 : [...paths].find((entry) => entry[1] === known.projectId);
4099
+ if (knownPath === void 0) return info(provider, sessionId, paths);
4100
+ return await info(provider, sessionId, new Map([knownPath])) ?? info(provider, sessionId, paths);
4101
+ }
4044
4102
  function watchSessions() {
4045
4103
  for (const watcher of watchers) watcher.close();
4046
4104
  watchers = [["claude", path.join(claudeBaseDir(), "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
@@ -4095,7 +4153,7 @@ async function updateSession(id, patch) {
4095
4153
  }
4096
4154
  if (patch.title !== void 0) refreshSessions(providerOf(id));
4097
4155
  }
4098
- async function removeSession(id) {
4156
+ async function removeSession(id, projectId) {
4099
4157
  const provider = providerOf(id);
4100
4158
  const run = runs.get(id);
4101
4159
  if (run !== void 0) {
@@ -4114,6 +4172,13 @@ async function removeSession(id) {
4114
4172
  db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
4115
4173
  db.delete(sessionOpens).where(eq(sessionOpens.sessionId, id)).run();
4116
4174
  listed[provider] = listed[provider].filter((item) => item.id !== id);
4175
+ const project = projects().find((item) => item.id === projectId);
4176
+ if (project === void 0 || !project.scratch) return;
4177
+ try {
4178
+ if ((await everything([[project.path, project.id]])).filter((item) => item.id !== id).length === 0) removeProject(project.id);
4179
+ } catch (error) {
4180
+ console.warn(`could not check other chats in ${project.path}: ${error}`);
4181
+ }
4117
4182
  }
4118
4183
  async function fork(sessionId, projectId, messageId) {
4119
4184
  const provider = providerOf(sessionId);
@@ -5930,25 +5995,37 @@ async function open$1(client, sessionId, projectId) {
5930
5995
  }
5931
5996
  async function send$1(client, request) {
5932
5997
  assertEnabled(request.settings.provider, request.settings.accountId);
5998
+ if (request.projectId === "no-project") {
5999
+ if (request.sessionId !== null) throw new Error("A started chat always has a project");
6000
+ const project = await addScratchProject(request.prompt.text);
6001
+ await submitFirst(client, await startRun(project.path, request.settings).catch(async (error) => {
6002
+ await removeScratchProject(project.id, project.path);
6003
+ throw error;
6004
+ }), request, project.id);
6005
+ return;
6006
+ }
5933
6007
  const cwd = projectPath(request.projectId);
5934
6008
  if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${cwd} is not a folder`);
5935
6009
  if (request.sessionId === null) {
5936
- const run = await startRun(cwd, request.settings);
5937
- const sessionId = run.sessionId;
5938
- client.opening = sessionId;
5939
- client.sessionId = sessionId;
5940
- client.send({
5941
- type: "started",
5942
- sessionId,
5943
- projectId: request.projectId
5944
- });
5945
- await run.submit(request.prompt, request.delivery);
5946
- recordInteraction(sessionId);
6010
+ await submitFirst(client, await startRun(cwd, request.settings), request, request.projectId);
5947
6011
  return;
5948
6012
  }
5949
6013
  const sessionId = request.sessionId;
5950
6014
  await locked(sessionId, () => sendExisting(client, sessionId, cwd, request));
5951
6015
  }
6016
+ async function submitFirst(client, run, request, projectId) {
6017
+ const sessionId = run.sessionId;
6018
+ client.opening = sessionId;
6019
+ client.sessionId = sessionId;
6020
+ client.send({
6021
+ type: "started",
6022
+ sessionId,
6023
+ projectId,
6024
+ from: request.projectId
6025
+ });
6026
+ await run.submit(request.prompt, request.delivery);
6027
+ recordInteraction(sessionId);
6028
+ }
5952
6029
  async function sendExisting(client, sessionId, cwd, request) {
5953
6030
  const provider = providerOf(sessionId);
5954
6031
  if (request.settings.provider !== provider) throw new Error(`This session belongs to ${providerName(provider)}`);
@@ -7643,7 +7720,7 @@ const handlers = {
7643
7720
  },
7644
7721
  "sessions.remove": async (params) => {
7645
7722
  try {
7646
- await removeSession(params.id);
7723
+ await removeSession(params.id, params.projectId);
7647
7724
  } finally {
7648
7725
  publishSessions();
7649
7726
  }
@@ -7721,8 +7798,8 @@ const handlers = {
7721
7798
  publishAccounts();
7722
7799
  return null;
7723
7800
  },
7724
- capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
7725
- "files.list": (params) => listFiles(projectPath(params.projectId)),
7801
+ capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null || params.projectId === "no-project" ? os.homedir() : projectPath(params.projectId)),
7802
+ "files.list": (params) => params.projectId === "no-project" ? [] : listFiles(projectPath(params.projectId)),
7726
7803
  "files.entries": (params) => listEntries(projectPath(params.projectId)),
7727
7804
  "files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
7728
7805
  "files.write": (params) => writeProjectFile(projectPath(params.projectId), params.path, params.content, params.base),