@chipschat/opencode 1.0.4 → 1.0.7
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 +95 -5
- package/dist/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/main.js +1048 -75
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -8,9 +8,9 @@ var __export = (target, all) => {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
// src/main.ts
|
|
11
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as
|
|
12
|
-
import { homedir as
|
|
13
|
-
import { dirname as
|
|
11
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, 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";
|
|
14
14
|
|
|
15
15
|
// node_modules/zod/v4/classic/external.js
|
|
16
16
|
var external_exports = {};
|
|
@@ -4871,10 +4871,10 @@ function partPattern(schema) {
|
|
|
4871
4871
|
const own2 = schema._zod.pattern?.source;
|
|
4872
4872
|
const inner = def.innerType ?? schema._zod.innerType;
|
|
4873
4873
|
if (inner) {
|
|
4874
|
-
const
|
|
4874
|
+
const before2 = inner._zod.pattern?.source;
|
|
4875
4875
|
const after = partPattern(inner);
|
|
4876
|
-
if (own2 &&
|
|
4877
|
-
return own2.replace(cleanRegex(
|
|
4876
|
+
if (own2 && before2 && after && after !== before2) {
|
|
4877
|
+
return own2.replace(cleanRegex(before2), () => cleanRegex(after));
|
|
4878
4878
|
}
|
|
4879
4879
|
return own2;
|
|
4880
4880
|
}
|
|
@@ -25467,7 +25467,270 @@ function fileStore(path) {
|
|
|
25467
25467
|
|
|
25468
25468
|
// src/bridge.ts
|
|
25469
25469
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
25470
|
-
import {
|
|
25470
|
+
import { homedir as homedir4 } from "node:os";
|
|
25471
|
+
import { join as join6 } from "node:path";
|
|
25472
|
+
|
|
25473
|
+
// src/context.ts
|
|
25474
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
25475
|
+
import { globSync, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
25476
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
25477
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative, sep as sep2 } from "node:path";
|
|
25478
|
+
var AGENTS_MD = "AGENTS.md";
|
|
25479
|
+
function placesOf(env, home3) {
|
|
25480
|
+
const config3 = env["OPENCODE_CONFIG_DIR"] || join5(env["XDG_CONFIG_HOME"] || join5(home3, ".config"), "opencode");
|
|
25481
|
+
return { home: home3, config: config3 };
|
|
25482
|
+
}
|
|
25483
|
+
function hashOf(text2) {
|
|
25484
|
+
return createHash2("sha256").update(text2, "utf8").digest("hex");
|
|
25485
|
+
}
|
|
25486
|
+
function isFile(path) {
|
|
25487
|
+
try {
|
|
25488
|
+
return statSync(path).isFile();
|
|
25489
|
+
} catch {
|
|
25490
|
+
return false;
|
|
25491
|
+
}
|
|
25492
|
+
}
|
|
25493
|
+
function textOf(path) {
|
|
25494
|
+
try {
|
|
25495
|
+
return readFileSync5(path, "utf8");
|
|
25496
|
+
} catch {
|
|
25497
|
+
return void 0;
|
|
25498
|
+
}
|
|
25499
|
+
}
|
|
25500
|
+
var within = (path, dir) => path === dir || path.startsWith(`${dir}${sep2}`);
|
|
25501
|
+
function upTo(dir, stop2) {
|
|
25502
|
+
const dirs = [dir];
|
|
25503
|
+
for (let at = dir; within(at, stop2) && at !== stop2 && dirname4(at) !== at; ) {
|
|
25504
|
+
at = dirname4(at);
|
|
25505
|
+
dirs.push(at);
|
|
25506
|
+
}
|
|
25507
|
+
return dirs;
|
|
25508
|
+
}
|
|
25509
|
+
function namedFiles(sources, folder2, root, home3) {
|
|
25510
|
+
const found = [];
|
|
25511
|
+
const glob = (pattern, cwd) => {
|
|
25512
|
+
try {
|
|
25513
|
+
return globSync(pattern, { cwd }).map((match) => join5(cwd, match));
|
|
25514
|
+
} catch {
|
|
25515
|
+
return [];
|
|
25516
|
+
}
|
|
25517
|
+
};
|
|
25518
|
+
for (const source2 of sources) {
|
|
25519
|
+
if (/^https?:\/\//i.test(source2)) continue;
|
|
25520
|
+
const expanded = source2.startsWith("~/") ? join5(home3, source2.slice(2)) : source2;
|
|
25521
|
+
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));
|
|
25522
|
+
for (const path of matches.sort()) if (isFile(path) && !found.includes(path)) found.push(path);
|
|
25523
|
+
}
|
|
25524
|
+
return found;
|
|
25525
|
+
}
|
|
25526
|
+
function before(version3, than) {
|
|
25527
|
+
const parts = (each) => each.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
25528
|
+
const [a, b] = [parts(version3), parts(than)];
|
|
25529
|
+
for (let at = 0; at < 3; at += 1) if ((a[at] ?? 0) !== (b[at] ?? 0)) return (a[at] ?? 0) < (b[at] ?? 0);
|
|
25530
|
+
return false;
|
|
25531
|
+
}
|
|
25532
|
+
var READS_NAMED = "2.0.30";
|
|
25533
|
+
var ContextFiles = class {
|
|
25534
|
+
constructor(places, folders) {
|
|
25535
|
+
this.places = places;
|
|
25536
|
+
this.folders = folders;
|
|
25537
|
+
}
|
|
25538
|
+
places;
|
|
25539
|
+
folders;
|
|
25540
|
+
/** `~/chips-opencode/agents/AGENTS.md`, or the whole path where it isn't in the home. */
|
|
25541
|
+
tilde(path) {
|
|
25542
|
+
const { home: home3 } = this.places;
|
|
25543
|
+
return within(path, home3) ? `~${path.slice(home3.length)}` : path;
|
|
25544
|
+
}
|
|
25545
|
+
/** A shared file's name: where it is, a step at a time (`~ › .config › opencode › AGENTS.md`), with no slash. */
|
|
25546
|
+
crumbs(path) {
|
|
25547
|
+
const { home: home3 } = this.places;
|
|
25548
|
+
const steps = within(path, home3) ? ["~", ...relative(home3, path).split(sep2)] : ["/", ...path.split(sep2)];
|
|
25549
|
+
return steps.filter((step) => step !== "").join(" \u203A ").slice(0, 255);
|
|
25550
|
+
}
|
|
25551
|
+
/**
|
|
25552
|
+
* The harness's own set, for agents whose folders read as `readings` says: every agent's
|
|
25553
|
+
* `AGENTS.md`, those above it, the person's own, and the named files not in an agent's folder.
|
|
25554
|
+
*/
|
|
25555
|
+
sharedSpots(readings) {
|
|
25556
|
+
const { places, folders } = this;
|
|
25557
|
+
const spots = [];
|
|
25558
|
+
const add = (spot) => {
|
|
25559
|
+
if (!spots.some((each) => each.path === spot.path)) spots.push(spot);
|
|
25560
|
+
};
|
|
25561
|
+
const reaches = (dir) => (folder2) => within(folder2, dir) && within(dir, readings.get(folder2)?.stop ?? folder2);
|
|
25562
|
+
if (folders !== void 0 && within(folders, places.home)) {
|
|
25563
|
+
const path = join5(folders, AGENTS_MD);
|
|
25564
|
+
add({
|
|
25565
|
+
path,
|
|
25566
|
+
name: this.crumbs(path),
|
|
25567
|
+
title: "For every OpenCode agent",
|
|
25568
|
+
about: this.tilde(path),
|
|
25569
|
+
editable: true,
|
|
25570
|
+
id: "every-agent",
|
|
25571
|
+
over: reaches(folders)
|
|
25572
|
+
});
|
|
25573
|
+
}
|
|
25574
|
+
const yours = join5(places.config, AGENTS_MD);
|
|
25575
|
+
for (const [folder2, reading] of readings) {
|
|
25576
|
+
for (const dir of upTo(folder2, reading.stop).slice(1)) {
|
|
25577
|
+
const path = join5(dir, AGENTS_MD);
|
|
25578
|
+
if (path === yours || !isFile(path)) continue;
|
|
25579
|
+
add({
|
|
25580
|
+
path,
|
|
25581
|
+
name: this.crumbs(path),
|
|
25582
|
+
title: `From ${this.tilde(dir)}`.slice(0, 80),
|
|
25583
|
+
about: this.tilde(path),
|
|
25584
|
+
editable: false,
|
|
25585
|
+
whyReadOnly: "It sits above the agents' folders.",
|
|
25586
|
+
id: `above-${hashOf(path).slice(0, 12)}`,
|
|
25587
|
+
over: reaches(dir)
|
|
25588
|
+
});
|
|
25589
|
+
}
|
|
25590
|
+
}
|
|
25591
|
+
if (isFile(yours)) {
|
|
25592
|
+
add({
|
|
25593
|
+
path: yours,
|
|
25594
|
+
name: this.crumbs(yours),
|
|
25595
|
+
title: "For all your OpenCode",
|
|
25596
|
+
about: this.tilde(yours),
|
|
25597
|
+
editable: false,
|
|
25598
|
+
whyReadOnly: "Your own OpenCode reads it too.",
|
|
25599
|
+
id: "yours"
|
|
25600
|
+
});
|
|
25601
|
+
}
|
|
25602
|
+
for (const [folder2, reading] of readings) {
|
|
25603
|
+
for (const path of reading.named) {
|
|
25604
|
+
if (within(path, folder2)) continue;
|
|
25605
|
+
add({
|
|
25606
|
+
path,
|
|
25607
|
+
name: this.crumbs(path),
|
|
25608
|
+
title: "Named in OpenCode's config",
|
|
25609
|
+
about: this.tilde(path),
|
|
25610
|
+
editable: false,
|
|
25611
|
+
whyReadOnly: "OpenCode's config names it for every session.",
|
|
25612
|
+
id: `named-${hashOf(path).slice(0, 12)}`,
|
|
25613
|
+
over: (other) => readings.get(other)?.named.includes(path) === true
|
|
25614
|
+
});
|
|
25615
|
+
}
|
|
25616
|
+
}
|
|
25617
|
+
return spots;
|
|
25618
|
+
}
|
|
25619
|
+
/** An agent's own files: its folder's `AGENTS.md`, and the named files in its folder. */
|
|
25620
|
+
ownSpots(folder2, reading) {
|
|
25621
|
+
const named = reading.named.filter((path) => within(path, folder2) && path !== join5(folder2, AGENTS_MD));
|
|
25622
|
+
return [
|
|
25623
|
+
{ path: join5(folder2, AGENTS_MD), name: AGENTS_MD, title: "How it works", editable: true },
|
|
25624
|
+
...named.map((path) => ({
|
|
25625
|
+
path,
|
|
25626
|
+
name: relative(folder2, path).split(sep2).join(" \u203A ").slice(0, 255),
|
|
25627
|
+
title: "Named in OpenCode's config",
|
|
25628
|
+
editable: true
|
|
25629
|
+
}))
|
|
25630
|
+
];
|
|
25631
|
+
}
|
|
25632
|
+
/** The shared files an agent in `folder` reads. */
|
|
25633
|
+
readBy(folder2, reading) {
|
|
25634
|
+
return this.sharedSpots(/* @__PURE__ */ new Map([[folder2, reading]])).filter((spot) => spot.over === void 0 || spot.over(folder2));
|
|
25635
|
+
}
|
|
25636
|
+
/** An agent's copy: its own files and the shared ones it reads, eight at most. */
|
|
25637
|
+
agentCopy(folder2, reading) {
|
|
25638
|
+
const rows = [];
|
|
25639
|
+
const notShown = [];
|
|
25640
|
+
const add = (row, text2) => {
|
|
25641
|
+
const named = { name: row["name"], title: row["title"] };
|
|
25642
|
+
if (text2 !== void 0 && contextBytes(text2) > CONTEXT_FILE_BYTES) notShown.push({ ...named, reason: "too_large" });
|
|
25643
|
+
else if (rows.length >= CONTEXT_FILES_SHOWN) notShown.push({ ...named, reason: "count" });
|
|
25644
|
+
else rows.push(row);
|
|
25645
|
+
};
|
|
25646
|
+
for (const spot of this.ownSpots(folder2, reading)) {
|
|
25647
|
+
const text2 = textOf(spot.path) ?? "";
|
|
25648
|
+
add({ name: spot.name, title: spot.title, role: "instructions", text: text2, hash: hashOf(text2), editable: true }, text2);
|
|
25649
|
+
}
|
|
25650
|
+
for (const spot of this.readBy(folder2, reading)) {
|
|
25651
|
+
add({
|
|
25652
|
+
name: spot.name,
|
|
25653
|
+
title: spot.title,
|
|
25654
|
+
role: "instructions",
|
|
25655
|
+
...spot.about === void 0 ? {} : { about: spot.about },
|
|
25656
|
+
shared: spot.id,
|
|
25657
|
+
replaceable: false,
|
|
25658
|
+
editable: false
|
|
25659
|
+
});
|
|
25660
|
+
}
|
|
25661
|
+
return { files: rows, ...notShown.length === 0 ? {} : { not_shown: notShown } };
|
|
25662
|
+
}
|
|
25663
|
+
/** The harness's own set, each file with how many of its agents (by their folders' readings) read it. */
|
|
25664
|
+
harnessCopy(readings, agentFolders) {
|
|
25665
|
+
const shared = [];
|
|
25666
|
+
const notShown = [];
|
|
25667
|
+
for (const spot of this.sharedSpots(readings)) {
|
|
25668
|
+
const text2 = textOf(spot.path) ?? "";
|
|
25669
|
+
const named = { name: spot.name, title: spot.title };
|
|
25670
|
+
if (contextBytes(text2) > CONTEXT_FILE_BYTES) {
|
|
25671
|
+
notShown.push({ ...named, reason: "too_large" });
|
|
25672
|
+
continue;
|
|
25673
|
+
}
|
|
25674
|
+
if (shared.length >= CONTEXT_FILES_SHOWN) {
|
|
25675
|
+
notShown.push({ ...named, reason: "count" });
|
|
25676
|
+
continue;
|
|
25677
|
+
}
|
|
25678
|
+
const over = spot.over;
|
|
25679
|
+
shared.push({
|
|
25680
|
+
id: spot.id,
|
|
25681
|
+
name: spot.name,
|
|
25682
|
+
title: spot.title,
|
|
25683
|
+
role: "instructions",
|
|
25684
|
+
...spot.about === void 0 ? {} : { about: spot.about },
|
|
25685
|
+
text: text2,
|
|
25686
|
+
hash: hashOf(text2),
|
|
25687
|
+
editable: spot.editable,
|
|
25688
|
+
...spot.whyReadOnly === void 0 ? {} : { why_read_only: spot.whyReadOnly },
|
|
25689
|
+
used_by: over === void 0 ? agentFolders.length : agentFolders.filter(over).length
|
|
25690
|
+
});
|
|
25691
|
+
}
|
|
25692
|
+
return { shared, ...notShown.length === 0 ? {} : { not_shown: notShown } };
|
|
25693
|
+
}
|
|
25694
|
+
/**
|
|
25695
|
+
* A person's edit of one of an agent's files, saved where OpenCode reads it, only if it is still
|
|
25696
|
+
* the version they changed. Refused, with the copy as it is, for a file that isn't the agent's own
|
|
25697
|
+
* to change, and as `changed` for one changed since.
|
|
25698
|
+
*/
|
|
25699
|
+
async saveAgent(folder2, reading, edit) {
|
|
25700
|
+
const now = () => this.agentCopy(folder2, reading);
|
|
25701
|
+
const spot = this.ownSpots(folder2, reading).find((each) => each.name === edit.name);
|
|
25702
|
+
if (spot === void 0) {
|
|
25703
|
+
const shared = this.readBy(folder2, reading).some((each) => each.name === edit.name);
|
|
25704
|
+
throw new Refuse(
|
|
25705
|
+
shared ? "Every agent reads this one. Change it on OpenCode's page." : `There is no file called ${edit.name} here.`,
|
|
25706
|
+
now(),
|
|
25707
|
+
"failed"
|
|
25708
|
+
);
|
|
25709
|
+
}
|
|
25710
|
+
if (edit.base_hash === null) throw new Refuse("This file is the agent's own already.", now(), "failed");
|
|
25711
|
+
await this.write(spot, edit, now);
|
|
25712
|
+
}
|
|
25713
|
+
/** A person's edit of one of the harness's own files, read by its agents beside their own. */
|
|
25714
|
+
async saveShared(readings, agentFolders, edit) {
|
|
25715
|
+
const now = () => this.harnessCopy(readings, agentFolders);
|
|
25716
|
+
const spot = this.sharedSpots(readings).find((each) => each.name === edit.name);
|
|
25717
|
+
if (spot === void 0) throw new Refuse(`There is no file called ${edit.name} here.`, now(), "failed");
|
|
25718
|
+
if (!spot.editable) throw new Refuse(spot.whyReadOnly ?? "It can't be changed from Chips.", now(), "failed");
|
|
25719
|
+
if (edit.base_hash === null) {
|
|
25720
|
+
throw new Refuse("Every agent reads this file beside its own, so none can have its own in its place.", now(), "failed");
|
|
25721
|
+
}
|
|
25722
|
+
await this.write(spot, edit, now);
|
|
25723
|
+
}
|
|
25724
|
+
async write(spot, edit, now) {
|
|
25725
|
+
if (hashOf(textOf(spot.path) ?? "") !== edit.base_hash) throw new Refuse("", now(), "changed");
|
|
25726
|
+
try {
|
|
25727
|
+
await mkdir2(dirname4(spot.path), { recursive: true });
|
|
25728
|
+
await writeFile2(spot.path, edit.text, "utf8");
|
|
25729
|
+
} catch (error62) {
|
|
25730
|
+
throw new Refuse(`It couldn't be saved: ${error62 instanceof Error ? error62.message : String(error62)}`.slice(0, 300), now(), "failed");
|
|
25731
|
+
}
|
|
25732
|
+
}
|
|
25733
|
+
};
|
|
25471
25734
|
|
|
25472
25735
|
// src/opencode.ts
|
|
25473
25736
|
var OpenCodeError = class extends Error {
|
|
@@ -25507,12 +25770,47 @@ var OpenCodeClient = class {
|
|
|
25507
25770
|
return false;
|
|
25508
25771
|
}
|
|
25509
25772
|
}
|
|
25773
|
+
/** Which OpenCode answers: `GET /api/info`'s `version`, `2.0.25`. */
|
|
25774
|
+
async version() {
|
|
25775
|
+
const info = await this.call("GET", "/api/info");
|
|
25776
|
+
return typeof info["version"] === "string" ? info["version"] : "";
|
|
25777
|
+
}
|
|
25778
|
+
/** The root of the project a folder is in, as OpenCode has it: the folder itself, outside any repository. */
|
|
25779
|
+
async projectRoot(directory) {
|
|
25780
|
+
const read = await this.call("GET", `/api/location${whereIs(directory)}`);
|
|
25781
|
+
const project = read["project"] ?? {};
|
|
25782
|
+
return typeof project["directory"] === "string" ? project["directory"] : directory;
|
|
25783
|
+
}
|
|
25784
|
+
/**
|
|
25785
|
+
* The instruction files the config documents for a folder name (`instructions`), each document's
|
|
25786
|
+
* in turn, each named once: a path, a glob or a URL, as written.
|
|
25787
|
+
*/
|
|
25788
|
+
async instructions(directory) {
|
|
25789
|
+
const answer = await this.fetcher(`${this.root}/api/config${whereIs(directory)}`, { headers: { authorization: this.auth } });
|
|
25790
|
+
if (!answer.ok) throw await failureOf(answer);
|
|
25791
|
+
const entries = await answer.json();
|
|
25792
|
+
const named = (Array.isArray(entries) ? entries : []).filter((entry) => entry["type"] === "document").flatMap((entry) => (entry["info"] ?? {})["instructions"]).filter((each) => typeof each === "string");
|
|
25793
|
+
return [...new Set(named)];
|
|
25794
|
+
}
|
|
25510
25795
|
async agents() {
|
|
25511
25796
|
return await this.catalogue("agent");
|
|
25512
25797
|
}
|
|
25513
25798
|
async models() {
|
|
25514
25799
|
return await this.catalogue("model");
|
|
25515
25800
|
}
|
|
25801
|
+
/** The model a session with none of its own runs on, when the server has one. */
|
|
25802
|
+
async defaultModel() {
|
|
25803
|
+
const read = await this.call("GET", `/api/model/default${this.where()}`);
|
|
25804
|
+
const data = read["data"];
|
|
25805
|
+
return typeof data === "object" && data !== null ? data : void 0;
|
|
25806
|
+
}
|
|
25807
|
+
/**
|
|
25808
|
+
* The model a session runs on from its next model call. OpenCode takes any model and level
|
|
25809
|
+
* here, one it can't run among them, so the bridge asks only for those of its catalogue.
|
|
25810
|
+
*/
|
|
25811
|
+
async setModel(sessionId, model) {
|
|
25812
|
+
await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/model`, { model });
|
|
25813
|
+
}
|
|
25516
25814
|
/**
|
|
25517
25815
|
* A project's catalogue. OpenCode 2.0 answers the first ask for a project it hasn't loaded with
|
|
25518
25816
|
* an empty list, and the next with the whole one: a server always has its `build` agent, so an
|
|
@@ -25577,6 +25875,13 @@ var OpenCodeClient = class {
|
|
|
25577
25875
|
if (typeof id2 !== "string") throw new OpenCodeError("OpenCode took the words and named no inbox item", 502);
|
|
25578
25876
|
return id2;
|
|
25579
25877
|
}
|
|
25878
|
+
/**
|
|
25879
|
+
* Compact a session: its inbox takes the ask, which starts at once when it is idle and at its next
|
|
25880
|
+
* step when it is working. `session.compaction.started` and `.ended` or `.failed` say how it goes.
|
|
25881
|
+
*/
|
|
25882
|
+
async compact(sessionId) {
|
|
25883
|
+
await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/compact`, {});
|
|
25884
|
+
}
|
|
25580
25885
|
/** Words the session hasn't taken yet, taken back. */
|
|
25581
25886
|
async withdraw(sessionId, item) {
|
|
25582
25887
|
await this.call("DELETE", `/api/session/${encodeURIComponent(sessionId)}/inbox/${encodeURIComponent(item)}`);
|
|
@@ -25626,6 +25931,22 @@ var OpenCodeClient = class {
|
|
|
25626
25931
|
const content = Array.isArray(newest?.["content"]) ? newest["content"] : [];
|
|
25627
25932
|
return content.filter((part) => part["type"] === "text" && typeof part["text"] === "string").map((part) => String(part["text"])).join("\n\n");
|
|
25628
25933
|
}
|
|
25934
|
+
/** The tokens and model of a session's newest model call, and when: nothing for a session with none. */
|
|
25935
|
+
async lastCall(sessionId) {
|
|
25936
|
+
const read = await this.call(
|
|
25937
|
+
"GET",
|
|
25938
|
+
`/api/session/${encodeURIComponent(sessionId)}/message?type=assistant&order=desc&limit=1`
|
|
25939
|
+
);
|
|
25940
|
+
const newest = (Array.isArray(read["data"]) ? read["data"] : [])[0];
|
|
25941
|
+
const tokens2 = newest?.["tokens"];
|
|
25942
|
+
if (typeof tokens2 !== "object" || tokens2 === null) return void 0;
|
|
25943
|
+
const time3 = newest?.["time"] ?? {};
|
|
25944
|
+
return {
|
|
25945
|
+
tokens: tokens2,
|
|
25946
|
+
...typeof newest?.["model"] === "object" && newest["model"] !== null ? { model: newest["model"] } : {},
|
|
25947
|
+
at: typeof time3["created"] === "number" ? time3["created"] : 0
|
|
25948
|
+
};
|
|
25949
|
+
}
|
|
25629
25950
|
/** Every event the server sends, each handed to `hear` as it comes, until the stream ends. */
|
|
25630
25951
|
async events(hear, signal) {
|
|
25631
25952
|
const answer = await this.fetcher(`${this.root}/api/event`, {
|
|
@@ -25649,7 +25970,7 @@ var OpenCodeClient = class {
|
|
|
25649
25970
|
/** `?location[directory]=…` for the catalogues, which are per project. */
|
|
25650
25971
|
where() {
|
|
25651
25972
|
const directory = this.address.directory;
|
|
25652
|
-
return directory === void 0 ? "" :
|
|
25973
|
+
return directory === void 0 ? "" : whereIs(directory);
|
|
25653
25974
|
}
|
|
25654
25975
|
async call(method, path, body) {
|
|
25655
25976
|
const answer = await this.fetcher(`${this.root}${path}`, {
|
|
@@ -25665,6 +25986,9 @@ var OpenCodeClient = class {
|
|
|
25665
25986
|
return text2 === "" ? {} : JSON.parse(text2);
|
|
25666
25987
|
}
|
|
25667
25988
|
};
|
|
25989
|
+
function whereIs(directory) {
|
|
25990
|
+
return `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
25991
|
+
}
|
|
25668
25992
|
function eventOf(block) {
|
|
25669
25993
|
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
25670
25994
|
if (data === "") return void 0;
|
|
@@ -25696,18 +26020,138 @@ async function failureOf(answer) {
|
|
|
25696
26020
|
return new OpenCodeError(message, answer.status, tag, kind);
|
|
25697
26021
|
}
|
|
25698
26022
|
|
|
26023
|
+
// src/models.ts
|
|
26024
|
+
var MODEL_CHOICES_MAX2 = 40;
|
|
26025
|
+
var EFFORT_LEVELS_MAX2 = 8;
|
|
26026
|
+
var LEVEL_WORDS = {
|
|
26027
|
+
none: "None",
|
|
26028
|
+
minimal: "Minimal",
|
|
26029
|
+
low: "Low",
|
|
26030
|
+
medium: "Medium",
|
|
26031
|
+
high: "High",
|
|
26032
|
+
xhigh: "Extra high",
|
|
26033
|
+
max: "Max",
|
|
26034
|
+
thinking: "Thinking"
|
|
26035
|
+
};
|
|
26036
|
+
function writtenOf(model) {
|
|
26037
|
+
return `${model.providerID}/${model.id}`;
|
|
26038
|
+
}
|
|
26039
|
+
function refOf(pick2) {
|
|
26040
|
+
return { ...modelRefOf(pick2.model), ...pick2.effort === void 0 ? {} : { variant: pick2.effort } };
|
|
26041
|
+
}
|
|
26042
|
+
function levelOf(model) {
|
|
26043
|
+
return model?.variant === void 0 || model.variant === "default" ? void 0 : model.variant;
|
|
26044
|
+
}
|
|
26045
|
+
function sameModel(a, b) {
|
|
26046
|
+
return a?.providerID === b?.providerID && a?.id === b?.id && levelOf(a) === levelOf(b);
|
|
26047
|
+
}
|
|
26048
|
+
function levelWords(id2) {
|
|
26049
|
+
return (LEVEL_WORDS[id2] ?? `${id2.charAt(0).toUpperCase()}${id2.slice(1)}`).slice(0, 40);
|
|
26050
|
+
}
|
|
26051
|
+
function runnable(catalogue) {
|
|
26052
|
+
return catalogue.filter((model) => model.enabled !== false && model.status !== "deprecated");
|
|
26053
|
+
}
|
|
26054
|
+
function levelsOf(model) {
|
|
26055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26056
|
+
const each = (model?.variants ?? []).filter((variant) => !seen.has(variant.id) && seen.add(variant.id));
|
|
26057
|
+
return each.slice(-EFFORT_LEVELS_MAX2).map((variant) => ({ id: variant.id, label: levelWords(variant.id) }));
|
|
26058
|
+
}
|
|
26059
|
+
var fitting = (choice, effort) => choice?.efforts.some((level) => level.id === effort) === true ? effort : void 0;
|
|
26060
|
+
function modelCopy(input2) {
|
|
26061
|
+
const { pick: pick2, fallback } = input2;
|
|
26062
|
+
const runsOn = pick2 === void 0 ? fallback?.model : refOf(pick2);
|
|
26063
|
+
if (runsOn === void 0) return void 0;
|
|
26064
|
+
const offered2 = runnable(input2.catalogue);
|
|
26065
|
+
const names = /* @__PURE__ */ new Map();
|
|
26066
|
+
for (const model of offered2) names.set(model.name, (names.get(model.name) ?? 0) + 1);
|
|
26067
|
+
const choices = offered2.map((model) => ({
|
|
26068
|
+
id: writtenOf(model),
|
|
26069
|
+
label: ((names.get(model.name) ?? 0) > 1 ? `${model.name} \xB7 ${model.providerID}` : model.name || model.id).slice(0, 80),
|
|
26070
|
+
efforts: levelsOf(model)
|
|
26071
|
+
}));
|
|
26072
|
+
const named = (ref) => {
|
|
26073
|
+
const id2 = writtenOf(ref);
|
|
26074
|
+
const known = choices.find((choice2) => choice2.id === id2);
|
|
26075
|
+
if (known !== void 0) return known;
|
|
26076
|
+
const level = levelOf(ref);
|
|
26077
|
+
const choice = { id: id2, label: ref.id.slice(0, 80), efforts: level === void 0 ? [] : [{ id: level, label: levelWords(level) }] };
|
|
26078
|
+
choices.push(choice);
|
|
26079
|
+
return choice;
|
|
26080
|
+
};
|
|
26081
|
+
const now = named(runsOn);
|
|
26082
|
+
const otherwise = fallback === void 0 ? void 0 : named(fallback.model);
|
|
26083
|
+
const effort = fitting(now, levelOf(runsOn));
|
|
26084
|
+
const fallbackEffort = fitting(otherwise, levelOf(fallback?.model));
|
|
26085
|
+
const keep = /* @__PURE__ */ new Set([now.id, ...otherwise === void 0 ? [] : [otherwise.id]]);
|
|
26086
|
+
let others = MODEL_CHOICES_MAX2 - keep.size;
|
|
26087
|
+
return {
|
|
26088
|
+
model: now.id,
|
|
26089
|
+
...effort === void 0 ? {} : { effort },
|
|
26090
|
+
own: pick2 !== void 0,
|
|
26091
|
+
...fallback === void 0 || otherwise === void 0 ? {} : {
|
|
26092
|
+
default: {
|
|
26093
|
+
label: fallback.label.slice(0, 80),
|
|
26094
|
+
model: otherwise.id,
|
|
26095
|
+
...fallbackEffort === void 0 ? {} : { effort: fallbackEffort }
|
|
26096
|
+
}
|
|
26097
|
+
},
|
|
26098
|
+
choices: choices.filter((choice) => keep.has(choice.id) || others-- > 0),
|
|
26099
|
+
editable: true
|
|
26100
|
+
};
|
|
26101
|
+
}
|
|
26102
|
+
function pickOf(change, catalogue) {
|
|
26103
|
+
if (change.model === null) return null;
|
|
26104
|
+
const model = runnable(catalogue).find((each) => writtenOf(each) === change.model);
|
|
26105
|
+
if (model === void 0) throw new Error(`OpenCode doesn't run ${change.model} here.`);
|
|
26106
|
+
if (change.effort === null) return { model: change.model };
|
|
26107
|
+
if (!(model.variants ?? []).some((variant) => variant.id === change.effort)) {
|
|
26108
|
+
throw new Error(`${model.name || model.id} has no ${levelWords(change.effort)} level.`);
|
|
26109
|
+
}
|
|
26110
|
+
return { model: change.model, effort: change.effort };
|
|
26111
|
+
}
|
|
26112
|
+
|
|
26113
|
+
// src/windows.ts
|
|
26114
|
+
var OWN_LIMIT = "OpenCode compacts it itself when it is nearly full, as its config says.";
|
|
26115
|
+
function usedOf(tokens2) {
|
|
26116
|
+
return tokens2.input + tokens2.cache.read + tokens2.cache.write + tokens2.output + tokens2.reasoning;
|
|
26117
|
+
}
|
|
26118
|
+
function compactedTo(data) {
|
|
26119
|
+
const tokens2 = data["tokens"];
|
|
26120
|
+
const summary = typeof tokens2?.output === "number" ? tokens2.output : 0;
|
|
26121
|
+
const recent = typeof data["recent"] === "string" ? data["recent"] : "";
|
|
26122
|
+
return Math.round(summary + recent.length / 4);
|
|
26123
|
+
}
|
|
26124
|
+
function windowCopy(kept, compacting) {
|
|
26125
|
+
return {
|
|
26126
|
+
used: Math.max(0, Math.round(kept.used)),
|
|
26127
|
+
window: Math.max(1, Math.round(kept.window)),
|
|
26128
|
+
...kept.model === void 0 ? {} : { model: kept.model.slice(0, 120) },
|
|
26129
|
+
as_of: kept.asOf,
|
|
26130
|
+
...compacting === void 0 ? {} : { compacting },
|
|
26131
|
+
...kept.last === void 0 ? {} : { last: kept.last },
|
|
26132
|
+
...kept.longTurn === void 0 ? {} : { long_turn: { ...kept.longTurn, folds: Math.min(kept.longTurn.folds, LONG_TURN_FOLDS_MAX) } },
|
|
26133
|
+
compacts: true,
|
|
26134
|
+
editable: false,
|
|
26135
|
+
why_read_only: OWN_LIMIT
|
|
26136
|
+
};
|
|
26137
|
+
}
|
|
26138
|
+
|
|
25699
26139
|
// src/bridge.ts
|
|
25700
|
-
var WANTS = ["agents", "new_agents", "forks"];
|
|
26140
|
+
var WANTS = ["agents", "new_spaces", "new_agents", "forks", "models", "context_files", "context_windows"];
|
|
26141
|
+
function wantsFor(ownServer2) {
|
|
26142
|
+
return ownServer2 ? [...WANTS, "restarts"] : WANTS;
|
|
26143
|
+
}
|
|
25701
26144
|
function freeFolder(root, name, taken) {
|
|
25702
26145
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
25703
26146
|
for (let count = 1; ; count += 1) {
|
|
25704
|
-
const folder2 =
|
|
26147
|
+
const folder2 = join6(root, count === 1 ? slug : `${slug}-${count}`);
|
|
25705
26148
|
if (!taken.includes(folder2)) return folder2;
|
|
25706
26149
|
}
|
|
25707
26150
|
}
|
|
25708
26151
|
var ids = createIdGenerator();
|
|
25709
26152
|
var QUIET_MS = 1500;
|
|
25710
26153
|
var INTERRUPTING = "interrupting";
|
|
26154
|
+
var TROUBLE_CHARS = 200;
|
|
25711
26155
|
var LOOKS_KEPT = 500;
|
|
25712
26156
|
var REPLY_CHARS = 1500;
|
|
25713
26157
|
var KINDS = {
|
|
@@ -25731,6 +26175,9 @@ var Bridge = class {
|
|
|
25731
26175
|
constructor(options) {
|
|
25732
26176
|
this.options = options;
|
|
25733
26177
|
this.log = options.log ?? (() => void 0);
|
|
26178
|
+
this.places = options.places ?? placesOf(process.env, homedir4());
|
|
26179
|
+
const known = options.folders !== void 0 || options.directory !== void 0;
|
|
26180
|
+
this.files = known ? new ContextFiles(this.places, options.folders) : void 0;
|
|
25734
26181
|
}
|
|
25735
26182
|
options;
|
|
25736
26183
|
agents = [];
|
|
@@ -25760,6 +26207,21 @@ var Bridge = class {
|
|
|
25760
26207
|
health;
|
|
25761
26208
|
listening = new AbortController();
|
|
25762
26209
|
connections = 0;
|
|
26210
|
+
/** Opens the event stream again now, rather than once its wait is over. */
|
|
26211
|
+
wake;
|
|
26212
|
+
/** A person's Restart under way: nothing is said of how it is meanwhile (Chips ADR 0094). */
|
|
26213
|
+
restarting;
|
|
26214
|
+
/** Why OpenCode's server isn't there, where the bridge knows more than that it doesn't answer. */
|
|
26215
|
+
why;
|
|
26216
|
+
/** The files OpenCode builds each agent from, where the bridge knows each agent's folder. */
|
|
26217
|
+
files;
|
|
26218
|
+
places;
|
|
26219
|
+
/** Each folder's project root, as OpenCode said it. */
|
|
26220
|
+
roots = /* @__PURE__ */ new Map();
|
|
26221
|
+
/** Whether the server reads the files its config's `instructions` names, asked once a start or a restart. */
|
|
26222
|
+
readsNamed;
|
|
26223
|
+
/** Agents compacting now, by whose sessions they are: waiting for their session's next step, or under way, and the ask from Chips. */
|
|
26224
|
+
compacting = /* @__PURE__ */ new Map();
|
|
25763
26225
|
/** Register the agents, start hearing OpenCode and what is said in Chips, and say how it is. */
|
|
25764
26226
|
async start() {
|
|
25765
26227
|
const { chips: chips2 } = this.options;
|
|
@@ -25779,9 +26241,17 @@ var Bridge = class {
|
|
|
25779
26241
|
chips2.on("agent.fork", (fork) => {
|
|
25780
26242
|
this.copy(fork).catch((error62) => this.log(`a copy went wrong: ${messageOf(error62)}`));
|
|
25781
26243
|
});
|
|
26244
|
+
chips2.onSpaceAsked((asked) => {
|
|
26245
|
+
this.claim(asked).catch((error62) => this.log(`a space from the + went wrong: ${messageOf(error62)}`));
|
|
26246
|
+
});
|
|
25782
26247
|
chips2.on("feedback", (feedback2) => {
|
|
25783
26248
|
this.notRight(feedback2).catch((error62) => this.log(`a second look went wrong: ${messageOf(error62)}`));
|
|
25784
26249
|
});
|
|
26250
|
+
for (const agent of await chips2.agents.list().catch(() => [])) {
|
|
26251
|
+
if (agent.state !== "archived") await this.sendModel(agent.agentId);
|
|
26252
|
+
}
|
|
26253
|
+
await this.sendContexts();
|
|
26254
|
+
await this.sendWindows();
|
|
25785
26255
|
await this.sayHowItIs();
|
|
25786
26256
|
this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
|
|
25787
26257
|
this.health.unref();
|
|
@@ -25790,6 +26260,38 @@ var Bridge = class {
|
|
|
25790
26260
|
if (this.health !== void 0) clearInterval(this.health);
|
|
25791
26261
|
this.listening.abort();
|
|
25792
26262
|
}
|
|
26263
|
+
/**
|
|
26264
|
+
* A person's Restart (Chips ADR 0094): the server stopped and started again, then how it is,
|
|
26265
|
+
* fine or why not. Asked again meanwhile, it is the same restart. Nothing where the bridge
|
|
26266
|
+
* doesn't run the server, which never says it hears restarts.
|
|
26267
|
+
*/
|
|
26268
|
+
restart() {
|
|
26269
|
+
this.restarting ??= (async () => {
|
|
26270
|
+
try {
|
|
26271
|
+
await this.restartServer();
|
|
26272
|
+
} finally {
|
|
26273
|
+
this.restarting = void 0;
|
|
26274
|
+
}
|
|
26275
|
+
await this.sayHowItIs();
|
|
26276
|
+
})();
|
|
26277
|
+
return this.restarting;
|
|
26278
|
+
}
|
|
26279
|
+
async restartServer() {
|
|
26280
|
+
const again2 = this.options.restartServer;
|
|
26281
|
+
if (again2 === void 0) return;
|
|
26282
|
+
this.log("a person asked for a restart");
|
|
26283
|
+
try {
|
|
26284
|
+
await again2();
|
|
26285
|
+
this.why = void 0;
|
|
26286
|
+
this.readsNamed = void 0;
|
|
26287
|
+
this.wake?.();
|
|
26288
|
+
} catch (error62) {
|
|
26289
|
+
const why = messageOf(error62).replace(/\s+/g, " ").trim();
|
|
26290
|
+
this.why = why.length <= TROUBLE_CHARS ? why : `${why.slice(0, TROUBLE_CHARS - 1)}\u2026`;
|
|
26291
|
+
this.log(`OpenCode's server isn't back: ${why}`);
|
|
26292
|
+
for (const turn of this.turns.values()) turn.end({ outcome: "failed", why: "OpenCode didn't come back after a restart" });
|
|
26293
|
+
}
|
|
26294
|
+
}
|
|
25793
26295
|
/** The Main agent, the first registered, once started: who says the harness's first words. */
|
|
25794
26296
|
get mainAgentId() {
|
|
25795
26297
|
return this.agents[0]?.agentId;
|
|
@@ -25803,16 +26305,16 @@ var Bridge = class {
|
|
|
25803
26305
|
*/
|
|
25804
26306
|
async seatMain(config3, known) {
|
|
25805
26307
|
const { chips: chips2 } = this.options;
|
|
25806
|
-
const
|
|
26308
|
+
const before2 = known.find((agent) => agent.lead);
|
|
25807
26309
|
const made = known.some((agent) => agent.key === chips2.harnessId);
|
|
25808
|
-
if (!made &&
|
|
25809
|
-
await this.goHome(
|
|
25810
|
-
return
|
|
26310
|
+
if (!made && before2 !== void 0) {
|
|
26311
|
+
await this.goHome(before2, known);
|
|
26312
|
+
return before2;
|
|
25811
26313
|
}
|
|
25812
26314
|
const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
|
|
25813
26315
|
await this.goHome(main2, known);
|
|
25814
|
-
if (
|
|
25815
|
-
await chips2.agents.register({ key:
|
|
26316
|
+
if (before2 !== void 0 && before2.agentId !== main2.agentId) {
|
|
26317
|
+
await chips2.agents.register({ key: before2.key, name: before2.name, state: "archived" }).catch((error62) => this.log(`${before2.name} stays: ${String(error62)}`));
|
|
25816
26318
|
}
|
|
25817
26319
|
return main2;
|
|
25818
26320
|
}
|
|
@@ -25823,11 +26325,17 @@ var Bridge = class {
|
|
|
25823
26325
|
const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home3));
|
|
25824
26326
|
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)}`));
|
|
25825
26327
|
}
|
|
25826
|
-
/**
|
|
26328
|
+
/**
|
|
26329
|
+
* Trouble while the server doesn't answer, and fine once it does (ADR 0083). Nothing while a
|
|
26330
|
+
* restart is under way: its end says (ADR 0094).
|
|
26331
|
+
*/
|
|
25827
26332
|
async sayHowItIs() {
|
|
26333
|
+
if (this.restarting !== void 0) return;
|
|
25828
26334
|
const up = await this.options.opencode.healthy();
|
|
26335
|
+
if (this.restarting !== void 0) return;
|
|
26336
|
+
if (up) this.why = void 0;
|
|
25829
26337
|
const chips2 = this.options.chips;
|
|
25830
|
-
await (up ? chips2.reportOk() : chips2.reportTrouble("OpenCode isn't answering")).catch(() => void 0);
|
|
26338
|
+
await (up ? chips2.reportOk() : chips2.reportTrouble(this.why ?? "OpenCode isn't answering")).catch(() => void 0);
|
|
25831
26339
|
}
|
|
25832
26340
|
/** OpenCode's event stream, opened again whenever it drops, a little later each time it fails. */
|
|
25833
26341
|
async listen() {
|
|
@@ -25841,8 +26349,15 @@ var Bridge = class {
|
|
|
25841
26349
|
if (this.listening.signal.aborted) return;
|
|
25842
26350
|
this.log(`OpenCode's events: ${messageOf(error62)}`);
|
|
25843
26351
|
}
|
|
25844
|
-
await new Promise((resolve5) =>
|
|
25845
|
-
|
|
26352
|
+
const woken = await new Promise((resolve5) => {
|
|
26353
|
+
const timer = setTimeout(() => resolve5(false), wait2);
|
|
26354
|
+
this.wake = () => {
|
|
26355
|
+
clearTimeout(timer);
|
|
26356
|
+
resolve5(true);
|
|
26357
|
+
};
|
|
26358
|
+
});
|
|
26359
|
+
this.wake = void 0;
|
|
26360
|
+
wait2 = woken ? first : Math.min(wait2 * 2, 15e3);
|
|
25846
26361
|
}
|
|
25847
26362
|
}
|
|
25848
26363
|
/**
|
|
@@ -25861,6 +26376,47 @@ var Bridge = class {
|
|
|
25861
26376
|
});
|
|
25862
26377
|
this.ours.add(agent.agentId);
|
|
25863
26378
|
this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
|
|
26379
|
+
await this.sendModel(agent.agentId);
|
|
26380
|
+
await this.sendContexts();
|
|
26381
|
+
}
|
|
26382
|
+
/**
|
|
26383
|
+
* A space a person asked for from Chips's + (Chips ADR 0241), or beside another harness: claimed
|
|
26384
|
+
* under a key of its own, named from its first words, and Chips makes the new agent the key is
|
|
26385
|
+
* for and puts it in the space. That agent answers the first words, which are the ask's and are
|
|
26386
|
+
* not heard again as a message, as the first OpenCode agent, in a session and a folder of its
|
|
26387
|
+
* own. A claim heard again finds the space claimed already, and the agent the same.
|
|
26388
|
+
*/
|
|
26389
|
+
async claim(asked) {
|
|
26390
|
+
const { chips: chips2 } = this.options;
|
|
26391
|
+
const { spaceId, firstMessage } = asked;
|
|
26392
|
+
const key = `opencode:space:${spaceId}`;
|
|
26393
|
+
await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
|
|
26394
|
+
const agent = (await chips2.agents.list()).find((each) => each.key === key);
|
|
26395
|
+
if (agent === void 0) {
|
|
26396
|
+
this.log(`claimed ${spaceId}, and Chips put no agent there`);
|
|
26397
|
+
return;
|
|
26398
|
+
}
|
|
26399
|
+
this.ours.add(agent.agentId);
|
|
26400
|
+
this.log(`${agent.name} claimed ${spaceId}`);
|
|
26401
|
+
await this.sendModel(agent.agentId);
|
|
26402
|
+
await this.sendContexts();
|
|
26403
|
+
const context = await chips2.roomContext(spaceId, { limit: 20 }).catch(() => void 0);
|
|
26404
|
+
const said = context?.lines.find((line) => line.message_id === firstMessage.messageId)?.from;
|
|
26405
|
+
const message = {
|
|
26406
|
+
type: "message.new",
|
|
26407
|
+
space_id: spaceId,
|
|
26408
|
+
message_id: firstMessage.messageId,
|
|
26409
|
+
text: firstMessage.text,
|
|
26410
|
+
from: {
|
|
26411
|
+
participant_id: asked.askedBy,
|
|
26412
|
+
kind: "person",
|
|
26413
|
+
display_name: said?.name ?? "Someone",
|
|
26414
|
+
role: said?.role ?? "member",
|
|
26415
|
+
...said?.workspace_role === void 0 ? {} : { workspace_role: said.workspace_role }
|
|
26416
|
+
},
|
|
26417
|
+
for: [agent.agentId]
|
|
26418
|
+
};
|
|
26419
|
+
await this.hear(message);
|
|
25864
26420
|
}
|
|
25865
26421
|
/** A person's Fork, made once however often it is heard at a time. */
|
|
25866
26422
|
copy(fork) {
|
|
@@ -25922,6 +26478,8 @@ var Bridge = class {
|
|
|
25922
26478
|
this.ours.add(copy.agentId);
|
|
25923
26479
|
if (kept.session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] = kept.session;
|
|
25924
26480
|
if (kept.folder !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: kept.folder };
|
|
26481
|
+
const pick2 = state2.models?.[who];
|
|
26482
|
+
if (pick2 !== void 0) state2.models = { ...state2.models, [copy.agentId]: pick2 };
|
|
25925
26483
|
saveState(state2);
|
|
25926
26484
|
if (copied !== void 0) {
|
|
25927
26485
|
await opencode2.update(copied.id, {
|
|
@@ -25932,6 +26490,7 @@ var Bridge = class {
|
|
|
25932
26490
|
this.log(
|
|
25933
26491
|
`${copy.name}, a copy of ${name}, made${kept.session === void 0 ? " new" : ` from ${kept.session}`}${copy.attached === false && copy.why !== void 0 ? `; not in the room: ${copy.why}` : ""}`
|
|
25934
26492
|
);
|
|
26493
|
+
void this.sendModel(copy.agentId).then(() => this.sendContexts());
|
|
25935
26494
|
}
|
|
25936
26495
|
/** The session of an agent's to copy: its room's, or where it is in none, the last it had. */
|
|
25937
26496
|
sessionToCopy(who, room) {
|
|
@@ -25939,6 +26498,304 @@ var Bridge = class {
|
|
|
25939
26498
|
if (room !== void 0 && room !== null) return sessions[`${who}|${room}`];
|
|
25940
26499
|
return Object.entries(sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
|
|
25941
26500
|
}
|
|
26501
|
+
// ------------------------------------------------------------------ what a person changes from an agent's page
|
|
26502
|
+
/**
|
|
26503
|
+
* Which OpenCode agent a Chips agent of the harness's runs as, and whose its sessions and its
|
|
26504
|
+
* pick are, as `roomOf` says: the key of one configured here, else its Chips id. Nothing for an
|
|
26505
|
+
* agent not the harness's.
|
|
26506
|
+
*/
|
|
26507
|
+
async whoseIs(agentId2) {
|
|
26508
|
+
const agent = await this.runnerOf(agentId2);
|
|
26509
|
+
if (agent === void 0 || agentId2 === this.options.chips.harnessId) return void 0;
|
|
26510
|
+
return { agent, who: agentId2 === agent.agentId ? agent.key : agentId2 };
|
|
26511
|
+
}
|
|
26512
|
+
/**
|
|
26513
|
+
* What an agent runs on with no pick of its own: its model in the bridge's config, else its
|
|
26514
|
+
* OpenCode agent's, else the server's default.
|
|
26515
|
+
*/
|
|
26516
|
+
async fallbackOf(agent) {
|
|
26517
|
+
if (agent.model !== void 0) return { model: agent.model, label: "As in config.json" };
|
|
26518
|
+
const { opencode: opencode2 } = this.options;
|
|
26519
|
+
const own2 = (await opencode2.agents().catch(() => [])).find((each) => each.id === agent.config.agent)?.model;
|
|
26520
|
+
if (own2 !== void 0) return { model: own2, label: "OpenCode's default" };
|
|
26521
|
+
const model = await opencode2.defaultModel();
|
|
26522
|
+
return model === void 0 ? void 0 : { model: { providerID: model.providerID, id: model.id }, label: "OpenCode's default" };
|
|
26523
|
+
}
|
|
26524
|
+
/** An agent's model, as OpenCode and the bridge have it now: nothing for an agent not the harness's. */
|
|
26525
|
+
async modelCopyOf(agentId2) {
|
|
26526
|
+
const whose = await this.whoseIs(agentId2);
|
|
26527
|
+
if (whose === void 0) return void 0;
|
|
26528
|
+
const [catalogue, fallback] = await Promise.all([this.options.opencode.models(), this.fallbackOf(whose.agent)]);
|
|
26529
|
+
return modelCopy({ catalogue, pick: this.options.state.models?.[whose.who], fallback });
|
|
26530
|
+
}
|
|
26531
|
+
/** An agent's model sent, where it changed since it last was (Chips takes the same copy once). */
|
|
26532
|
+
async sendModel(agentId2) {
|
|
26533
|
+
try {
|
|
26534
|
+
const copy = await this.modelCopyOf(agentId2);
|
|
26535
|
+
if (copy !== void 0) await this.options.chips.settings.model(agentId2, copy);
|
|
26536
|
+
} catch (error62) {
|
|
26537
|
+
this.log(`the model of ${agentId2} not sent: ${messageOf(error62)}`);
|
|
26538
|
+
}
|
|
26539
|
+
}
|
|
26540
|
+
/**
|
|
26541
|
+
* A person's pick of an agent's model and level from its page (Chips ADR 0241): kept with the
|
|
26542
|
+
* bridge's state, and given to each of its sessions before its next message there. Only a model
|
|
26543
|
+
* OpenCode runs, at one of its levels; `null` is its default again. Answered with its copy.
|
|
26544
|
+
*/
|
|
26545
|
+
async modelChanged(change) {
|
|
26546
|
+
const whose = await this.whoseIs(change.agent_id);
|
|
26547
|
+
if (whose === void 0) throw new Refuse("This harness has no such agent.");
|
|
26548
|
+
const { opencode: opencode2, state: state2, saveState } = this.options;
|
|
26549
|
+
let pick2;
|
|
26550
|
+
try {
|
|
26551
|
+
pick2 = pickOf(change, await opencode2.models());
|
|
26552
|
+
} catch (error62) {
|
|
26553
|
+
throw new Refuse(messageOf(error62), await this.modelCopyOf(change.agent_id).catch(() => void 0));
|
|
26554
|
+
}
|
|
26555
|
+
const models = { ...state2.models };
|
|
26556
|
+
if (pick2 === null) delete models[whose.who];
|
|
26557
|
+
else models[whose.who] = pick2;
|
|
26558
|
+
state2.models = models;
|
|
26559
|
+
saveState(state2);
|
|
26560
|
+
const name = await this.nameOf(whose);
|
|
26561
|
+
this.log(`${name} runs on ${pick2 === null ? "its default" : `${pick2.model}${pick2.effort === void 0 ? "" : ` at ${pick2.effort}`}`}, from Chips`);
|
|
26562
|
+
const copy = await this.modelCopyOf(change.agent_id);
|
|
26563
|
+
if (copy === void 0) throw new Refuse("OpenCode names no model for it.");
|
|
26564
|
+
return copy;
|
|
26565
|
+
}
|
|
26566
|
+
/**
|
|
26567
|
+
* The session a message goes to, put on the model its agent runs on, where it isn't on it
|
|
26568
|
+
* already (Chips ADR 0241): its pick, else its model in the bridge's config. A session that was
|
|
26569
|
+
* put on another before, with neither now, goes back to its OpenCode agent's or the server's.
|
|
26570
|
+
*/
|
|
26571
|
+
async modelled(sessionId, { who, agent }) {
|
|
26572
|
+
const { opencode: opencode2, state: state2 } = this.options;
|
|
26573
|
+
const pick2 = state2.models?.[who];
|
|
26574
|
+
try {
|
|
26575
|
+
const has = (await opencode2.session(sessionId))["model"];
|
|
26576
|
+
const want = pick2 !== void 0 ? refOf(pick2) : agent.model ?? (has === void 0 ? void 0 : (await this.fallbackOf(agent))?.model);
|
|
26577
|
+
if (want === void 0 || sameModel(want, has)) return;
|
|
26578
|
+
await opencode2.setModel(sessionId, want);
|
|
26579
|
+
this.log(`${sessionId} runs on ${writtenOf(want)}${want.variant === void 0 ? "" : ` at ${want.variant}`} from now`);
|
|
26580
|
+
} catch (error62) {
|
|
26581
|
+
if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) {
|
|
26582
|
+
this.log(`${sessionId} stays on its model: ${messageOf(error62)}`);
|
|
26583
|
+
}
|
|
26584
|
+
}
|
|
26585
|
+
}
|
|
26586
|
+
/** The folder an agent's sessions work in, its own or the one project's; none where the bridge knows neither. */
|
|
26587
|
+
async agentFolder(whose) {
|
|
26588
|
+
return await this.folderFor(whose) ?? this.options.directory;
|
|
26589
|
+
}
|
|
26590
|
+
/**
|
|
26591
|
+
* What OpenCode reads for a folder beside its `AGENTS.md` files: where its walk up stops, and
|
|
26592
|
+
* from 2.0.30 the files its config's `instructions` names. A folder whose project OpenCode can't
|
|
26593
|
+
* say is its own project.
|
|
26594
|
+
*/
|
|
26595
|
+
async readingOf(folder2) {
|
|
26596
|
+
const { opencode: opencode2 } = this.options;
|
|
26597
|
+
const { home: home3 } = this.places;
|
|
26598
|
+
let root = this.roots.get(folder2);
|
|
26599
|
+
if (root === void 0) {
|
|
26600
|
+
root = await opencode2.projectRoot(folder2).catch(() => folder2);
|
|
26601
|
+
this.roots.set(folder2, root);
|
|
26602
|
+
}
|
|
26603
|
+
this.readsNamed ??= opencode2.version().then(
|
|
26604
|
+
(version3) => !before(version3, READS_NAMED),
|
|
26605
|
+
() => false
|
|
26606
|
+
);
|
|
26607
|
+
const sources = await this.readsNamed ? await opencode2.instructions(folder2).catch(() => []) : [];
|
|
26608
|
+
return { stop: within(folder2, home3) ? home3 : root, named: namedFiles(sources, folder2, root, home3) };
|
|
26609
|
+
}
|
|
26610
|
+
/** Each agent of the harness's, with the folder its sessions work in and what OpenCode reads there. */
|
|
26611
|
+
async agentReadings() {
|
|
26612
|
+
const read = [];
|
|
26613
|
+
for (const agent of await this.options.chips.agents.list().catch(() => [])) {
|
|
26614
|
+
if (agent.state === "archived") continue;
|
|
26615
|
+
const whose = await this.whoseIs(agent.agentId);
|
|
26616
|
+
const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
|
|
26617
|
+
if (folder2 !== void 0) read.push({ agentId: agent.agentId, folder: folder2, reading: await this.readingOf(folder2) });
|
|
26618
|
+
}
|
|
26619
|
+
return read;
|
|
26620
|
+
}
|
|
26621
|
+
/** The harness's own set and each agent's files, as they are on disk now: each sent where it changed. */
|
|
26622
|
+
async sendContexts() {
|
|
26623
|
+
const files = this.files;
|
|
26624
|
+
if (files === void 0) return;
|
|
26625
|
+
try {
|
|
26626
|
+
const read = await this.agentReadings();
|
|
26627
|
+
const readings = new Map(read.map((each) => [each.folder, each.reading]));
|
|
26628
|
+
await this.options.chips.settings.harnessContext(files.harnessCopy(readings, read.map((each) => each.folder)));
|
|
26629
|
+
for (const { agentId: agentId2, folder: folder2, reading } of read) {
|
|
26630
|
+
await this.options.chips.settings.context(agentId2, files.agentCopy(folder2, reading));
|
|
26631
|
+
}
|
|
26632
|
+
} catch (error62) {
|
|
26633
|
+
this.log(`the agents' files not sent: ${messageOf(error62)}`);
|
|
26634
|
+
}
|
|
26635
|
+
}
|
|
26636
|
+
/** One agent's files sent, where they changed. */
|
|
26637
|
+
async sendContext(agentId2) {
|
|
26638
|
+
const whose = await this.whoseIs(agentId2);
|
|
26639
|
+
const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
|
|
26640
|
+
if (this.files === void 0 || folder2 === void 0) return;
|
|
26641
|
+
try {
|
|
26642
|
+
await this.options.chips.settings.context(agentId2, this.files.agentCopy(folder2, await this.readingOf(folder2)));
|
|
26643
|
+
} catch (error62) {
|
|
26644
|
+
this.log(`the files of ${agentId2} not sent: ${messageOf(error62)}`);
|
|
26645
|
+
}
|
|
26646
|
+
}
|
|
26647
|
+
/**
|
|
26648
|
+
* A person's edit of a file from Chips (Chips ADR 0134): saved where OpenCode reads it, only if
|
|
26649
|
+
* it is still the version they changed, and answered with the copy as it is now. OpenCode reads
|
|
26650
|
+
* it again before the agent's next model call, and tells the agent what changed: every agent
|
|
26651
|
+
* working in that folder, or for the harness's own set every agent that reads the file.
|
|
26652
|
+
*/
|
|
26653
|
+
async contextEdited(edit) {
|
|
26654
|
+
const files = this.files;
|
|
26655
|
+
if (files === void 0) throw new Refuse("This bridge doesn't know where its agents' files are.");
|
|
26656
|
+
if (edit.agent_id === void 0) {
|
|
26657
|
+
const read = await this.agentReadings();
|
|
26658
|
+
const readings = new Map(read.map((each) => [each.folder, each.reading]));
|
|
26659
|
+
const folders = read.map((each) => each.folder);
|
|
26660
|
+
await files.saveShared(readings, folders, edit);
|
|
26661
|
+
this.log(`OpenCode's ${edit.name} saved from Chips`);
|
|
26662
|
+
return files.harnessCopy(readings, folders);
|
|
26663
|
+
}
|
|
26664
|
+
const whose = await this.whoseIs(edit.agent_id);
|
|
26665
|
+
const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
|
|
26666
|
+
if (whose === void 0 || folder2 === void 0) throw new Refuse("This harness has no such agent.");
|
|
26667
|
+
const reading = await this.readingOf(folder2);
|
|
26668
|
+
await files.saveAgent(folder2, reading, edit);
|
|
26669
|
+
this.log(`${await this.nameOf(whose)}'s ${edit.name} saved from Chips`);
|
|
26670
|
+
for (const other of await this.agentReadings()) {
|
|
26671
|
+
if (other.agentId !== edit.agent_id && other.folder === folder2) void this.sendContext(other.agentId);
|
|
26672
|
+
}
|
|
26673
|
+
return files.agentCopy(folder2, reading);
|
|
26674
|
+
}
|
|
26675
|
+
// ------------------------------------------------------------------ how full each agent is (Chips ADRs 0146, 0241)
|
|
26676
|
+
/** The Chips agent whose sessions are `who`'s: a configured agent's, or the agent itself. */
|
|
26677
|
+
agentIdOf(who) {
|
|
26678
|
+
return this.agents.find((agent) => agent.key === who)?.agentId ?? who;
|
|
26679
|
+
}
|
|
26680
|
+
/** Whose a session is, by the rooms the bridge keeps: nothing for one it didn't make. */
|
|
26681
|
+
whoseSession(sessionId) {
|
|
26682
|
+
const id2 = Object.entries(this.options.state.sessions).find(([, each]) => each === sessionId)?.[0];
|
|
26683
|
+
return id2?.slice(0, id2.lastIndexOf("|"));
|
|
26684
|
+
}
|
|
26685
|
+
/** An agent's window copy sent, and kept with the bridge's state: nothing until it has said how full it is. */
|
|
26686
|
+
async sendWindow(who) {
|
|
26687
|
+
const kept = this.options.state.windows?.[who];
|
|
26688
|
+
if (kept === void 0) return;
|
|
26689
|
+
this.options.saveState(this.options.state);
|
|
26690
|
+
await this.options.chips.settings.contextWindow(this.agentIdOf(who), windowCopy(kept, this.compacting.get(who)?.state)).catch((error62) => this.log(`the context window of ${who} not sent: ${messageOf(error62)}`));
|
|
26691
|
+
}
|
|
26692
|
+
/**
|
|
26693
|
+
* Each agent's window as it last said, at start. One the bridge kept nothing of, from before it
|
|
26694
|
+
* kept them, is read from the newest model call of its sessions.
|
|
26695
|
+
*/
|
|
26696
|
+
async sendWindows() {
|
|
26697
|
+
const { opencode: opencode2, state: state2 } = this.options;
|
|
26698
|
+
const whose = new Set(Object.keys(state2.sessions).map((id2) => id2.slice(0, id2.lastIndexOf("|"))));
|
|
26699
|
+
for (const who of whose) {
|
|
26700
|
+
if (state2.windows?.[who] === void 0) {
|
|
26701
|
+
let newest;
|
|
26702
|
+
for (const [id2, sessionId] of Object.entries(state2.sessions)) {
|
|
26703
|
+
if (!id2.startsWith(`${who}|`)) continue;
|
|
26704
|
+
const call = await opencode2.lastCall(sessionId).catch(() => void 0);
|
|
26705
|
+
if (call !== void 0 && (newest === void 0 || call.at > newest.call.at)) newest = { sessionId, call };
|
|
26706
|
+
}
|
|
26707
|
+
const model = newest?.call.model === void 0 ? void 0 : writtenOf(newest.call.model);
|
|
26708
|
+
const window = model === void 0 ? void 0 : await this.windowOf(model);
|
|
26709
|
+
if (newest === void 0 || model === void 0 || window === void 0) continue;
|
|
26710
|
+
const asOf = new Date(newest.call.at > 0 ? newest.call.at : Date.now()).toISOString();
|
|
26711
|
+
state2.windows = { ...state2.windows, [who]: { used: usedOf(newest.call.tokens), window, model, asOf, session: newest.sessionId } };
|
|
26712
|
+
}
|
|
26713
|
+
await this.sendWindow(who);
|
|
26714
|
+
}
|
|
26715
|
+
}
|
|
26716
|
+
/** A turn ended: how full it got, where that passed half the window, and the copy as it is now. */
|
|
26717
|
+
async turnHeld(turn) {
|
|
26718
|
+
const kept = this.options.state.windows?.[turn.who];
|
|
26719
|
+
if (kept === void 0) return;
|
|
26720
|
+
if (turn.peak > kept.window / 2) {
|
|
26721
|
+
kept.longTurn = { at: (/* @__PURE__ */ new Date()).toISOString(), peak: Math.round(turn.peak), folds: turn.folds };
|
|
26722
|
+
}
|
|
26723
|
+
await this.sendWindow(turn.who);
|
|
26724
|
+
}
|
|
26725
|
+
/**
|
|
26726
|
+
* What OpenCode says of a compaction of one of the agents' sessions: under way, done, with how
|
|
26727
|
+
* much it held before and after, or failed. A Compact now asked from Chips that failed is
|
|
26728
|
+
* refused after all, in OpenCode's words.
|
|
26729
|
+
*/
|
|
26730
|
+
compactionHeard(sessionId, event) {
|
|
26731
|
+
const who = this.whoseSession(sessionId);
|
|
26732
|
+
const kept = who === void 0 ? void 0 : this.options.state.windows?.[who];
|
|
26733
|
+
if (who === void 0 || kept === void 0) return;
|
|
26734
|
+
const asked = this.compacting.get(who);
|
|
26735
|
+
if (event.type === "session.compaction.started") {
|
|
26736
|
+
if (asked === void 0) this.compacting.set(who, { state: "running" });
|
|
26737
|
+
else asked.state = "running";
|
|
26738
|
+
void this.sendWindow(who);
|
|
26739
|
+
return;
|
|
26740
|
+
}
|
|
26741
|
+
if (event.type === "session.compaction.ended") {
|
|
26742
|
+
this.compacting.delete(who);
|
|
26743
|
+
const to = compactedTo(event.data);
|
|
26744
|
+
const by = event.data["reason"] === "manual" ? "asked" : "itself";
|
|
26745
|
+
kept.last = { at: (/* @__PURE__ */ new Date()).toISOString(), from: Math.round(kept.used), to, by };
|
|
26746
|
+
kept.used = to;
|
|
26747
|
+
kept.asOf = kept.last.at;
|
|
26748
|
+
kept.session = sessionId;
|
|
26749
|
+
const turn = this.turns.get(sessionId);
|
|
26750
|
+
if (turn !== void 0 && by === "itself") turn.folds += 1;
|
|
26751
|
+
this.log(`${who} compacted (${by}), ${kept.last.from} to ${to} tokens`);
|
|
26752
|
+
void this.sendWindow(who);
|
|
26753
|
+
return;
|
|
26754
|
+
}
|
|
26755
|
+
if (event.type === "session.compaction.failed") {
|
|
26756
|
+
this.compacting.delete(who);
|
|
26757
|
+
const error62 = event.data["error"] ?? {};
|
|
26758
|
+
const why = text(error62["message"]) ?? "OpenCode didn't compact it.";
|
|
26759
|
+
this.log(`${who} not compacted: ${why}`);
|
|
26760
|
+
if (asked?.changeId === void 0) {
|
|
26761
|
+
void this.sendWindow(who);
|
|
26762
|
+
return;
|
|
26763
|
+
}
|
|
26764
|
+
void this.options.chips.settings.contextWindowFailed(this.agentIdOf(who), asked.changeId, why, windowCopy(kept)).catch((failed) => this.log(`the context window of ${who} not sent: ${messageOf(failed)}`));
|
|
26765
|
+
}
|
|
26766
|
+
}
|
|
26767
|
+
/**
|
|
26768
|
+
* A person's Compact now (Chips ADR 0146): asked of OpenCode for the session the agent said it
|
|
26769
|
+
* was in last, which takes it at once when idle and at its next step when working, and answered
|
|
26770
|
+
* with the copy saying so. How it goes comes in OpenCode's own events.
|
|
26771
|
+
*/
|
|
26772
|
+
async windowCompact(ask2) {
|
|
26773
|
+
const whose = await this.whoseIs(ask2.agent_id);
|
|
26774
|
+
if (whose === void 0) throw new Refuse("This harness has no such agent.");
|
|
26775
|
+
const { who } = whose;
|
|
26776
|
+
const kept = this.options.state.windows?.[who];
|
|
26777
|
+
if (kept === void 0) throw new Refuse(`${await this.nameOf(whose)} hasn't said how full it is yet.`);
|
|
26778
|
+
if (this.compacting.has(who)) throw new Refuse("It is compacting already.", windowCopy(kept, this.compacting.get(who)?.state));
|
|
26779
|
+
const { opencode: opencode2 } = this.options;
|
|
26780
|
+
const busy = this.turns.has(kept.session) || (await opencode2.active().catch(() => /* @__PURE__ */ new Set())).has(kept.session);
|
|
26781
|
+
this.compacting.set(who, { state: busy ? "queued" : "running", changeId: ask2.change_id });
|
|
26782
|
+
try {
|
|
26783
|
+
await opencode2.compact(kept.session);
|
|
26784
|
+
} catch (error62) {
|
|
26785
|
+
this.compacting.delete(who);
|
|
26786
|
+
throw new Refuse(`OpenCode didn't take it: ${messageOf(error62)}`, windowCopy(kept));
|
|
26787
|
+
}
|
|
26788
|
+
this.log(`${await this.nameOf(whose)} compacts ${busy ? "at its session's next step" : "now"}, from Chips`);
|
|
26789
|
+
return windowCopy(kept, this.compacting.get(who)?.state);
|
|
26790
|
+
}
|
|
26791
|
+
/** A person's limit: OpenCode keeps its own, set in its config, and the copy says so. */
|
|
26792
|
+
async windowChange(ask2) {
|
|
26793
|
+
const whose = await this.whoseIs(ask2.agent_id);
|
|
26794
|
+
const kept = whose === void 0 ? void 0 : this.options.state.windows?.[whose.who];
|
|
26795
|
+
if (whose === void 0) throw new Refuse("This harness has no such agent.");
|
|
26796
|
+
const copy = kept === void 0 ? void 0 : windowCopy(kept, this.compacting.get(whose.who)?.state);
|
|
26797
|
+
throw new Refuse(copy?.why_read_only ?? "OpenCode keeps its own limit.", copy);
|
|
26798
|
+
}
|
|
25942
26799
|
// ------------------------------------------------------------------ what is said to it
|
|
25943
26800
|
/** A message for some of its agents: each takes a turn in its room, one after another there. */
|
|
25944
26801
|
async hear(message, own2 = {}) {
|
|
@@ -26043,13 +26900,17 @@ var Bridge = class {
|
|
|
26043
26900
|
return;
|
|
26044
26901
|
}
|
|
26045
26902
|
let end2 = () => void 0;
|
|
26046
|
-
const
|
|
26903
|
+
const ended2 = new Promise((resolve5) => {
|
|
26047
26904
|
end2 = resolve5;
|
|
26048
26905
|
});
|
|
26049
26906
|
const turn = {
|
|
26050
26907
|
run: run2,
|
|
26051
26908
|
speaker,
|
|
26052
26909
|
agent,
|
|
26910
|
+
who: room.who,
|
|
26911
|
+
agentId: speakerId ?? agent.agentId,
|
|
26912
|
+
peak: 0,
|
|
26913
|
+
folds: 0,
|
|
26053
26914
|
sessionId,
|
|
26054
26915
|
waiting: /* @__PURE__ */ new Set(),
|
|
26055
26916
|
taken: /* @__PURE__ */ new Set(),
|
|
@@ -26076,11 +26937,14 @@ var Bridge = class {
|
|
|
26076
26937
|
const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
|
|
26077
26938
|
void this.steered(turn, words4, steer.mode);
|
|
26078
26939
|
});
|
|
26079
|
-
const last = await
|
|
26940
|
+
const last = await ended2;
|
|
26080
26941
|
await turn.chain;
|
|
26081
26942
|
if (this.turns.get(sessionId) === turn) this.turns.delete(sessionId);
|
|
26082
26943
|
if (last.outcome === "succeeded") await this.finish(turn);
|
|
26083
26944
|
else await this.ended(turn, last);
|
|
26945
|
+
await this.sendModel(turn.agentId);
|
|
26946
|
+
await this.sendContext(turn.agentId);
|
|
26947
|
+
await this.turnHeld(turn);
|
|
26084
26948
|
}
|
|
26085
26949
|
/** The words into the room's session: the one it had, or a new one when OpenCode no longer has it. */
|
|
26086
26950
|
async give({ id: id2, who, agent, spaceId }, text2) {
|
|
@@ -26092,16 +26956,18 @@ var Bridge = class {
|
|
|
26092
26956
|
}
|
|
26093
26957
|
if (kept !== void 0) {
|
|
26094
26958
|
try {
|
|
26959
|
+
await this.modelled(kept, { who, agent });
|
|
26095
26960
|
return { sessionId: kept, given: await this.prompted(kept, text2) };
|
|
26096
26961
|
} catch (error62) {
|
|
26097
26962
|
if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) throw error62;
|
|
26098
26963
|
this.log(`${agent.config.name}'s session for ${spaceId} is gone from OpenCode: a new one`);
|
|
26099
26964
|
}
|
|
26100
26965
|
}
|
|
26966
|
+
const pick2 = state2.models?.[who];
|
|
26101
26967
|
const made = await opencode2.newSession({
|
|
26102
26968
|
title: `Chips space ${spaceId}`,
|
|
26103
26969
|
agent: agent.config.agent,
|
|
26104
|
-
model: agent.model,
|
|
26970
|
+
model: pick2 === void 0 ? agent.model : refOf(pick2),
|
|
26105
26971
|
directory: await this.folderFor({ who, agent }),
|
|
26106
26972
|
metadata: { chips_space: spaceId, chips_agent: who }
|
|
26107
26973
|
});
|
|
@@ -26238,6 +27104,11 @@ var Bridge = class {
|
|
|
26238
27104
|
return;
|
|
26239
27105
|
}
|
|
26240
27106
|
if (event.type === "session.forked") return;
|
|
27107
|
+
if (event.type.startsWith("session.compaction.")) {
|
|
27108
|
+
const going = this.turns.get(sessionId);
|
|
27109
|
+
if (going === void 0) this.compactionHeard(sessionId, event);
|
|
27110
|
+
else going.chain = going.chain.then(() => this.compactionHeard(sessionId, event));
|
|
27111
|
+
}
|
|
26241
27112
|
const own2 = this.turns.get(sessionId);
|
|
26242
27113
|
if (own2 === void 0 && this.early.has(sessionId)) {
|
|
26243
27114
|
this.early.get(sessionId)?.push(event);
|
|
@@ -26409,6 +27280,9 @@ var Bridge = class {
|
|
|
26409
27280
|
const limit = await this.windowOf(model);
|
|
26410
27281
|
if (limit === void 0) return;
|
|
26411
27282
|
await turn.run.context({ usedTokens: prompt + written, limitTokens: limit, model }).catch(() => void 0);
|
|
27283
|
+
const windows = this.options.state.windows ??= {};
|
|
27284
|
+
windows[turn.who] = { ...windows[turn.who], used: prompt + written, window: limit, model, asOf: (/* @__PURE__ */ new Date()).toISOString(), session: turn.sessionId };
|
|
27285
|
+
turn.peak = Math.max(turn.peak, prompt + written);
|
|
26412
27286
|
}
|
|
26413
27287
|
/** A model's context window, from OpenCode's catalogue, read once (and once more for each it lacked). */
|
|
26414
27288
|
async windowOf(model) {
|
|
@@ -26442,7 +27316,12 @@ var Bridge = class {
|
|
|
26442
27316
|
if (decision === void 0 || pending === void 0 || pending.answered) return;
|
|
26443
27317
|
pending.answered = true;
|
|
26444
27318
|
if (decision.allowed && toolCall !== void 0) turn.allowed.set(toolCall, approvalId);
|
|
26445
|
-
await this.options.opencode.replyPermission(
|
|
27319
|
+
await this.options.opencode.replyPermission(
|
|
27320
|
+
askingSession,
|
|
27321
|
+
requestId,
|
|
27322
|
+
decisionOf(decision, card.action.kind === "shell.exec"),
|
|
27323
|
+
decision.allowed ? void 0 : decision.reason
|
|
27324
|
+
).catch((error62) => this.log(`OpenCode didn't take the answer: ${messageOf(error62)}`));
|
|
26446
27325
|
}
|
|
26447
27326
|
/** A permission answered in OpenCode's own apps: settled in Chips, once, so the card cools (ADR 0079). */
|
|
26448
27327
|
async settled(turn, data) {
|
|
@@ -26580,9 +27459,24 @@ var Bridge = class {
|
|
|
26580
27459
|
if (retryable) void this.sayHowItIs();
|
|
26581
27460
|
}
|
|
26582
27461
|
};
|
|
26583
|
-
function
|
|
27462
|
+
function settingsFor(bridge2) {
|
|
27463
|
+
const active = () => {
|
|
27464
|
+
const running = bridge2();
|
|
27465
|
+
if (running === void 0) throw new Refuse("The harness is still starting.");
|
|
27466
|
+
return running;
|
|
27467
|
+
};
|
|
27468
|
+
return {
|
|
27469
|
+
models: (change) => active().modelChanged(change),
|
|
27470
|
+
context: (edit) => active().contextEdited(edit),
|
|
27471
|
+
contextWindows: {
|
|
27472
|
+
compact: (ask2) => active().windowCompact(ask2),
|
|
27473
|
+
change: (ask2) => active().windowChange(ask2)
|
|
27474
|
+
}
|
|
27475
|
+
};
|
|
27476
|
+
}
|
|
27477
|
+
function decisionOf(decision, command = false) {
|
|
26584
27478
|
if (!decision.allowed) return "reject";
|
|
26585
|
-
return decision.scope === "always" ? "always" : "once";
|
|
27479
|
+
return decision.scope === "always" && !command ? "always" : "once";
|
|
26586
27480
|
}
|
|
26587
27481
|
function questionOf(field, title2) {
|
|
26588
27482
|
const prompt = (field.description ?? field.title ?? title2).slice(0, 500);
|
|
@@ -26668,6 +27562,12 @@ function spoken(words4) {
|
|
|
26668
27562
|
return end2 === -1 ? words4 : words4.slice(end2 + THOUGHT_ENDS.length);
|
|
26669
27563
|
}
|
|
26670
27564
|
var THOUGHT_ENDS = "</think>";
|
|
27565
|
+
function titleFrom(text2) {
|
|
27566
|
+
const line = text2.trim().split("\n")[0] ?? "";
|
|
27567
|
+
if (line.length <= 48) return line;
|
|
27568
|
+
const cut2 = line.slice(0, 48);
|
|
27569
|
+
return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
|
|
27570
|
+
}
|
|
26671
27571
|
function forkName(original) {
|
|
26672
27572
|
return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
|
|
26673
27573
|
}
|
|
@@ -26732,12 +27632,12 @@ function lastLine(said) {
|
|
|
26732
27632
|
}
|
|
26733
27633
|
|
|
26734
27634
|
// src/keep.ts
|
|
26735
|
-
import { homedir as
|
|
26736
|
-
import { basename as basename3, join as
|
|
27635
|
+
import { homedir as homedir5 } from "node:os";
|
|
27636
|
+
import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
|
|
26737
27637
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
26738
27638
|
var flags = process.argv.slice(2);
|
|
26739
27639
|
var words3 = flags.filter((word) => !word.startsWith("--"));
|
|
26740
|
-
var whole = (word) => resolve2(word.startsWith("~/") ?
|
|
27640
|
+
var whole = (word) => resolve2(word.startsWith("~/") ? join7(homedir5(), word.slice(2)) : word);
|
|
26741
27641
|
var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
|
|
26742
27642
|
var self = fileURLToPath2(import.meta.url);
|
|
26743
27643
|
var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
|
|
@@ -26755,7 +27655,7 @@ function stopIfAsked(label2) {
|
|
|
26755
27655
|
if (stopped2.state === "theirs") end(`${stopped2.path} wasn't written by this bridge: it was left as it is.`, 1);
|
|
26756
27656
|
end(`Nothing called ${label2} was kept running here.`, 0);
|
|
26757
27657
|
}
|
|
26758
|
-
function keepIfAsked(label2, url2, names, log2,
|
|
27658
|
+
function keepIfAsked(label2, url2, names, log2, before2 = () => void 0) {
|
|
26759
27659
|
if (!flags.includes("--keep")) return;
|
|
26760
27660
|
const node2 = keptNode();
|
|
26761
27661
|
const kept = keepRunning({
|
|
@@ -26765,7 +27665,7 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
|
|
|
26765
27665
|
environment: keepEnvironment(url2, process.env, names, node2)
|
|
26766
27666
|
});
|
|
26767
27667
|
if (kept.state === "kept") {
|
|
26768
|
-
|
|
27668
|
+
before2();
|
|
26769
27669
|
end(
|
|
26770
27670
|
`Kept running as ${label2}: it starts at login, and again whenever it stops. What it says goes to ${kept.logFile}. \`${stopLine}\` stops it.`,
|
|
26771
27671
|
0
|
|
@@ -26785,9 +27685,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
|
|
|
26785
27685
|
// src/server.ts
|
|
26786
27686
|
import { spawn } from "node:child_process";
|
|
26787
27687
|
import { randomBytes } from "node:crypto";
|
|
26788
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as
|
|
27688
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
26789
27689
|
import { createServer } from "node:net";
|
|
26790
|
-
import { delimiter as delimiter2, join as
|
|
27690
|
+
import { delimiter as delimiter2, join as join8 } from "node:path";
|
|
26791
27691
|
import { createInterface } from "node:readline";
|
|
26792
27692
|
var ServerError = class extends Error {
|
|
26793
27693
|
constructor(message, missing = false) {
|
|
@@ -26797,10 +27697,11 @@ var ServerError = class extends Error {
|
|
|
26797
27697
|
missing;
|
|
26798
27698
|
name = "ServerError";
|
|
26799
27699
|
};
|
|
27700
|
+
var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
26800
27701
|
var onPath = (command, env) => command.includes("/") ? existsSync5(command) : (env["PATH"] ?? "").split(delimiter2).some((folder2) => {
|
|
26801
27702
|
if (folder2 === "") return false;
|
|
26802
27703
|
try {
|
|
26803
|
-
accessSync2(
|
|
27704
|
+
accessSync2(join8(folder2, command), constants2.X_OK);
|
|
26804
27705
|
return true;
|
|
26805
27706
|
} catch {
|
|
26806
27707
|
return false;
|
|
@@ -26817,48 +27718,66 @@ async function freePort() {
|
|
|
26817
27718
|
});
|
|
26818
27719
|
return await tryPort(4096) ?? await tryPort(0) ?? 4096;
|
|
26819
27720
|
}
|
|
26820
|
-
async function
|
|
27721
|
+
async function infoOf(url2, password) {
|
|
26821
27722
|
try {
|
|
26822
27723
|
const answer = await fetch(`${url2}/api/info`, {
|
|
26823
27724
|
headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` },
|
|
26824
27725
|
signal: AbortSignal.timeout(2e3)
|
|
26825
27726
|
});
|
|
26826
|
-
if (answer.ok) return
|
|
27727
|
+
if (answer.ok) return await answer.json().catch(() => ({})) ?? {};
|
|
26827
27728
|
return answer.status === 401 ? "refused" : "none";
|
|
26828
27729
|
} catch {
|
|
26829
27730
|
return "none";
|
|
26830
27731
|
}
|
|
26831
27732
|
}
|
|
27733
|
+
async function answers2(url2, password) {
|
|
27734
|
+
const info = await infoOf(url2, password);
|
|
27735
|
+
return typeof info === "string" ? info : "ok";
|
|
27736
|
+
}
|
|
26832
27737
|
function keptIn(path) {
|
|
26833
27738
|
if (!existsSync5(path)) return void 0;
|
|
26834
27739
|
try {
|
|
26835
|
-
const kept = JSON.parse(
|
|
27740
|
+
const kept = JSON.parse(readFileSync6(path, "utf8"));
|
|
26836
27741
|
return typeof kept.port === "number" && typeof kept.password === "string" ? kept : void 0;
|
|
26837
27742
|
} catch {
|
|
26838
27743
|
return void 0;
|
|
26839
27744
|
}
|
|
26840
27745
|
}
|
|
27746
|
+
function alive(pid) {
|
|
27747
|
+
try {
|
|
27748
|
+
process.kill(pid, 0);
|
|
27749
|
+
return true;
|
|
27750
|
+
} catch (error62) {
|
|
27751
|
+
return error62.code === "EPERM";
|
|
27752
|
+
}
|
|
27753
|
+
}
|
|
27754
|
+
async function ended(pid, ms) {
|
|
27755
|
+
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
27756
|
+
try {
|
|
27757
|
+
process.kill(pid, signal);
|
|
27758
|
+
} catch {
|
|
27759
|
+
return;
|
|
27760
|
+
}
|
|
27761
|
+
for (const by = Date.now() + (signal === "SIGTERM" ? ms : 2e3); alive(pid) && Date.now() < by; ) await sleep(50);
|
|
27762
|
+
if (!alive(pid)) return;
|
|
27763
|
+
}
|
|
27764
|
+
}
|
|
26841
27765
|
async function ownServer(options) {
|
|
26842
27766
|
const env = options.env ?? process.env;
|
|
26843
27767
|
const command = options.command ?? "opencode";
|
|
26844
27768
|
const log2 = options.log ?? (() => void 0);
|
|
26845
|
-
const path =
|
|
27769
|
+
const path = join8(options.folder, "server.json");
|
|
26846
27770
|
let kept = keptIn(path);
|
|
27771
|
+
let before2 = false;
|
|
26847
27772
|
if (kept !== void 0) {
|
|
26848
|
-
const
|
|
26849
|
-
|
|
26850
|
-
if (now === "ok") {
|
|
26851
|
-
log2(`OpenCode's server from before is still answering at ${url3}`);
|
|
26852
|
-
return { url: url3, password: kept.password, stop: () => void 0 };
|
|
26853
|
-
}
|
|
27773
|
+
const now = await answers2(`http://127.0.0.1:${kept.port}`, kept.password);
|
|
27774
|
+
before2 = now === "ok";
|
|
26854
27775
|
if (now === "refused") kept = { port: await freePort(), password: kept.password };
|
|
26855
27776
|
}
|
|
26856
|
-
if (!onPath(command, env)) {
|
|
27777
|
+
if (!before2 && !onPath(command, env)) {
|
|
26857
27778
|
throw new ServerError("OpenCode isn't installed on this machine: install it (opencode.ai), then run this again.", true);
|
|
26858
27779
|
}
|
|
26859
27780
|
kept ??= { port: await freePort(), password: randomBytes(24).toString("base64url") };
|
|
26860
|
-
writeFileSync4(path, `${JSON.stringify(kept, null, 2)}
|
|
26861
|
-
`, { mode: 384 });
|
|
26862
27781
|
const { port, password } = kept;
|
|
26863
27782
|
const url2 = `http://127.0.0.1:${port}`;
|
|
26864
27783
|
let child;
|
|
@@ -26866,8 +27785,11 @@ async function ownServer(options) {
|
|
|
26866
27785
|
let answered = false;
|
|
26867
27786
|
let failed;
|
|
26868
27787
|
let wait2 = 2e3;
|
|
27788
|
+
let again2;
|
|
26869
27789
|
const said = [];
|
|
26870
27790
|
const start = () => {
|
|
27791
|
+
writeFileSync4(path, `${JSON.stringify({ port, password }, null, 2)}
|
|
27792
|
+
`, { mode: 384 });
|
|
26871
27793
|
const started = spawn(command, ["serve", "--hostname", "127.0.0.1", "--port", String(port)], {
|
|
26872
27794
|
cwd: options.cwd,
|
|
26873
27795
|
env: { ...env, OPENCODE_PASSWORD: password },
|
|
@@ -26889,9 +27811,10 @@ async function ownServer(options) {
|
|
|
26889
27811
|
return;
|
|
26890
27812
|
}
|
|
26891
27813
|
log2(`OpenCode's server stopped (${signal ?? `exit ${code}`}): starting it again in ${wait2 / 1e3} s`);
|
|
26892
|
-
setTimeout(() => {
|
|
26893
|
-
if (!stopping) start();
|
|
26894
|
-
}, wait2)
|
|
27814
|
+
again2 = setTimeout(() => {
|
|
27815
|
+
if (!stopping && child === void 0) start();
|
|
27816
|
+
}, wait2);
|
|
27817
|
+
again2.unref();
|
|
26895
27818
|
wait2 = Math.min(wait2 * 2, 6e4);
|
|
26896
27819
|
});
|
|
26897
27820
|
started.on("error", (error62) => {
|
|
@@ -26900,33 +27823,76 @@ async function ownServer(options) {
|
|
|
26900
27823
|
});
|
|
26901
27824
|
child = started;
|
|
26902
27825
|
};
|
|
26903
|
-
|
|
26904
|
-
|
|
26905
|
-
|
|
26906
|
-
|
|
26907
|
-
|
|
26908
|
-
|
|
26909
|
-
|
|
26910
|
-
|
|
27826
|
+
const up = async (anew) => {
|
|
27827
|
+
stopping = false;
|
|
27828
|
+
answered = false;
|
|
27829
|
+
failed = void 0;
|
|
27830
|
+
said.length = 0;
|
|
27831
|
+
start();
|
|
27832
|
+
const by = Date.now() + (options.waitMs ?? 3e4);
|
|
27833
|
+
while (await answers2(url2, password) !== "ok") {
|
|
27834
|
+
if (failed !== void 0 || Date.now() > by) {
|
|
27835
|
+
stopping = true;
|
|
27836
|
+
child?.kill("SIGTERM");
|
|
27837
|
+
const last = said.slice(-3).join(" ").trim();
|
|
27838
|
+
throw new ServerError(`OpenCode's server didn't start${anew ? " again" : ""} at ${url2}${last === "" ? "" : `: ${last}`}`);
|
|
27839
|
+
}
|
|
27840
|
+
await sleep(250);
|
|
27841
|
+
}
|
|
27842
|
+
answered = true;
|
|
27843
|
+
wait2 = 2e3;
|
|
27844
|
+
};
|
|
27845
|
+
const down = async () => {
|
|
27846
|
+
stopping = true;
|
|
27847
|
+
clearTimeout(again2);
|
|
27848
|
+
const stopMs = options.stopMs ?? 1e4;
|
|
27849
|
+
const pid = child?.pid;
|
|
27850
|
+
if (pid !== void 0) return ended(pid, stopMs);
|
|
27851
|
+
const info = await infoOf(url2, password);
|
|
27852
|
+
if (typeof info === "string") return;
|
|
27853
|
+
if (typeof info["pid"] !== "number" || info["pid"] <= 0 || info["pid"] === process.pid) {
|
|
27854
|
+
throw new ServerError(
|
|
27855
|
+
`OpenCode's server at ${url2} was running before this bridge and doesn't say which process it is: stop it on this machine, then restart again.`
|
|
27856
|
+
);
|
|
26911
27857
|
}
|
|
26912
|
-
await
|
|
27858
|
+
await ended(info["pid"], stopMs);
|
|
27859
|
+
};
|
|
27860
|
+
if (before2) log2(`OpenCode's server from before is still answering at ${url2}`);
|
|
27861
|
+
else {
|
|
27862
|
+
await up(false);
|
|
27863
|
+
log2(`OpenCode's own server answering at ${url2}`);
|
|
26913
27864
|
}
|
|
26914
|
-
|
|
26915
|
-
|
|
27865
|
+
let restarting;
|
|
27866
|
+
const restart = async () => {
|
|
27867
|
+
if (!onPath(command, env)) {
|
|
27868
|
+
throw new ServerError("OpenCode isn't installed on this machine any more: install it (opencode.ai), then restart again.", true);
|
|
27869
|
+
}
|
|
27870
|
+
log2(`restarting OpenCode's server at ${url2}`);
|
|
27871
|
+
await down();
|
|
27872
|
+
await up(true);
|
|
27873
|
+
log2(`OpenCode's server is answering again at ${url2}`);
|
|
27874
|
+
};
|
|
26916
27875
|
return {
|
|
26917
27876
|
url: url2,
|
|
26918
27877
|
password,
|
|
26919
27878
|
stop: () => {
|
|
26920
27879
|
stopping = true;
|
|
27880
|
+
clearTimeout(again2);
|
|
26921
27881
|
child?.kill("SIGTERM");
|
|
27882
|
+
},
|
|
27883
|
+
restart: () => {
|
|
27884
|
+
restarting ??= restart().finally(() => {
|
|
27885
|
+
restarting = void 0;
|
|
27886
|
+
});
|
|
27887
|
+
return restarting;
|
|
26922
27888
|
}
|
|
26923
27889
|
};
|
|
26924
27890
|
}
|
|
26925
27891
|
|
|
26926
27892
|
// src/setup.ts
|
|
26927
|
-
import { existsSync as existsSync6, readFileSync as
|
|
26928
|
-
import { homedir as
|
|
26929
|
-
import { join as
|
|
27893
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
27894
|
+
import { homedir as homedir6 } from "node:os";
|
|
27895
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
26930
27896
|
var SetupError = class extends Error {
|
|
26931
27897
|
constructor(message, words4 = {}) {
|
|
26932
27898
|
super(message);
|
|
@@ -26943,9 +27909,9 @@ var hostOf = (url2) => {
|
|
|
26943
27909
|
}
|
|
26944
27910
|
};
|
|
26945
27911
|
var STILL_GOOD = "The link is still good: run the same command again once that's done.";
|
|
26946
|
-
var home = (path) => path.startsWith("~/") ?
|
|
27912
|
+
var home = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
|
|
26947
27913
|
function readConfig(path, env = process.env) {
|
|
26948
|
-
const raw = existsSync6(path) ? JSON.parse(
|
|
27914
|
+
const raw = existsSync6(path) ? JSON.parse(readFileSync7(path, "utf8")) : {};
|
|
26949
27915
|
const passwordEnv = typeof raw["passwordEnv"] === "string" ? raw["passwordEnv"] : "OPENCODE_PASSWORD";
|
|
26950
27916
|
const password = filledIn(env[passwordEnv]) ? env[passwordEnv] : void 0;
|
|
26951
27917
|
const ownServer2 = password === void 0 && typeof raw["url"] !== "string";
|
|
@@ -27001,7 +27967,7 @@ async function checkOpenCode(config3, client) {
|
|
|
27001
27967
|
}
|
|
27002
27968
|
|
|
27003
27969
|
// src/main.ts
|
|
27004
|
-
var home2 = (path) => path.startsWith("~/") ?
|
|
27970
|
+
var home2 = (path) => path.startsWith("~/") ? join10(homedir7(), path.slice(2)) : path;
|
|
27005
27971
|
var link = process.env["CHIPS_JOIN_LINK"] || void 0;
|
|
27006
27972
|
var LABEL = "chat.chips.opencode";
|
|
27007
27973
|
stopIfAsked(LABEL);
|
|
@@ -27021,13 +27987,13 @@ try {
|
|
|
27021
27987
|
} catch (error62) {
|
|
27022
27988
|
config2 = stop(error62);
|
|
27023
27989
|
}
|
|
27024
|
-
var folder =
|
|
27990
|
+
var folder = dirname5(configPath);
|
|
27025
27991
|
mkdirSync5(folder, { recursive: true, mode: 448 });
|
|
27026
|
-
var statePath =
|
|
27992
|
+
var statePath = join10(folder, "sessions.json");
|
|
27027
27993
|
var server;
|
|
27028
27994
|
if (link !== void 0) reportStep("checking", { detail: "Looking for OpenCode" });
|
|
27029
27995
|
if (config2.ownServer) {
|
|
27030
|
-
const cwd = config2.directory ??
|
|
27996
|
+
const cwd = config2.directory ?? dirname5(config2.folders);
|
|
27031
27997
|
mkdirSync5(cwd, { recursive: true });
|
|
27032
27998
|
try {
|
|
27033
27999
|
server = await ownServer({ folder, cwd, log });
|
|
@@ -27068,20 +28034,26 @@ var joined = await join3({
|
|
|
27068
28034
|
url: process.env["CHIPS_URL"],
|
|
27069
28035
|
name: config2.name,
|
|
27070
28036
|
software: "chips-opencode 1.0.0",
|
|
27071
|
-
wants:
|
|
28037
|
+
wants: wantsFor(server !== void 0),
|
|
27072
28038
|
onCode: (code) => log(`Allow this harness in Chips: ${code}`)
|
|
27073
28039
|
}).catch(refused2);
|
|
27074
28040
|
keepIfAsked(LABEL, joined.url, [config2.passwordEnv], log, () => server?.stop());
|
|
27075
|
-
var state = existsSync7(statePath) ? JSON.parse(
|
|
28041
|
+
var state = existsSync7(statePath) ? JSON.parse(readFileSync8(statePath, "utf8")) : { sessions: {} };
|
|
28042
|
+
var bridge;
|
|
27076
28043
|
var chips = await connect({
|
|
27077
28044
|
url: joined.url,
|
|
27078
28045
|
token: joined.token,
|
|
27079
28046
|
client: { name: "chips-opencode", version: "1.0.0" },
|
|
27080
|
-
store: fileStore(
|
|
28047
|
+
store: fileStore(join10(folder, "chips-state.json")),
|
|
28048
|
+
// A person's Restart (Chips ADR 0094), heard only where the bridge runs OpenCode's server: one a
|
|
28049
|
+
// person runs themselves isn't the bridge's to restart, so this connection doesn't say it hears them.
|
|
28050
|
+
...server === void 0 ? {} : { onRestart: () => bridge?.restart() },
|
|
28051
|
+
// What a person changes of an agent from its page (Chips ADR 0241): its model and its files.
|
|
28052
|
+
settings: settingsFor(() => bridge)
|
|
27081
28053
|
});
|
|
27082
28054
|
var git = await checkGit();
|
|
27083
28055
|
if (!git.ok) log(`rooms' projects are off: ${git.why}`);
|
|
27084
|
-
|
|
28056
|
+
bridge = new Bridge({
|
|
27085
28057
|
chips,
|
|
27086
28058
|
opencode,
|
|
27087
28059
|
projects: git.ok,
|
|
@@ -27091,7 +28063,8 @@ var bridge = new Bridge({
|
|
|
27091
28063
|
saveState: (next) => writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
|
|
27092
28064
|
`, { mode: 384 }),
|
|
27093
28065
|
log,
|
|
27094
|
-
|
|
28066
|
+
restartServer: server?.restart,
|
|
28067
|
+
...config2.directory === void 0 ? { folders: config2.folders } : { directory: config2.directory }
|
|
27095
28068
|
});
|
|
27096
28069
|
await bridge.start();
|
|
27097
28070
|
log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through OpenCode at ${address.url}`);
|