@chipschat/opencode 1.0.6 → 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 +69 -6
- package/dist/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/main.js +887 -44
- 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,15 +26020,131 @@ 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"];
|
|
25701
26141
|
function wantsFor(ownServer2) {
|
|
25702
26142
|
return ownServer2 ? [...WANTS, "restarts"] : WANTS;
|
|
25703
26143
|
}
|
|
25704
26144
|
function freeFolder(root, name, taken) {
|
|
25705
26145
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
25706
26146
|
for (let count = 1; ; count += 1) {
|
|
25707
|
-
const folder2 =
|
|
26147
|
+
const folder2 = join6(root, count === 1 ? slug : `${slug}-${count}`);
|
|
25708
26148
|
if (!taken.includes(folder2)) return folder2;
|
|
25709
26149
|
}
|
|
25710
26150
|
}
|
|
@@ -25735,6 +26175,9 @@ var Bridge = class {
|
|
|
25735
26175
|
constructor(options) {
|
|
25736
26176
|
this.options = options;
|
|
25737
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;
|
|
25738
26181
|
}
|
|
25739
26182
|
options;
|
|
25740
26183
|
agents = [];
|
|
@@ -25770,6 +26213,15 @@ var Bridge = class {
|
|
|
25770
26213
|
restarting;
|
|
25771
26214
|
/** Why OpenCode's server isn't there, where the bridge knows more than that it doesn't answer. */
|
|
25772
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();
|
|
25773
26225
|
/** Register the agents, start hearing OpenCode and what is said in Chips, and say how it is. */
|
|
25774
26226
|
async start() {
|
|
25775
26227
|
const { chips: chips2 } = this.options;
|
|
@@ -25789,9 +26241,17 @@ var Bridge = class {
|
|
|
25789
26241
|
chips2.on("agent.fork", (fork) => {
|
|
25790
26242
|
this.copy(fork).catch((error62) => this.log(`a copy went wrong: ${messageOf(error62)}`));
|
|
25791
26243
|
});
|
|
26244
|
+
chips2.onSpaceAsked((asked) => {
|
|
26245
|
+
this.claim(asked).catch((error62) => this.log(`a space from the + went wrong: ${messageOf(error62)}`));
|
|
26246
|
+
});
|
|
25792
26247
|
chips2.on("feedback", (feedback2) => {
|
|
25793
26248
|
this.notRight(feedback2).catch((error62) => this.log(`a second look went wrong: ${messageOf(error62)}`));
|
|
25794
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();
|
|
25795
26255
|
await this.sayHowItIs();
|
|
25796
26256
|
this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
|
|
25797
26257
|
this.health.unref();
|
|
@@ -25823,6 +26283,7 @@ var Bridge = class {
|
|
|
25823
26283
|
try {
|
|
25824
26284
|
await again2();
|
|
25825
26285
|
this.why = void 0;
|
|
26286
|
+
this.readsNamed = void 0;
|
|
25826
26287
|
this.wake?.();
|
|
25827
26288
|
} catch (error62) {
|
|
25828
26289
|
const why = messageOf(error62).replace(/\s+/g, " ").trim();
|
|
@@ -25844,16 +26305,16 @@ var Bridge = class {
|
|
|
25844
26305
|
*/
|
|
25845
26306
|
async seatMain(config3, known) {
|
|
25846
26307
|
const { chips: chips2 } = this.options;
|
|
25847
|
-
const
|
|
26308
|
+
const before2 = known.find((agent) => agent.lead);
|
|
25848
26309
|
const made = known.some((agent) => agent.key === chips2.harnessId);
|
|
25849
|
-
if (!made &&
|
|
25850
|
-
await this.goHome(
|
|
25851
|
-
return
|
|
26310
|
+
if (!made && before2 !== void 0) {
|
|
26311
|
+
await this.goHome(before2, known);
|
|
26312
|
+
return before2;
|
|
25852
26313
|
}
|
|
25853
26314
|
const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
|
|
25854
26315
|
await this.goHome(main2, known);
|
|
25855
|
-
if (
|
|
25856
|
-
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)}`));
|
|
25857
26318
|
}
|
|
25858
26319
|
return main2;
|
|
25859
26320
|
}
|
|
@@ -25915,6 +26376,47 @@ var Bridge = class {
|
|
|
25915
26376
|
});
|
|
25916
26377
|
this.ours.add(agent.agentId);
|
|
25917
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);
|
|
25918
26420
|
}
|
|
25919
26421
|
/** A person's Fork, made once however often it is heard at a time. */
|
|
25920
26422
|
copy(fork) {
|
|
@@ -25976,6 +26478,8 @@ var Bridge = class {
|
|
|
25976
26478
|
this.ours.add(copy.agentId);
|
|
25977
26479
|
if (kept.session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] = kept.session;
|
|
25978
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 };
|
|
25979
26483
|
saveState(state2);
|
|
25980
26484
|
if (copied !== void 0) {
|
|
25981
26485
|
await opencode2.update(copied.id, {
|
|
@@ -25986,6 +26490,7 @@ var Bridge = class {
|
|
|
25986
26490
|
this.log(
|
|
25987
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}` : ""}`
|
|
25988
26492
|
);
|
|
26493
|
+
void this.sendModel(copy.agentId).then(() => this.sendContexts());
|
|
25989
26494
|
}
|
|
25990
26495
|
/** The session of an agent's to copy: its room's, or where it is in none, the last it had. */
|
|
25991
26496
|
sessionToCopy(who, room) {
|
|
@@ -25993,6 +26498,304 @@ var Bridge = class {
|
|
|
25993
26498
|
if (room !== void 0 && room !== null) return sessions[`${who}|${room}`];
|
|
25994
26499
|
return Object.entries(sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
|
|
25995
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
|
+
}
|
|
25996
26799
|
// ------------------------------------------------------------------ what is said to it
|
|
25997
26800
|
/** A message for some of its agents: each takes a turn in its room, one after another there. */
|
|
25998
26801
|
async hear(message, own2 = {}) {
|
|
@@ -26104,6 +26907,10 @@ var Bridge = class {
|
|
|
26104
26907
|
run: run2,
|
|
26105
26908
|
speaker,
|
|
26106
26909
|
agent,
|
|
26910
|
+
who: room.who,
|
|
26911
|
+
agentId: speakerId ?? agent.agentId,
|
|
26912
|
+
peak: 0,
|
|
26913
|
+
folds: 0,
|
|
26107
26914
|
sessionId,
|
|
26108
26915
|
waiting: /* @__PURE__ */ new Set(),
|
|
26109
26916
|
taken: /* @__PURE__ */ new Set(),
|
|
@@ -26135,6 +26942,9 @@ var Bridge = class {
|
|
|
26135
26942
|
if (this.turns.get(sessionId) === turn) this.turns.delete(sessionId);
|
|
26136
26943
|
if (last.outcome === "succeeded") await this.finish(turn);
|
|
26137
26944
|
else await this.ended(turn, last);
|
|
26945
|
+
await this.sendModel(turn.agentId);
|
|
26946
|
+
await this.sendContext(turn.agentId);
|
|
26947
|
+
await this.turnHeld(turn);
|
|
26138
26948
|
}
|
|
26139
26949
|
/** The words into the room's session: the one it had, or a new one when OpenCode no longer has it. */
|
|
26140
26950
|
async give({ id: id2, who, agent, spaceId }, text2) {
|
|
@@ -26146,16 +26956,18 @@ var Bridge = class {
|
|
|
26146
26956
|
}
|
|
26147
26957
|
if (kept !== void 0) {
|
|
26148
26958
|
try {
|
|
26959
|
+
await this.modelled(kept, { who, agent });
|
|
26149
26960
|
return { sessionId: kept, given: await this.prompted(kept, text2) };
|
|
26150
26961
|
} catch (error62) {
|
|
26151
26962
|
if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) throw error62;
|
|
26152
26963
|
this.log(`${agent.config.name}'s session for ${spaceId} is gone from OpenCode: a new one`);
|
|
26153
26964
|
}
|
|
26154
26965
|
}
|
|
26966
|
+
const pick2 = state2.models?.[who];
|
|
26155
26967
|
const made = await opencode2.newSession({
|
|
26156
26968
|
title: `Chips space ${spaceId}`,
|
|
26157
26969
|
agent: agent.config.agent,
|
|
26158
|
-
model: agent.model,
|
|
26970
|
+
model: pick2 === void 0 ? agent.model : refOf(pick2),
|
|
26159
26971
|
directory: await this.folderFor({ who, agent }),
|
|
26160
26972
|
metadata: { chips_space: spaceId, chips_agent: who }
|
|
26161
26973
|
});
|
|
@@ -26292,6 +27104,11 @@ var Bridge = class {
|
|
|
26292
27104
|
return;
|
|
26293
27105
|
}
|
|
26294
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
|
+
}
|
|
26295
27112
|
const own2 = this.turns.get(sessionId);
|
|
26296
27113
|
if (own2 === void 0 && this.early.has(sessionId)) {
|
|
26297
27114
|
this.early.get(sessionId)?.push(event);
|
|
@@ -26463,6 +27280,9 @@ var Bridge = class {
|
|
|
26463
27280
|
const limit = await this.windowOf(model);
|
|
26464
27281
|
if (limit === void 0) return;
|
|
26465
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);
|
|
26466
27286
|
}
|
|
26467
27287
|
/** A model's context window, from OpenCode's catalogue, read once (and once more for each it lacked). */
|
|
26468
27288
|
async windowOf(model) {
|
|
@@ -26639,6 +27459,21 @@ var Bridge = class {
|
|
|
26639
27459
|
if (retryable) void this.sayHowItIs();
|
|
26640
27460
|
}
|
|
26641
27461
|
};
|
|
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
|
+
}
|
|
26642
27477
|
function decisionOf(decision, command = false) {
|
|
26643
27478
|
if (!decision.allowed) return "reject";
|
|
26644
27479
|
return decision.scope === "always" && !command ? "always" : "once";
|
|
@@ -26727,6 +27562,12 @@ function spoken(words4) {
|
|
|
26727
27562
|
return end2 === -1 ? words4 : words4.slice(end2 + THOUGHT_ENDS.length);
|
|
26728
27563
|
}
|
|
26729
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
|
+
}
|
|
26730
27571
|
function forkName(original) {
|
|
26731
27572
|
return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
|
|
26732
27573
|
}
|
|
@@ -26791,12 +27632,12 @@ function lastLine(said) {
|
|
|
26791
27632
|
}
|
|
26792
27633
|
|
|
26793
27634
|
// src/keep.ts
|
|
26794
|
-
import { homedir as
|
|
26795
|
-
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";
|
|
26796
27637
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
26797
27638
|
var flags = process.argv.slice(2);
|
|
26798
27639
|
var words3 = flags.filter((word) => !word.startsWith("--"));
|
|
26799
|
-
var whole = (word) => resolve2(word.startsWith("~/") ?
|
|
27640
|
+
var whole = (word) => resolve2(word.startsWith("~/") ? join7(homedir5(), word.slice(2)) : word);
|
|
26800
27641
|
var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
|
|
26801
27642
|
var self = fileURLToPath2(import.meta.url);
|
|
26802
27643
|
var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
|
|
@@ -26814,7 +27655,7 @@ function stopIfAsked(label2) {
|
|
|
26814
27655
|
if (stopped2.state === "theirs") end(`${stopped2.path} wasn't written by this bridge: it was left as it is.`, 1);
|
|
26815
27656
|
end(`Nothing called ${label2} was kept running here.`, 0);
|
|
26816
27657
|
}
|
|
26817
|
-
function keepIfAsked(label2, url2, names, log2,
|
|
27658
|
+
function keepIfAsked(label2, url2, names, log2, before2 = () => void 0) {
|
|
26818
27659
|
if (!flags.includes("--keep")) return;
|
|
26819
27660
|
const node2 = keptNode();
|
|
26820
27661
|
const kept = keepRunning({
|
|
@@ -26824,7 +27665,7 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
|
|
|
26824
27665
|
environment: keepEnvironment(url2, process.env, names, node2)
|
|
26825
27666
|
});
|
|
26826
27667
|
if (kept.state === "kept") {
|
|
26827
|
-
|
|
27668
|
+
before2();
|
|
26828
27669
|
end(
|
|
26829
27670
|
`Kept running as ${label2}: it starts at login, and again whenever it stops. What it says goes to ${kept.logFile}. \`${stopLine}\` stops it.`,
|
|
26830
27671
|
0
|
|
@@ -26844,9 +27685,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
|
|
|
26844
27685
|
// src/server.ts
|
|
26845
27686
|
import { spawn } from "node:child_process";
|
|
26846
27687
|
import { randomBytes } from "node:crypto";
|
|
26847
|
-
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";
|
|
26848
27689
|
import { createServer } from "node:net";
|
|
26849
|
-
import { delimiter as delimiter2, join as
|
|
27690
|
+
import { delimiter as delimiter2, join as join8 } from "node:path";
|
|
26850
27691
|
import { createInterface } from "node:readline";
|
|
26851
27692
|
var ServerError = class extends Error {
|
|
26852
27693
|
constructor(message, missing = false) {
|
|
@@ -26860,7 +27701,7 @@ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
|
26860
27701
|
var onPath = (command, env) => command.includes("/") ? existsSync5(command) : (env["PATH"] ?? "").split(delimiter2).some((folder2) => {
|
|
26861
27702
|
if (folder2 === "") return false;
|
|
26862
27703
|
try {
|
|
26863
|
-
accessSync2(
|
|
27704
|
+
accessSync2(join8(folder2, command), constants2.X_OK);
|
|
26864
27705
|
return true;
|
|
26865
27706
|
} catch {
|
|
26866
27707
|
return false;
|
|
@@ -26896,7 +27737,7 @@ async function answers2(url2, password) {
|
|
|
26896
27737
|
function keptIn(path) {
|
|
26897
27738
|
if (!existsSync5(path)) return void 0;
|
|
26898
27739
|
try {
|
|
26899
|
-
const kept = JSON.parse(
|
|
27740
|
+
const kept = JSON.parse(readFileSync6(path, "utf8"));
|
|
26900
27741
|
return typeof kept.port === "number" && typeof kept.password === "string" ? kept : void 0;
|
|
26901
27742
|
} catch {
|
|
26902
27743
|
return void 0;
|
|
@@ -26925,15 +27766,15 @@ async function ownServer(options) {
|
|
|
26925
27766
|
const env = options.env ?? process.env;
|
|
26926
27767
|
const command = options.command ?? "opencode";
|
|
26927
27768
|
const log2 = options.log ?? (() => void 0);
|
|
26928
|
-
const path =
|
|
27769
|
+
const path = join8(options.folder, "server.json");
|
|
26929
27770
|
let kept = keptIn(path);
|
|
26930
|
-
let
|
|
27771
|
+
let before2 = false;
|
|
26931
27772
|
if (kept !== void 0) {
|
|
26932
27773
|
const now = await answers2(`http://127.0.0.1:${kept.port}`, kept.password);
|
|
26933
|
-
|
|
27774
|
+
before2 = now === "ok";
|
|
26934
27775
|
if (now === "refused") kept = { port: await freePort(), password: kept.password };
|
|
26935
27776
|
}
|
|
26936
|
-
if (!
|
|
27777
|
+
if (!before2 && !onPath(command, env)) {
|
|
26937
27778
|
throw new ServerError("OpenCode isn't installed on this machine: install it (opencode.ai), then run this again.", true);
|
|
26938
27779
|
}
|
|
26939
27780
|
kept ??= { port: await freePort(), password: randomBytes(24).toString("base64url") };
|
|
@@ -27016,7 +27857,7 @@ async function ownServer(options) {
|
|
|
27016
27857
|
}
|
|
27017
27858
|
await ended(info["pid"], stopMs);
|
|
27018
27859
|
};
|
|
27019
|
-
if (
|
|
27860
|
+
if (before2) log2(`OpenCode's server from before is still answering at ${url2}`);
|
|
27020
27861
|
else {
|
|
27021
27862
|
await up(false);
|
|
27022
27863
|
log2(`OpenCode's own server answering at ${url2}`);
|
|
@@ -27049,9 +27890,9 @@ async function ownServer(options) {
|
|
|
27049
27890
|
}
|
|
27050
27891
|
|
|
27051
27892
|
// src/setup.ts
|
|
27052
|
-
import { existsSync as existsSync6, readFileSync as
|
|
27053
|
-
import { homedir as
|
|
27054
|
-
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";
|
|
27055
27896
|
var SetupError = class extends Error {
|
|
27056
27897
|
constructor(message, words4 = {}) {
|
|
27057
27898
|
super(message);
|
|
@@ -27068,9 +27909,9 @@ var hostOf = (url2) => {
|
|
|
27068
27909
|
}
|
|
27069
27910
|
};
|
|
27070
27911
|
var STILL_GOOD = "The link is still good: run the same command again once that's done.";
|
|
27071
|
-
var home = (path) => path.startsWith("~/") ?
|
|
27912
|
+
var home = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
|
|
27072
27913
|
function readConfig(path, env = process.env) {
|
|
27073
|
-
const raw = existsSync6(path) ? JSON.parse(
|
|
27914
|
+
const raw = existsSync6(path) ? JSON.parse(readFileSync7(path, "utf8")) : {};
|
|
27074
27915
|
const passwordEnv = typeof raw["passwordEnv"] === "string" ? raw["passwordEnv"] : "OPENCODE_PASSWORD";
|
|
27075
27916
|
const password = filledIn(env[passwordEnv]) ? env[passwordEnv] : void 0;
|
|
27076
27917
|
const ownServer2 = password === void 0 && typeof raw["url"] !== "string";
|
|
@@ -27126,7 +27967,7 @@ async function checkOpenCode(config3, client) {
|
|
|
27126
27967
|
}
|
|
27127
27968
|
|
|
27128
27969
|
// src/main.ts
|
|
27129
|
-
var home2 = (path) => path.startsWith("~/") ?
|
|
27970
|
+
var home2 = (path) => path.startsWith("~/") ? join10(homedir7(), path.slice(2)) : path;
|
|
27130
27971
|
var link = process.env["CHIPS_JOIN_LINK"] || void 0;
|
|
27131
27972
|
var LABEL = "chat.chips.opencode";
|
|
27132
27973
|
stopIfAsked(LABEL);
|
|
@@ -27146,13 +27987,13 @@ try {
|
|
|
27146
27987
|
} catch (error62) {
|
|
27147
27988
|
config2 = stop(error62);
|
|
27148
27989
|
}
|
|
27149
|
-
var folder =
|
|
27990
|
+
var folder = dirname5(configPath);
|
|
27150
27991
|
mkdirSync5(folder, { recursive: true, mode: 448 });
|
|
27151
|
-
var statePath =
|
|
27992
|
+
var statePath = join10(folder, "sessions.json");
|
|
27152
27993
|
var server;
|
|
27153
27994
|
if (link !== void 0) reportStep("checking", { detail: "Looking for OpenCode" });
|
|
27154
27995
|
if (config2.ownServer) {
|
|
27155
|
-
const cwd = config2.directory ??
|
|
27996
|
+
const cwd = config2.directory ?? dirname5(config2.folders);
|
|
27156
27997
|
mkdirSync5(cwd, { recursive: true });
|
|
27157
27998
|
try {
|
|
27158
27999
|
server = await ownServer({ folder, cwd, log });
|
|
@@ -27197,16 +28038,18 @@ var joined = await join3({
|
|
|
27197
28038
|
onCode: (code) => log(`Allow this harness in Chips: ${code}`)
|
|
27198
28039
|
}).catch(refused2);
|
|
27199
28040
|
keepIfAsked(LABEL, joined.url, [config2.passwordEnv], log, () => server?.stop());
|
|
27200
|
-
var state = existsSync7(statePath) ? JSON.parse(
|
|
28041
|
+
var state = existsSync7(statePath) ? JSON.parse(readFileSync8(statePath, "utf8")) : { sessions: {} };
|
|
27201
28042
|
var bridge;
|
|
27202
28043
|
var chips = await connect({
|
|
27203
28044
|
url: joined.url,
|
|
27204
28045
|
token: joined.token,
|
|
27205
28046
|
client: { name: "chips-opencode", version: "1.0.0" },
|
|
27206
|
-
store: fileStore(
|
|
28047
|
+
store: fileStore(join10(folder, "chips-state.json")),
|
|
27207
28048
|
// A person's Restart (Chips ADR 0094), heard only where the bridge runs OpenCode's server: one a
|
|
27208
28049
|
// person runs themselves isn't the bridge's to restart, so this connection doesn't say it hears them.
|
|
27209
|
-
...server === void 0 ? {} : { onRestart: () => bridge?.restart() }
|
|
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)
|
|
27210
28053
|
});
|
|
27211
28054
|
var git = await checkGit();
|
|
27212
28055
|
if (!git.ok) log(`rooms' projects are off: ${git.why}`);
|
|
@@ -27221,7 +28064,7 @@ bridge = new Bridge({
|
|
|
27221
28064
|
`, { mode: 384 }),
|
|
27222
28065
|
log,
|
|
27223
28066
|
restartServer: server?.restart,
|
|
27224
|
-
...config2.directory === void 0 ? { folders: config2.folders } : {}
|
|
28067
|
+
...config2.directory === void 0 ? { folders: config2.folders } : { directory: config2.directory }
|
|
27225
28068
|
});
|
|
27226
28069
|
await bridge.start();
|
|
27227
28070
|
log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through OpenCode at ${address.url}`);
|