@chipschat/opencode 1.0.14 → 1.0.15

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
@@ -92,7 +92,7 @@ each model call and tells the agent what changed, so an edit applies from its ne
92
92
 
93
93
  - The agent's own, which a person may change from its page: its folder's `AGENTS.md`, shown empty
94
94
  until there is one, and any file `instructions` finds in its folder.
95
- - The harness's own set, on its page: `~/chips-opencode/agents/AGENTS.md`, which every agent
95
+ - The harness's own set, on its page: `~/Chips/Agents/opencode/AGENTS.md`, which every agent
96
96
  reads and a person may change; any `AGENTS.md` above it; and your own, with the files
97
97
  `instructions` names elsewhere, which your own OpenCode reads too, so they are shown and not
98
98
  changed.
@@ -163,7 +163,7 @@ first. The steps below run the bridge from this checkout instead, to work on it.
163
163
  ```
164
164
 
165
165
  The bridge runs OpenCode's server itself (`opencode serve` on `127.0.0.1`, 4096 when it is
166
- free), with a password it makes and keeps in `~/.chips-opencode/server.json`, readable by you
166
+ free), with a password it makes and keeps in `~/Chips/.bridges/opencode/server.json`, readable by you
167
167
  only: nobody starts a server or makes up a password. While the bridge runs, a server that stops
168
168
  is started again; when the bridge stops, so does its server.
169
169
 
@@ -177,13 +177,22 @@ first. The steps below run the bridge from this checkout instead, to work on it.
177
177
  two answer twice. The key Chips gives it is kept; a new link from Chips replaces it. Or join with
178
178
  `CHIPS_URL` and a code a person allows.
179
179
 
180
- 3. Optionally, name the agents in `~/.chips-opencode/config.json` (see `config.example.json`).
180
+ 3. Optionally, name the agents in `~/Chips/.bridges/opencode/config.json` (see `config.example.json`).
181
181
  Without a config it is OpenCode's `build` agent, named OpenCode.
182
182
 
183
- Each agent's sessions work in a folder of its own, `~/chips-opencode/agents/<its name>`, made
183
+ Each agent's sessions work in a folder of its own, `~/Chips/Agents/opencode/<its name>`, made
184
184
  the first time it is spoken to and kept after a rename; `folders` moves them. A `directory`
185
185
  puts every session in that one project instead, a repository the agents share.
186
186
 
187
+ Everything the bridge keeps is under `~/Chips` (Chips ADR 0283): its config, sessions, server
188
+ password and state in `~/Chips/.bridges/opencode`, the agents' folders in
189
+ `~/Chips/Agents/opencode`, where its own server runs from. Until 1.0.15 they were
190
+ `~/.chips-opencode` and `~/chips-opencode/agents`. The first start of 1.0.15 moves them and the
191
+ folder paths it saved, and once OpenCode answers, asks it to move each session of a folder that
192
+ moved (`POST /api/session/:id/move`), with everything said in it: OpenCode won't use a session
193
+ whose folder is gone. It renames and never copies; where something is at the new place already it
194
+ moves in what isn't there yet and says in its log what it left.
195
+
187
196
  **A server of your own.** To use an `opencode serve` you run yourself, give its password in the
188
197
  environment variable `passwordEnv` names (`OPENCODE_PASSWORD` by default, never in the file),
189
198
  and its `url` in the config when it isn't `127.0.0.1:4096`. `npm run keep` takes the password
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- `dist/main.js` in @chipschat/opencode 1.0.14 carries these packages, bundled in. Each one's
3
+ `dist/main.js` in @chipschat/opencode 1.0.15 carries these packages, bundled in. Each one's
4
4
  licence follows.
5
5
 
6
6
  ## @chipschat/harness 1.0.2 (Apache-2.0)
package/dist/main.js CHANGED
@@ -8,9 +8,8 @@ var __export = (target, all) => {
8
8
  };
9
9
 
10
10
  // src/main.ts
11
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
12
- import { homedir as homedir7 } from "node:os";
13
- import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
11
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
12
+ import { dirname as dirname6, join as join10 } from "node:path";
14
13
 
15
14
  // node_modules/zod/v4/classic/external.js
16
15
  var external_exports = {};
@@ -5106,7 +5105,7 @@ function isRecursive(inst, stack, resolve5) {
5106
5105
  }
5107
5106
  return answer;
5108
5107
  };
5109
- const merge2 = (answer) => {
5108
+ const merge3 = (answer) => {
5110
5109
  if (answer > result)
5111
5110
  result = answer;
5112
5111
  };
@@ -5115,7 +5114,7 @@ function isRecursive(inst, stack, resolve5) {
5115
5114
  switch (kind) {
5116
5115
  case "object": {
5117
5116
  const raw = rawShape(def);
5118
- merge2(raw ? shape(raw, true) : ASSUMED);
5117
+ merge3(raw ? shape(raw, true) : ASSUMED);
5119
5118
  check2(def.catchall);
5120
5119
  break;
5121
5120
  }
@@ -5165,7 +5164,7 @@ function isRecursive(inst, stack, resolve5) {
5165
5164
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5166
5165
  case "lazy": {
5167
5166
  const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
5168
- merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5167
+ merge3(inner ? isRecursive(inner, stack, false) : ASSUMED);
5169
5168
  break;
5170
5169
  }
5171
5170
  // a leaf by choice: `parts` are regex fragments, not data positions
@@ -23398,8 +23397,8 @@ var Project = class {
23398
23397
  const end2 = all.findIndex((line, at) => at > start && line.startsWith(">>>>>>> "));
23399
23398
  if (start < 0 || end2 < 0)
23400
23399
  return void 0;
23401
- const shown = all.slice(Math.max(0, start - 1), end2 + 2);
23402
- return shown.length <= MARKED_LINES ? shown.join("\n") : [...shown.slice(0, MARKED_LINES - 1), "\u2026"].join("\n");
23400
+ const shown2 = all.slice(Math.max(0, start - 1), end2 + 2);
23401
+ return shown2.length <= MARKED_LINES ? shown2.join("\n") : [...shown2.slice(0, MARKED_LINES - 1), "\u2026"].join("\n");
23403
23402
  } catch {
23404
23403
  return void 0;
23405
23404
  }
@@ -25304,7 +25303,7 @@ async function ask(url2, options, send, machine) {
25304
25303
  if (asked.status !== 201)
25305
25304
  throw await refused(asked);
25306
25305
  const { request_id, secret, code, interval } = await asked.json();
25307
- const show = options.onCode ?? ((shown, where) => process.stderr.write(`Allow ${options.name} in Chips at ${where}. Its code is ${shown}.
25306
+ const show = options.onCode ?? ((shown2, where) => process.stderr.write(`Allow ${options.name} in Chips at ${where}. Its code is ${shown2}.
25308
25307
  `));
25309
25308
  show(code, url2);
25310
25309
  for (; ; ) {
@@ -25871,9 +25870,9 @@ import { globSync, readFileSync as readFileSync5, statSync } from "node:fs";
25871
25870
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
25872
25871
  import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative, sep as sep2 } from "node:path";
25873
25872
  var AGENTS_MD = "AGENTS.md";
25874
- function placesOf(env, home3) {
25875
- const config3 = env["OPENCODE_CONFIG_DIR"] || join5(env["XDG_CONFIG_HOME"] || join5(home3, ".config"), "opencode");
25876
- return { home: home3, config: config3 };
25873
+ function placesOf(env, home) {
25874
+ const config3 = env["OPENCODE_CONFIG_DIR"] || join5(env["XDG_CONFIG_HOME"] || join5(home, ".config"), "opencode");
25875
+ return { home, config: config3 };
25877
25876
  }
25878
25877
  function hashOf(text2) {
25879
25878
  return createHash2("sha256").update(text2, "utf8").digest("hex");
@@ -25901,7 +25900,7 @@ function upTo(dir, stop2) {
25901
25900
  }
25902
25901
  return dirs;
25903
25902
  }
25904
- function namedFiles(sources, folder2, root, home3) {
25903
+ function namedFiles(sources, folder2, root, home) {
25905
25904
  const found = [];
25906
25905
  const glob = (pattern, cwd) => {
25907
25906
  try {
@@ -25912,7 +25911,7 @@ function namedFiles(sources, folder2, root, home3) {
25912
25911
  };
25913
25912
  for (const source2 of sources) {
25914
25913
  if (/^https?:\/\//i.test(source2)) continue;
25915
- const expanded = source2.startsWith("~/") ? join5(home3, source2.slice(2)) : source2;
25914
+ const expanded = source2.startsWith("~/") ? join5(home, source2.slice(2)) : source2;
25916
25915
  const matches = isAbsolute2(expanded) ? glob(expanded.slice(dirname4(expanded).length + 1), dirname4(expanded)) : upTo(folder2, within(folder2, root) ? root : folder2).flatMap((dir) => glob(expanded, dir));
25917
25916
  for (const path of matches.sort()) if (isFile(path) && !found.includes(path)) found.push(path);
25918
25917
  }
@@ -25932,15 +25931,15 @@ var ContextFiles = class {
25932
25931
  }
25933
25932
  places;
25934
25933
  folders;
25935
- /** `~/chips-opencode/agents/AGENTS.md`, or the whole path where it isn't in the home. */
25934
+ /** `~/Chips/Agents/opencode/AGENTS.md`, or the whole path where it isn't in the home. */
25936
25935
  tilde(path) {
25937
- const { home: home3 } = this.places;
25938
- return within(path, home3) ? `~${path.slice(home3.length)}` : path;
25936
+ const { home } = this.places;
25937
+ return within(path, home) ? `~${path.slice(home.length)}` : path;
25939
25938
  }
25940
25939
  /** A shared file's name: where it is, a step at a time (`~ › .config › opencode › AGENTS.md`), with no slash. */
25941
25940
  crumbs(path) {
25942
- const { home: home3 } = this.places;
25943
- const steps = within(path, home3) ? ["~", ...relative(home3, path).split(sep2)] : ["/", ...path.split(sep2)];
25941
+ const { home } = this.places;
25942
+ const steps = within(path, home) ? ["~", ...relative(home, path).split(sep2)] : ["/", ...path.split(sep2)];
25944
25943
  return steps.filter((step) => step !== "").join(" \u203A ").slice(0, 255);
25945
25944
  }
25946
25945
  /**
@@ -26249,6 +26248,13 @@ var OpenCodeClient = class {
26249
26248
  const metadata = data["metadata"];
26250
26249
  return { id: id2, metadata: typeof metadata === "object" && metadata !== null ? metadata : {} };
26251
26250
  }
26251
+ /**
26252
+ * A session moved to another folder (2.0.25), with everything said in it; the session's context
26253
+ * says it moved. It works where the old folder is gone, as after Chips ADR 0283's move.
26254
+ */
26255
+ async moveSession(sessionId, directory) {
26256
+ await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/move`, { directory });
26257
+ }
26252
26258
  /** A session's title, or its metadata, which this replaces whole. */
26253
26259
  async update(sessionId, changes) {
26254
26260
  await this.call("PATCH", `/api/session/${encodeURIComponent(sessionId)}`, {
@@ -26720,10 +26726,10 @@ var Bridge = class {
26720
26726
  }
26721
26727
  /** The Main agent into the harness's own room, in the place of whichever of ours is there. */
26722
26728
  async goHome(main2, known) {
26723
- const { chips: chips2, home: home3 } = this.options;
26724
- if (home3 === void 0 || main2.spaces.includes(home3)) return;
26725
- const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home3));
26726
- await chips2.agents.attach(main2.agentId, { spaceId: home3, replacing: there?.agentId ?? chips2.harnessId }).catch((error62) => this.log(`${main2.name} stays out of its home: ${String(error62)}`));
26729
+ const { chips: chips2, home } = this.options;
26730
+ if (home === void 0 || main2.spaces.includes(home)) return;
26731
+ const there2 = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home));
26732
+ await chips2.agents.attach(main2.agentId, { spaceId: home, replacing: there2?.agentId ?? chips2.harnessId }).catch((error62) => this.log(`${main2.name} stays out of its home: ${String(error62)}`));
26727
26733
  }
26728
26734
  /**
26729
26735
  * Trouble while the server doesn't answer, and fine once it does (ADR 0083). Nothing while a
@@ -26838,7 +26844,7 @@ var Bridge = class {
26838
26844
  * that isn't one of the harness's has no copy, nor has an ask the room stopped waiting for.
26839
26845
  */
26840
26846
  async makeCopy(fork) {
26841
- const { chips: chips2, opencode: opencode2, state: state2, saveState } = this.options;
26847
+ const { chips: chips2, opencode: opencode2, state: state2, saveState: saveState2 } = this.options;
26842
26848
  const spaceId = fork.space_id;
26843
26849
  if (spaceId === void 0) return;
26844
26850
  const late = Date.now() - Date.parse(fork.ts);
@@ -26869,7 +26875,7 @@ var Bridge = class {
26869
26875
  const folder2 = await this.folderFor({ who, agent });
26870
26876
  kept = { ...copied === void 0 ? {} : { session: copied.id }, ...folder2 === void 0 ? {} : { folder: folder2 } };
26871
26877
  state2.forks = { ...state2.forks, [fork.id]: kept };
26872
- saveState(state2);
26878
+ saveState2(state2);
26873
26879
  }
26874
26880
  const copy = await chips2.agents.register({
26875
26881
  key: `opencode:fork:${fork.id}`,
@@ -26882,7 +26888,7 @@ var Bridge = class {
26882
26888
  if (kept.folder !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: kept.folder };
26883
26889
  const pick2 = state2.models?.[who];
26884
26890
  if (pick2 !== void 0) state2.models = { ...state2.models, [copy.agentId]: pick2 };
26885
- saveState(state2);
26891
+ saveState2(state2);
26886
26892
  if (copied !== void 0) {
26887
26893
  await opencode2.update(copied.id, {
26888
26894
  title: `Chips space ${spaceId}`,
@@ -26947,7 +26953,7 @@ var Bridge = class {
26947
26953
  async modelChanged(change) {
26948
26954
  const whose = await this.whoseIs(change.agent_id);
26949
26955
  if (whose === void 0) throw new Refuse("This harness has no such agent.");
26950
- const { opencode: opencode2, state: state2, saveState } = this.options;
26956
+ const { opencode: opencode2, state: state2, saveState: saveState2 } = this.options;
26951
26957
  let pick2;
26952
26958
  try {
26953
26959
  pick2 = pickOf(change, await opencode2.models());
@@ -26958,7 +26964,7 @@ var Bridge = class {
26958
26964
  if (pick2 === null) delete models[whose.who];
26959
26965
  else models[whose.who] = pick2;
26960
26966
  state2.models = models;
26961
- saveState(state2);
26967
+ saveState2(state2);
26962
26968
  const name = await this.nameOf(whose);
26963
26969
  this.log(`${name} runs on ${pick2 === null ? "its default" : `${pick2.model}${pick2.effort === void 0 ? "" : ` at ${pick2.effort}`}`}, from Chips`);
26964
26970
  const copy = await this.modelCopyOf(change.agent_id);
@@ -26996,7 +27002,7 @@ var Bridge = class {
26996
27002
  */
26997
27003
  async readingOf(folder2) {
26998
27004
  const { opencode: opencode2 } = this.options;
26999
- const { home: home3 } = this.places;
27005
+ const { home } = this.places;
27000
27006
  let root = this.roots.get(folder2);
27001
27007
  if (root === void 0) {
27002
27008
  root = await opencode2.projectRoot(folder2).catch(() => folder2);
@@ -27007,7 +27013,7 @@ var Bridge = class {
27007
27013
  () => false
27008
27014
  );
27009
27015
  const sources = await this.readsNamed ? await opencode2.instructions(folder2).catch(() => []) : [];
27010
- return { stop: within(folder2, home3) ? home3 : root, named: namedFiles(sources, folder2, root, home3) };
27016
+ return { stop: within(folder2, home) ? home : root, named: namedFiles(sources, folder2, root, home) };
27011
27017
  }
27012
27018
  /** Each agent of the harness's, with the folder its sessions work in and what OpenCode reads there. */
27013
27019
  async agentReadings() {
@@ -27353,7 +27359,7 @@ var Bridge = class {
27353
27359
  }
27354
27360
  /** The words into the room's session: the one it had, or a new one when OpenCode no longer has it. */
27355
27361
  async give({ id: id2, who, agent, spaceId }, said, rules) {
27356
- const { opencode: opencode2, state: state2, saveState } = this.options;
27362
+ const { opencode: opencode2, state: state2, saveState: saveState2 } = this.options;
27357
27363
  let kept = state2.sessions[id2];
27358
27364
  if (kept === void 0 && this.copying.size > 0) {
27359
27365
  await Promise.allSettled(this.copying.values());
@@ -27377,7 +27383,7 @@ var Bridge = class {
27377
27383
  metadata: { chips_space: spaceId, chips_agent: who }
27378
27384
  });
27379
27385
  state2.sessions[id2] = made;
27380
- saveState(state2);
27386
+ saveState2(state2);
27381
27387
  return { sessionId: made, given: await this.prompted(made, said(made), rules) };
27382
27388
  }
27383
27389
  /**
@@ -27385,14 +27391,14 @@ var Bridge = class {
27385
27391
  * kept by whose it is, so a rename doesn't move it. None without `folders`: the project's.
27386
27392
  */
27387
27393
  async folderFor({ who, agent }) {
27388
- const { folders: root, state: state2, saveState } = this.options;
27394
+ const { folders: root, state: state2, saveState: saveState2 } = this.options;
27389
27395
  if (root === void 0) return void 0;
27390
27396
  let folder2 = state2.folders?.[who];
27391
27397
  if (folder2 === void 0) {
27392
27398
  const name = await this.nameOf({ who, agent });
27393
27399
  folder2 = freeFolder(root, name, Object.values(state2.folders ?? {}));
27394
27400
  state2.folders = { ...state2.folders, [who]: folder2 };
27395
- saveState(state2);
27401
+ saveState2(state2);
27396
27402
  }
27397
27403
  mkdirSync4(folder2, { recursive: true });
27398
27404
  return folder2;
@@ -28044,12 +28050,55 @@ function lastLine(said) {
28044
28050
  }
28045
28051
 
28046
28052
  // src/keep.ts
28047
- import { homedir as homedir5 } from "node:os";
28048
- import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
28053
+ import { basename as basename3 } from "node:path";
28049
28054
  import { fileURLToPath as fileURLToPath2 } from "node:url";
28055
+
28056
+ // src/places.ts
28057
+ import { existsSync as existsSync5 } from "node:fs";
28058
+ import { homedir as homedir5 } from "node:os";
28059
+ import { isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve2, sep as sep3 } from "node:path";
28060
+ function placesOf2(home = homedir5()) {
28061
+ return {
28062
+ settings: join7(home, "Chips", ".bridges", "opencode"),
28063
+ agents: join7(home, "Chips", "Agents", "opencode"),
28064
+ oldSettings: join7(home, ".chips-opencode"),
28065
+ oldAgents: join7(home, "chips-opencode", "agents")
28066
+ };
28067
+ }
28068
+ var fromHome = (path, home = homedir5()) => path === "~" ? home : path.startsWith("~/") ? join7(home, path.slice(2)) : path;
28069
+ function inside2(root, path) {
28070
+ const under = relative2(root, path);
28071
+ return under === ".." || under.startsWith(`..${sep3}`) || isAbsolute3(under) ? void 0 : under;
28072
+ }
28073
+ function nowAt(path, before2, after) {
28074
+ const under = inside2(before2, path);
28075
+ if (under === void 0) return path;
28076
+ const moved = join7(after, under);
28077
+ return existsSync5(path) && !existsSync5(moved) ? path : moved;
28078
+ }
28079
+ var configOf = (word, places, home) => resolve2(fromHome(word ?? join7(places.settings, "config.json"), home));
28080
+ function configTargetOf(word, home = homedir5()) {
28081
+ const places = placesOf2(home);
28082
+ const path = configOf(word, places, home);
28083
+ const under = inside2(places.oldSettings, path);
28084
+ return under === void 0 ? path : join7(places.settings, under);
28085
+ }
28086
+ function configPathOf(word, home = homedir5()) {
28087
+ const places = placesOf2(home);
28088
+ const target = configTargetOf(word, home);
28089
+ const under = inside2(places.settings, target);
28090
+ const old = under === void 0 ? void 0 : join7(places.oldSettings, under);
28091
+ return old !== void 0 && existsSync5(old) && !existsSync5(target) ? old : target;
28092
+ }
28093
+ function foldersPathOf(path, home = homedir5()) {
28094
+ const places = placesOf2(home);
28095
+ return nowAt(resolve2(fromHome(path, home)), places.oldAgents, places.agents);
28096
+ }
28097
+
28098
+ // src/keep.ts
28050
28099
  var flags = process.argv.slice(2);
28051
28100
  var words3 = flags.filter((word) => !word.startsWith("--"));
28052
- var whole = (word) => resolve2(word.startsWith("~/") ? join7(homedir5(), word.slice(2)) : word);
28101
+ var whole = (word) => configPathOf(word);
28053
28102
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
28054
28103
  var self = fileURLToPath2(import.meta.url);
28055
28104
  var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
@@ -28094,12 +28143,147 @@ function keepIfAsked(label2, url2, names, log2, before2 = () => void 0) {
28094
28143
  );
28095
28144
  }
28096
28145
 
28146
+ // src/move.ts
28147
+ import { lstatSync, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync6, renameSync as renameSync2, rmdirSync, writeFileSync as writeFileSync4 } from "node:fs";
28148
+ import { homedir as homedir6 } from "node:os";
28149
+ import { dirname as dirname5, join as join8, resolve as resolve3 } from "node:path";
28150
+ var there = (path) => {
28151
+ try {
28152
+ lstatSync(path);
28153
+ return true;
28154
+ } catch {
28155
+ return false;
28156
+ }
28157
+ };
28158
+ var isFolder = (path) => lstatSync(path).isDirectory();
28159
+ var shown = (path, home) => path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
28160
+ function merge2(from, to, log2, home = homedir6(), quiet = false) {
28161
+ if (!there(from)) return;
28162
+ try {
28163
+ if (!there(to)) {
28164
+ mkdirSync5(dirname5(to), { recursive: true });
28165
+ renameSync2(from, to);
28166
+ if (!quiet) log2(`moved ${shown(from, home)} to ${shown(to, home)}`);
28167
+ return;
28168
+ }
28169
+ if (!isFolder(from) || !isFolder(to)) {
28170
+ log2(`${shown(from, home)} was left where it is: ${shown(to, home)} is there already`);
28171
+ return;
28172
+ }
28173
+ for (const entry of readdirSync(from)) merge2(join8(from, entry), join8(to, entry), log2, home, true);
28174
+ if (readdirSync(from).length === 0) rmdirSync(from);
28175
+ if (!quiet) log2(`moved what was in ${shown(from, home)} into ${shown(to, home)}`);
28176
+ } catch (error62) {
28177
+ log2(`${shown(from, home)} couldn't be moved to ${shown(to, home)}, so it is used where it is: ${error62.message}`);
28178
+ }
28179
+ }
28180
+ function removeIfEmpty(path) {
28181
+ try {
28182
+ if (readdirSync(path).length === 0) rmdirSync(path);
28183
+ } catch {
28184
+ }
28185
+ }
28186
+ function pairOf(path, places) {
28187
+ const old = inside2(places.oldAgents, path);
28188
+ if (old !== void 0) return { old: path, now: join8(places.agents, old) };
28189
+ const now = inside2(places.agents, path);
28190
+ return now === void 0 ? void 0 : { old: join8(places.oldAgents, now), now: path };
28191
+ }
28192
+ function writeJson(path, value) {
28193
+ const next = `${path}.next`;
28194
+ writeFileSync4(next, `${JSON.stringify(value, null, 2)}
28195
+ `, { mode: 384 });
28196
+ renameSync2(next, path);
28197
+ }
28198
+ var readJson = (path) => {
28199
+ try {
28200
+ const read = JSON.parse(readFileSync6(path, "utf8"));
28201
+ return typeof read === "object" && read !== null ? read : void 0;
28202
+ } catch {
28203
+ return void 0;
28204
+ }
28205
+ };
28206
+ function moveOld(configPath2, log2, home = homedir6()) {
28207
+ const places = placesOf2(home);
28208
+ const settings = dirname5(configPath2);
28209
+ const under = inside2(places.settings, settings);
28210
+ if (under !== void 0) {
28211
+ mkdirSync5(dirname5(dirname5(places.settings)), { recursive: true });
28212
+ mkdirSync5(dirname5(places.settings), { recursive: true, mode: 448 });
28213
+ merge2(join8(places.oldSettings, under), settings, log2, home);
28214
+ }
28215
+ const config3 = readJson(configPath2);
28216
+ const said = config3?.["folders"];
28217
+ const root = typeof said === "string" && said !== "" ? resolve3(fromHome(said, home)) : places.agents;
28218
+ const pair = pairOf(root, places);
28219
+ if (pair !== void 0) merge2(pair.old, pair.now, log2, home);
28220
+ if (config3 !== void 0 && typeof said === "string") {
28221
+ const moved = nowAt(root, places.oldAgents, places.agents);
28222
+ if (moved !== root) writeJson(configPath2, { ...config3, folders: `~/${moved.slice(home.length + 1)}` });
28223
+ }
28224
+ const statePath2 = join8(settings, "sessions.json");
28225
+ const state2 = readJson(statePath2);
28226
+ if (state2 === void 0) return;
28227
+ const moving = { ...state2.moving };
28228
+ let changed = false;
28229
+ const folders = { ...state2.folders };
28230
+ for (const [who, folder2] of Object.entries(folders)) {
28231
+ const moved = nowAt(folder2, places.oldAgents, places.agents);
28232
+ if (moved === folder2) continue;
28233
+ folders[who] = moved;
28234
+ for (const [room, session] of Object.entries(state2.sessions ?? {})) {
28235
+ if (room.slice(0, room.lastIndexOf("|")) === who) moving[session] = moved;
28236
+ }
28237
+ changed = true;
28238
+ }
28239
+ const forks = { ...state2.forks };
28240
+ for (const [ask2, fork] of Object.entries(forks)) {
28241
+ if (fork.folder === void 0) continue;
28242
+ const moved = nowAt(fork.folder, places.oldAgents, places.agents);
28243
+ if (moved === fork.folder) continue;
28244
+ forks[ask2] = { ...fork, folder: moved };
28245
+ if (fork.session !== void 0) moving[fork.session] = moved;
28246
+ changed = true;
28247
+ }
28248
+ if (changed) {
28249
+ writeJson(statePath2, {
28250
+ ...state2,
28251
+ folders,
28252
+ ...state2.forks === void 0 ? {} : { forks },
28253
+ ...Object.keys(moving).length === 0 ? {} : { moving }
28254
+ });
28255
+ }
28256
+ removeIfEmpty(places.oldSettings);
28257
+ removeIfEmpty(places.oldAgents);
28258
+ removeIfEmpty(dirname5(places.oldAgents));
28259
+ }
28260
+ async function moveSessions(state2, opencode2, save, log2, home = homedir6()) {
28261
+ const moving = state2.moving;
28262
+ if (moving === void 0) return;
28263
+ for (const [session, folder2] of Object.entries(moving)) {
28264
+ try {
28265
+ await opencode2.moveSession(session, folder2);
28266
+ log2(`moved OpenCode's session ${session} to ${shown(folder2, home)}`);
28267
+ delete moving[session];
28268
+ } catch (error62) {
28269
+ if (!(error62 instanceof OpenCodeError)) {
28270
+ log2(`OpenCode's session ${session} wasn't moved to ${shown(folder2, home)}, for now: ${String(error62)}`);
28271
+ continue;
28272
+ }
28273
+ log2(`OpenCode wouldn't move its session ${session} to ${shown(folder2, home)}: ${error62.message}`);
28274
+ delete moving[session];
28275
+ }
28276
+ }
28277
+ if (Object.keys(moving).length === 0) delete state2.moving;
28278
+ save(state2);
28279
+ }
28280
+
28097
28281
  // src/server.ts
28098
28282
  import { spawn } from "node:child_process";
28099
28283
  import { randomBytes } from "node:crypto";
28100
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
28284
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
28101
28285
  import { createServer } from "node:net";
28102
- import { delimiter as delimiter2, join as join8 } from "node:path";
28286
+ import { delimiter as delimiter2, join as join9 } from "node:path";
28103
28287
  import { createInterface } from "node:readline";
28104
28288
  var ServerError = class extends Error {
28105
28289
  constructor(message, missing = false) {
@@ -28110,10 +28294,10 @@ var ServerError = class extends Error {
28110
28294
  name = "ServerError";
28111
28295
  };
28112
28296
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
28113
- var onPath = (command, env) => command.includes("/") ? existsSync5(command) : (env["PATH"] ?? "").split(delimiter2).some((folder2) => {
28297
+ var onPath = (command, env) => command.includes("/") ? existsSync6(command) : (env["PATH"] ?? "").split(delimiter2).some((folder2) => {
28114
28298
  if (folder2 === "") return false;
28115
28299
  try {
28116
- accessSync2(join8(folder2, command), constants2.X_OK);
28300
+ accessSync2(join9(folder2, command), constants2.X_OK);
28117
28301
  return true;
28118
28302
  } catch {
28119
28303
  return false;
@@ -28147,9 +28331,9 @@ async function answers2(url2, password) {
28147
28331
  return typeof info === "string" ? info : "ok";
28148
28332
  }
28149
28333
  function keptIn(path) {
28150
- if (!existsSync5(path)) return void 0;
28334
+ if (!existsSync6(path)) return void 0;
28151
28335
  try {
28152
- const kept = JSON.parse(readFileSync6(path, "utf8"));
28336
+ const kept = JSON.parse(readFileSync7(path, "utf8"));
28153
28337
  return typeof kept.port === "number" && typeof kept.password === "string" ? kept : void 0;
28154
28338
  } catch {
28155
28339
  return void 0;
@@ -28178,7 +28362,7 @@ async function ownServer(options) {
28178
28362
  const env = options.env ?? process.env;
28179
28363
  const command = options.command ?? "opencode";
28180
28364
  const log2 = options.log ?? (() => void 0);
28181
- const path = join8(options.folder, "server.json");
28365
+ const path = join9(options.folder, "server.json");
28182
28366
  let kept = keptIn(path);
28183
28367
  let before2 = false;
28184
28368
  if (kept !== void 0) {
@@ -28200,7 +28384,7 @@ async function ownServer(options) {
28200
28384
  let again2;
28201
28385
  const said = [];
28202
28386
  const start = () => {
28203
- writeFileSync4(path, `${JSON.stringify({ port, password }, null, 2)}
28387
+ writeFileSync5(path, `${JSON.stringify({ port, password }, null, 2)}
28204
28388
  `, { mode: 384 });
28205
28389
  const started = spawn(command, ["serve", "--hostname", "127.0.0.1", "--port", String(port)], {
28206
28390
  cwd: options.cwd,
@@ -28302,9 +28486,8 @@ async function ownServer(options) {
28302
28486
  }
28303
28487
 
28304
28488
  // src/setup.ts
28305
- import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
28306
- import { homedir as homedir6 } from "node:os";
28307
- import { join as join9, resolve as resolve3 } from "node:path";
28489
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
28490
+ import { resolve as resolve4 } from "node:path";
28308
28491
  var SetupError = class extends Error {
28309
28492
  constructor(message, words4 = {}) {
28310
28493
  super(message);
@@ -28321,9 +28504,8 @@ var hostOf = (url2) => {
28321
28504
  }
28322
28505
  };
28323
28506
  var STILL_GOOD = "The link is still good: run the same command again once that's done.";
28324
- var home = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
28325
28507
  function readConfig(path, env = process.env) {
28326
- const raw = existsSync6(path) ? JSON.parse(readFileSync7(path, "utf8")) : {};
28508
+ const raw = existsSync7(path) ? JSON.parse(readFileSync8(path, "utf8")) : {};
28327
28509
  const passwordEnv = typeof raw["passwordEnv"] === "string" ? raw["passwordEnv"] : "OPENCODE_PASSWORD";
28328
28510
  const password = filledIn(env[passwordEnv]) ? env[passwordEnv] : void 0;
28329
28511
  const ownServer2 = password === void 0 && typeof raw["url"] !== "string";
@@ -28350,8 +28532,8 @@ function readConfig(path, env = process.env) {
28350
28532
  password,
28351
28533
  passwordEnv,
28352
28534
  ownServer: ownServer2,
28353
- directory: typeof raw["directory"] === "string" ? resolve3(home(raw["directory"])) : void 0,
28354
- folders: resolve3(home(typeof raw["folders"] === "string" ? raw["folders"] : "~/chips-opencode/agents")),
28535
+ directory: typeof raw["directory"] === "string" ? resolve4(fromHome(raw["directory"])) : void 0,
28536
+ folders: foldersPathOf(typeof raw["folders"] === "string" ? raw["folders"] : placesOf2().agents),
28355
28537
  agents
28356
28538
  };
28357
28539
  }
@@ -28379,17 +28561,17 @@ async function checkOpenCode(config3, client) {
28379
28561
  }
28380
28562
 
28381
28563
  // src/version.ts
28382
- import { readFileSync as readFileSync8 } from "node:fs";
28383
- var VERSION = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8")).version;
28564
+ import { readFileSync as readFileSync9 } from "node:fs";
28565
+ var VERSION = JSON.parse(readFileSync9(new URL("../package.json", import.meta.url), "utf8")).version;
28384
28566
 
28385
28567
  // src/main.ts
28386
- var home2 = (path) => path.startsWith("~/") ? join10(homedir7(), path.slice(2)) : path;
28387
28568
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
28388
28569
  var LABEL = "chat.chips.opencode";
28389
28570
  stopIfAsked(LABEL);
28390
- var configPath = resolve4(home2(words3[0] ?? "~/.chips-opencode/config.json"));
28391
28571
  var log = (line) => process.stdout.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
28392
28572
  `);
28573
+ moveOld(configTargetOf(words3[0]), log);
28574
+ var configPath = configPathOf(words3[0]);
28393
28575
  var stop = (error62) => {
28394
28576
  if (!(error62 instanceof SetupError)) throw error62;
28395
28577
  reportStep("failed", error62.words);
@@ -28403,14 +28585,14 @@ try {
28403
28585
  } catch (error62) {
28404
28586
  config2 = stop(error62);
28405
28587
  }
28406
- var folder = dirname5(configPath);
28407
- mkdirSync5(folder, { recursive: true, mode: 448 });
28588
+ var folder = dirname6(configPath);
28589
+ mkdirSync6(folder, { recursive: true, mode: 448 });
28408
28590
  var statePath = join10(folder, "sessions.json");
28409
28591
  var server;
28410
28592
  if (link !== void 0) reportStep("checking", { detail: "Looking for OpenCode" });
28411
28593
  if (config2.ownServer) {
28412
- const cwd = config2.directory ?? dirname5(config2.folders);
28413
- mkdirSync5(cwd, { recursive: true });
28594
+ const cwd = config2.directory ?? config2.folders;
28595
+ mkdirSync6(cwd, { recursive: true });
28414
28596
  try {
28415
28597
  server = await ownServer({ folder, cwd, log });
28416
28598
  } catch (error62) {
@@ -28454,7 +28636,10 @@ var joined = await join3({
28454
28636
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
28455
28637
  }).catch(refused2);
28456
28638
  keepIfAsked(LABEL, joined.url, [config2.passwordEnv], log, () => server?.stop());
28457
- var state = existsSync7(statePath) ? JSON.parse(readFileSync9(statePath, "utf8")) : { sessions: {} };
28639
+ var state = existsSync8(statePath) ? JSON.parse(readFileSync10(statePath, "utf8")) : { sessions: {} };
28640
+ var saveState = (next) => writeFileSync6(statePath, `${JSON.stringify(next, null, 2)}
28641
+ `, { mode: 384 });
28642
+ if (offered !== void 0) await moveSessions(state, opencode, saveState, log);
28458
28643
  var bridge;
28459
28644
  var chips = await connect({
28460
28645
  url: joined.url,
@@ -28476,8 +28661,7 @@ bridge = new Bridge({
28476
28661
  agents: config2.agents,
28477
28662
  home: joined.spaceId,
28478
28663
  state,
28479
- saveState: (next) => writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
28480
- `, { mode: 384 }),
28664
+ saveState,
28481
28665
  log,
28482
28666
  restartServer: server?.restart,
28483
28667
  ...config2.directory === void 0 ? { folders: config2.folders } : { directory: config2.directory }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipschat/opencode",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "OpenCode as a Chips harness, on its V2 server (opencode serve): each OpenCode agent a Chips agent, each room an OpenCode session.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {