@chipschat/opencode 1.0.6 → 1.0.9

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/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 readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
12
- import { homedir as homedir6 } from "node:os";
13
- import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
11
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
12
+ import { homedir as homedir7 } from "node:os";
13
+ import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
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 before = inner._zod.pattern?.source;
4874
+ const before2 = inner._zod.pattern?.source;
4875
4875
  const after = partPattern(inner);
4876
- if (own2 && before && after && after !== before) {
4877
- return own2.replace(cleanRegex(before), () => cleanRegex(after));
4876
+ if (own2 && before2 && after && after !== before2) {
4877
+ return own2.replace(cleanRegex(before2), () => cleanRegex(after));
4878
4878
  }
4879
4879
  return own2;
4880
4880
  }
@@ -19943,8 +19943,9 @@ var policyRule = external_exports.object({
19943
19943
  external: external_exports.boolean().optional(),
19944
19944
  irreversible: external_exports.boolean().optional(),
19945
19945
  money_usd_over: external_exports.number().nonnegative().optional(),
19946
- risk: external_exports.array(riskClass).max(8).optional()
19947
- }).optional().describe("Extra conditions on the action's flags."),
19946
+ risk: external_exports.array(riskClass).max(8).optional(),
19947
+ starts_with: external_exports.array(external_exports.string().min(1).max(400)).min(1).max(8).optional().describe("For an action that carries a command (ADR 0239): the words a part of it starts with. A command goes ahead on such rules only when every part starts with one that allows it.")
19948
+ }).optional().describe("Extra conditions on the action's flags, and for a command, how its parts start."),
19948
19949
  decision: external_exports.enum(["allow", "ask", "deny"]).describe("What to do when the rule matches."),
19949
19950
  approvers: external_exports.array(approverRef).max(16).optional().describe("Who may answer, when the decision is `ask`."),
19950
19951
  reason: external_exports.string().max(400).optional().describe("Shown to the harness, and recorded in the audit log."),
@@ -20463,9 +20464,17 @@ var roomIdle = external_exports.object({
20463
20464
  var roomNote = external_exports.object({
20464
20465
  members: external_exports.array(roomMember).max(500).describe("The room's people, then its agents, each by name, anybody removed left out."),
20465
20466
  notes: external_exports.record(external_exports.string().max(120), external_exports.string().max(64e3)).describe("The note for each who takes the turn (`for`), by their id: who is here, with handles, and that only those @-mentioned act. A harness puts it at the top of the turn."),
20467
+ rules: external_exports.record(external_exports.string().max(120), external_exports.string().max(4e3)).optional().describe("How the space works, for each who takes the turn, by their id: the words its note ends with (ADR 0250), that everyone here reads what it writes and, beside other agents, what an @ does and when to answer NO_REPLY. They stay the same while the space's agents do, so a harness whose session keeps its earlier turns may say them once, and the note without them after."),
20466
20468
  working: external_exports.array(roomWork).max(500).optional().describe("The room's agents with a turn going as Chips sends this one (ADR 0104), the takers' own included. A turn nothing has been heard from for ten minutes is not counted."),
20467
20469
  idle: external_exports.array(roomIdle).max(500).optional().describe("The room's other agents that have had a turn here: how long since the last one ended (ADR 0104).")
20468
20470
  });
20471
+ function noteWithoutRules(room, taker) {
20472
+ const note = room?.notes[taker];
20473
+ const rules = room?.rules?.[taker];
20474
+ if (note === void 0 || rules === void 0 || !note.endsWith(rules))
20475
+ return note;
20476
+ return note.slice(0, note.length - rules.length).trimEnd();
20477
+ }
20469
20478
  var MONTHS = [
20470
20479
  "January",
20471
20480
  "February",
@@ -20485,8 +20494,8 @@ var MONTHS = [
20485
20494
  var CONTEXT_CEILING = { lines: 200, chars: 32e3 };
20486
20495
  var CONTEXT_HEADING = "Said in this space since you last spoke:";
20487
20496
  var contextBudget = external_exports.object({
20488
- lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 20 when absent. Chips hands over 200 at most, and none when this is 0."),
20489
- chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 4,000 when absent. Chips hands over 32,000 at most.")
20497
+ lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 40 when absent. Chips hands over 200 at most, and none when this is 0."),
20498
+ chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 16,000 when absent. Chips hands over 32,000 at most.")
20490
20499
  }).describe("How much of the room a harness is handed with a message for it (ADR 0088). Its own to say: a budget promises nothing to anybody else.");
20491
20500
  var contextLine = external_exports.object({
20492
20501
  seq: external_exports.int().positive().describe("Where in the log it happened."),
@@ -20698,12 +20707,14 @@ var contextAnswer = {
20698
20707
  message: external_exports.string().min(1).max(300).optional().describe("What went wrong, for the person.")
20699
20708
  }).optional().describe("An edit not made. The copy beside it is the file as it is now.")
20700
20709
  };
20710
+ var readNote = external_exports.string().min(1).max(300).describe("When the agent reads these, in the harness's words: `Hermes reads these when a conversation starts.` Absent: at the start of every turn.");
20701
20711
  function uniqueNames(files) {
20702
20712
  return new Set(files.map((file3) => file3.name)).size === files.length;
20703
20713
  }
20704
20714
  var agentContextPut = external_exports.object({
20705
20715
  files: external_exports.array(contextFile).max(CONTEXT_FILES_SHOWN).refine(uniqueNames, { message: "Each file is named once." }).describe("The agent's files, in the order its page shows them: eight at most."),
20706
20716
  not_shown: external_exports.array(notShownFile).max(CONTEXT_NOT_SHOWN_MAX).optional(),
20717
+ read_note: readNote.optional(),
20707
20718
  ...contextAnswer
20708
20719
  }).describe("`PUT /v1/agents/:id/context`: a harness's copy of one of its agents' files.");
20709
20720
  var harnessContextPut = external_exports.object({
@@ -20736,6 +20747,7 @@ var agentContextView = external_exports.object({
20736
20747
  harness_id: id("harness"),
20737
20748
  files: external_exports.array(contextFile),
20738
20749
  not_shown: external_exports.array(notShownFile),
20750
+ read_note: readNote.optional(),
20739
20751
  read_at: isoTimestamp.describe("When the harness last sent its copy."),
20740
20752
  away: external_exports.boolean().describe("No socket of the harness hears edits now: read, but not saved.")
20741
20753
  }).describe("`GET /v1/agents/:id/context`, for the harness's owner.");
@@ -20863,7 +20875,7 @@ var messageNew = defineEvent({
20863
20875
  summary: "Someone said something, with any attachments as signed URLs the harness can fetch.",
20864
20876
  requiresRun: false,
20865
20877
  payload: deliveredMessage.extend({
20866
- context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 20 lines and 4,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
20878
+ context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 40 lines and 16,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
20867
20879
  room: roomNote.optional().describe("Who is in the room, with the handle that reaches each, and a note for each who takes the turn saying so and what an @ does (ADR 0092). A harness puts `notes[<taker>]` at the top of that turn. Absent on a message in no room."),
20868
20880
  exchange: deliveredExchange.optional().describe("How the agents' talk in the room stands, once it is worth a word (ADR 0097): from eight messages between agents since a person last acted or an agent last made something new. A harness puts `note` at the top of the turn, as Chips's, ahead of the room's note. Absent below that, and worked out from the log, so a replay hands over the same facts."),
20869
20881
  handed: external_exports.literal(true).optional().describe("Chips held this message when it was said and hands it over now a person has acted (ADR 0097): the newest of an agent's held messages, once; or, with `lead`, hands it to the room's lead. Its seq and id are the hand-over's, its `message_id` the message's own."),
@@ -20913,6 +20925,7 @@ var approvalResolved = defineEvent({
20913
20925
  approval_id: id("approval"),
20914
20926
  decision: external_exports.enum(["approved", "edited", "denied", "expired"]).describe("`expired` means nobody answered and the fallback applied."),
20915
20927
  scope: external_exports.enum(["once", "session", "always"]).default("once").describe("How far the answer reaches. `always` becomes a policy rule."),
20928
+ everywhere: external_exports.boolean().optional().describe("With `always`: the rule is the harness's in every space, not only this one (ADR 0239). An owner or an admin only."),
20916
20929
  updated_input: external_exports.unknown().optional().describe("The edited action input, when the decision is `edited`."),
20917
20930
  reason: external_exports.string().max(4e3).optional().describe("Why. The harness gets to read this."),
20918
20931
  resolved_by: external_exports.string().min(1).max(120).describe("The participant who answered, or `system` on expiry \u2014 a harness when it settles its own ask, answered in its own tools. Filled in by Chips from the credential that sent it; a value the sender writes is replaced."),
@@ -21177,6 +21190,10 @@ function namesInWords(names) {
21177
21190
  return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
21178
21191
  }
21179
21192
  function renderProjectNote(facts, options) {
21193
+ return `${renderProjectFacts(facts)}
21194
+ ${renderProjectAsks(facts, options)}`;
21195
+ }
21196
+ function renderProjectFacts(facts) {
21180
21197
  const files = `${facts.files} file${facts.files === 1 ? "" : "s"}`;
21181
21198
  const lines2 = [
21182
21199
  `The space's project is a git repository. Your copy is ${facts.folder} (${files}), ${facts.trouble === void 0 ? "brought up to date just now" : "not brought up to date"}.`
@@ -21194,9 +21211,9 @@ function renderProjectNote(facts, options) {
21194
21211
  for (const key of facts.keys) {
21195
21212
  lines2.push(`${key.path} holds what looks like ${key.what}, so your last change wasn't sent: take it out and read it from outside the project.`);
21196
21213
  }
21197
- return [...lines2, ...asks(facts, options)].join("\n");
21214
+ return lines2.join("\n");
21198
21215
  }
21199
- function asks(facts, options) {
21216
+ function renderProjectAsks(facts, options) {
21200
21217
  const lines2 = [
21201
21218
  "Work on the project in that folder and leave git to Chips: when your turn ends, what you changed is committed under your name and sent to everyone here. Keep scratch work outside it.",
21202
21219
  // Sent with the answer too, a project's file became a room file of its own, with versions of
@@ -21217,7 +21234,7 @@ function asks(facts, options) {
21217
21234
  if (facts.pages === true) {
21218
21235
  lines2.push("A page opens in Chips as one document: name every file it needs in its tags and imports (<link>, <script src>, <img src>, import), since it can't fetch files while it runs.");
21219
21236
  }
21220
- return lines2;
21237
+ return lines2.join("\n");
21221
21238
  }
21222
21239
 
21223
21240
  // node_modules/@chipschat/protocol/dist/src/events/harness-to-chips.js
@@ -21869,7 +21886,7 @@ var membershipRemoved = defineEvent({
21869
21886
  payload: external_exports.object({
21870
21887
  participant_id: external_exports.string().min(1).max(120).describe("Who left."),
21871
21888
  removed_by: external_exports.string().min(1).max(120).optional().describe("Who removed them, if not themselves."),
21872
- reason: external_exports.enum([...RELEASE_REASONS, "person_removed"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160). Written by Chips; absent, a person took them out or they left."),
21889
+ reason: external_exports.enum([...RELEASE_REASONS, "person_removed", "person_left"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160); `person_left`: they deleted their account and left it (ADR 0243). Written by Chips; absent, a person took them out or they left the room."),
21873
21890
  moved_to: id("space").optional().describe("For an agent moved away: the room it went to, so this room can say so.")
21874
21891
  })
21875
21892
  });
@@ -22178,14 +22195,14 @@ var personRemoved = defineEvent({
22178
22195
  type: "person.removed",
22179
22196
  direction: "workspace",
22180
22197
  sender: "chips",
22181
- summary: "A person was removed from the workspace: signed out everywhere, out of every room, and the harnesses they owned handed to whoever removed them. What they said stays, under their name.",
22198
+ summary: "A person was removed from the workspace, or deleted their account: signed out everywhere, out of every room, and the harnesses they owned handed on. What they said stays, under their name.",
22182
22199
  requiresRun: false,
22183
22200
  payload: external_exports.object({
22184
22201
  participant_id: id("person").describe("Who was removed."),
22185
22202
  display_name: external_exports.string().min(1).max(120).describe("What they were called when they were removed, so the audit needs no lookup."),
22186
- removed_by: id("person").describe("The owner or admin who removed them."),
22203
+ removed_by: id("person").describe("The owner or admin who removed them, or they themselves when they deleted their account (ADR 0243)."),
22187
22204
  spaces: external_exports.array(id("space")).max(1e3).optional().describe("The rooms they were taken out of, each with its own `membership.removed`."),
22188
- harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, handed to `removed_by` by `harness.transferred`.")
22205
+ harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, each handed on by `harness.transferred`: to `removed_by`, or, for an account deleted, to an owner.")
22189
22206
  })
22190
22207
  });
22191
22208
  var joinWho = {
@@ -22401,6 +22418,89 @@ var EVENT_TYPES = EVENTS.map((event) => event.type);
22401
22418
  var byType = new Map(EVENTS.map((event) => [event.type, event]));
22402
22419
  var schemas = new Map(EVENTS.map((event) => [event.type, eventSchema(event)]));
22403
22420
 
22421
+ // node_modules/@chipschat/protocol/dist/src/names.js
22422
+ var LOOK_LIKE = {
22423
+ A: "\u0410\u0391",
22424
+ // Cyrillic А, Greek Α
22425
+ B: "\u0412\u0392",
22426
+ // Cyrillic В, Greek Β
22427
+ C: "\u0421",
22428
+ // Cyrillic С
22429
+ E: "\u0415\u0395",
22430
+ // Cyrillic Е, Greek Ε
22431
+ H: "\u041D\u0397",
22432
+ // Cyrillic Н, Greek Η
22433
+ I: "\u0406\u04C0\u0399",
22434
+ // Cyrillic І and Ӏ, Greek Ι
22435
+ J: "\u0408",
22436
+ // Cyrillic Ј
22437
+ K: "\u041A\u039A",
22438
+ // Cyrillic К, Greek Κ
22439
+ M: "\u041C\u039C",
22440
+ // Cyrillic М, Greek Μ
22441
+ N: "\u039D",
22442
+ // Greek Ν
22443
+ O: "\u041E\u039F",
22444
+ // Cyrillic О, Greek Ο
22445
+ P: "\u0420\u03A1",
22446
+ // Cyrillic Р, Greek Ρ
22447
+ Q: "\u051A",
22448
+ // Cyrillic Ԛ
22449
+ S: "\u0405",
22450
+ // Cyrillic Ѕ
22451
+ T: "\u0422\u03A4",
22452
+ // Cyrillic Т, Greek Τ
22453
+ W: "\u051C",
22454
+ // Cyrillic Ԝ
22455
+ X: "\u0425\u03A7",
22456
+ // Cyrillic Х, Greek Χ
22457
+ Y: "\u0423\u04AE\u03A5",
22458
+ // Cyrillic У and Ү, Greek Υ
22459
+ Z: "\u0396",
22460
+ // Greek Ζ
22461
+ a: "\u0430\u03B1",
22462
+ // Cyrillic а, Greek α
22463
+ c: "\u0441",
22464
+ // Cyrillic с
22465
+ d: "\u0501",
22466
+ // Cyrillic ԁ
22467
+ e: "\u0435",
22468
+ // Cyrillic е
22469
+ h: "\u04BB",
22470
+ // Cyrillic һ
22471
+ i: "\u0456\u03B9\u0131",
22472
+ // Cyrillic і, Greek ι, Latin dotless ı
22473
+ j: "\u0458",
22474
+ // Cyrillic ј
22475
+ k: "\u03BA",
22476
+ // Greek κ
22477
+ l: "\u04CF",
22478
+ // Cyrillic ӏ
22479
+ o: "\u043E\u03BF",
22480
+ // Cyrillic о, Greek ο
22481
+ p: "\u0440\u03C1",
22482
+ // Cyrillic р, Greek ρ
22483
+ q: "\u051B",
22484
+ // Cyrillic ԛ
22485
+ s: "\u0455",
22486
+ // Cyrillic ѕ
22487
+ u: "\u03C5",
22488
+ // Greek υ
22489
+ v: "\u03BD",
22490
+ // Greek ν
22491
+ w: "\u051D",
22492
+ // Cyrillic ԝ
22493
+ x: "\u0445\u03C7",
22494
+ // Cyrillic х, Greek χ
22495
+ y: "\u0443\u04AF"
22496
+ // Cyrillic у and ү
22497
+ };
22498
+ var LATIN_OF = new Map(Object.entries(LOOK_LIKE).flatMap(([latin, others]) => [...others].map((other) => [other, latin])));
22499
+ var HIDDEN = "\\p{Cc}\\p{Cf}\\u034f\\u115f\\u1160\\u3164\\uffa0\\u2800";
22500
+ var UNSEEN = new RegExp(`[${HIDDEN}\\ufe00-\\ufe0f\\u{e0100}-\\u{e01ef}]`, "gu");
22501
+ var HIDDEN_ONE = new RegExp(`[${HIDDEN}]`, "u");
22502
+ var ALPHABETS = [new RegExp("\\p{Script=Latin}", "u"), new RegExp("\\p{Script=Cyrillic}", "u"), new RegExp("\\p{Script=Greek}", "u")];
22503
+
22404
22504
  // node_modules/@chipschat/protocol/dist/src/first-names.js
22405
22505
  var AGENT_FIRST_NAMES = [
22406
22506
  "Abel Aldo Amir Amos Anders Ansel Anton Ari Arlo Arne Aron Arvid Asher Avi Axel Aziz",
@@ -22448,7 +22548,7 @@ var clientHelloFrame = external_exports.object({
22448
22548
  })).max(64).optional().describe("Not read by Chips: a connection is its token's, and the server's `hello` names the token's harness and its agents (ADR 0087). Absent for a person's client."),
22449
22549
  since: external_exports.int().nonnegative().optional().describe("Not read by Chips: a stream resumes from `subscribe`'s `since`."),
22450
22550
  accepts: external_exports.array(external_exports.string().min(1).max(40)).max(32).optional().describe("Server frames beyond the base set this client understands, by the names in `SERVER_EXTENSIONS`. A name the server does not know is ignored rather than refused, so a client may name a frame a newer server sends and still be answered by an older one (ADR 0211, Phase 3). A client that says `events` is sent its replay in pages rather than one frame per event; one that says `presence` is sent an agent's state as it changes (ADR 0083); a harness's socket that says `restart` is sent `restart` when a person asks it to restart (ADR 0094); an owner's or admin's that says `join` is sent each join request as it changes (ADR 0122); a harness's that says `context` is sent `context.edit` when a person changes one of its context files (ADR 0134); a harness's that says `models` is sent `model.change` when a person changes an agent's model or effort (ADR 0140); a harness's that says `heartbeats` is sent `heartbeat.change` when a person switches an agent's heartbeat or changes how often it beats (ADR 0143); a harness's that says `context_windows` is sent `context_window.compact` and `context_window.change` when a person asks an agent to compact now or changes the limit it compacts itself at (ADR 0146); a person's that says `folders` is sent `folder.open` when they ask, from another of their devices, for a room's folder to be opened in Finder (ADR 0225), and `folder.run` when they ask for its project to be run or stopped there (ADR 0226); a person's that says `folder_runs` is sent `folder.run_state` when their Mac says how a run of theirs stands (ADR 0226); one that says nothing is sent what it was always sent, since an unknown frame is dropped in silence."),
22451
- context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 20 lines and 4,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
22551
+ context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 40 lines and 16,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
22452
22552
  }).describe("The first frame a client sends.");
22453
22553
  var subscribeFrame = external_exports.object({
22454
22554
  frame: external_exports.literal("subscribe"),
@@ -25010,8 +25110,19 @@ async function redeem(url2, secret, options, send, machine) {
25010
25110
  throw await refused(answered);
25011
25111
  return await answered.json();
25012
25112
  }
25113
+ function reaching(send, url2) {
25114
+ return async (input2, init) => {
25115
+ try {
25116
+ return await send(input2, init);
25117
+ } catch (error62) {
25118
+ if (error62 instanceof Error && error62.name === "AbortError")
25119
+ throw error62;
25120
+ throw new JoinError("unreachable", `Can't reach Chips at ${url2} from this computer. It has to be on the same network as Chips, on its tailnet, or Chips has to be at an address this computer can reach.`);
25121
+ }
25122
+ };
25123
+ }
25013
25124
  function askOrRedeem(server2, options) {
25014
- const send = options.fetch ?? fetch;
25125
+ const send = reaching(options.fetch ?? fetch, server2.url);
25015
25126
  const machine = options.machine ?? hostname3();
25016
25127
  return server2.secret === void 0 ? ask(server2.url, options, send, machine) : redeem(server2.url, server2.secret, options, send, machine);
25017
25128
  }
@@ -25177,14 +25288,31 @@ async function sendAnswer(answer, options) {
25177
25288
  }
25178
25289
 
25179
25290
  // node_modules/@chipschat/harness/dist/prompt.js
25291
+ function takerOf(message, options) {
25292
+ return options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
25293
+ }
25294
+ function othersBeside(message, taker) {
25295
+ return (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
25296
+ }
25297
+ function rulesFor(message, options = {}) {
25298
+ const taker = takerOf(message, options);
25299
+ const { project } = options;
25300
+ const rules = message.room?.rules ?? {};
25301
+ return [
25302
+ taker === void 0 ? void 0 : rules[taker],
25303
+ project === void 0 ? void 0 : renderProjectAsks(project, { othersInRoom: othersBeside(message, taker) }),
25304
+ options.sendsFiles === true ? sendingFiles({ project: project !== void 0 }) : void 0
25305
+ ].filter((part) => part !== void 0 && part !== "").join("\n\n");
25306
+ }
25180
25307
  function promptFor(message, options = {}) {
25181
- const taker = options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
25308
+ const taker = takerOf(message, options);
25309
+ const held = options.rulesHeld === true;
25182
25310
  const notes = message.room?.notes ?? {};
25183
- const note = taker === void 0 ? void 0 : notes[taker];
25311
+ const note = taker === void 0 ? void 0 : held ? noteWithoutRules(message.room, taker) : notes[taker];
25184
25312
  const who = options.sayWho === false ? "" : `${withRole(message.from.display_name, message.from)}: `;
25185
25313
  const files = (message.attachments ?? []).map(renderAttachment);
25186
25314
  const words4 = [`${who}${message.text}`, ...files].join("\n");
25187
- const sending = options.sendsFiles === true ? sendingFiles({ project: options.project !== void 0 }) : void 0;
25315
+ const sending = options.sendsFiles === true && !held ? sendingFiles({ project: options.project !== void 0 }) : void 0;
25188
25316
  const replying = renderReplyingTo(message.replying_to, {
25189
25317
  sender: {
25190
25318
  id: message.from.participant_id,
@@ -25196,8 +25324,7 @@ function promptFor(message, options = {}) {
25196
25324
  at: message.ts,
25197
25325
  zone: options.zone
25198
25326
  });
25199
- const others = (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
25200
- const project = options.project === void 0 ? void 0 : renderProjectNote(options.project, { othersInRoom: others });
25327
+ const project = options.project === void 0 ? void 0 : held ? renderProjectFacts(options.project) : renderProjectNote(options.project, { othersInRoom: othersBeside(message, taker) });
25201
25328
  return [
25202
25329
  message.exchange?.note,
25203
25330
  note,
@@ -25209,6 +25336,24 @@ function promptFor(message, options = {}) {
25209
25336
  ].filter((part) => part !== void 0 && part !== "").join("\n\n");
25210
25337
  }
25211
25338
 
25339
+ // node_modules/@chipschat/harness/dist/rules.js
25340
+ var SessionRules = class {
25341
+ held = /* @__PURE__ */ new Map();
25342
+ /** Whether `session` holds these rules: told them word for word, or there are none to hold. */
25343
+ holds(session, rules) {
25344
+ return rules === "" || this.held.get(session) === rules;
25345
+ }
25346
+ /** That `session` was told these rules: its turn that said them was given to it. */
25347
+ told(session, rules) {
25348
+ if (rules !== "")
25349
+ this.held.set(session, rules);
25350
+ }
25351
+ /** That `session` no longer holds what it was told: it compacted, or it is gone. */
25352
+ forget(session) {
25353
+ this.held.delete(session);
25354
+ }
25355
+ };
25356
+
25212
25357
  // node_modules/@chipschat/harness/dist/service.js
25213
25358
  import { execFileSync as execFileSync2 } from "node:child_process";
25214
25359
  import { accessSync, chmodSync as chmodSync2, constants, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
@@ -25384,7 +25529,10 @@ function nodeOnPath(env = process.env) {
25384
25529
  }
25385
25530
  var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
25386
25531
  var LOAD_TRIES = 40;
25387
- var runOf = (options) => options.run ?? ((command) => void execFileSync2(command[0], command.slice(1), { stdio: "ignore" }));
25532
+ var runOf = (options) => options.run ?? ((command) => execFileSync2(command[0], command.slice(1), {
25533
+ encoding: "utf8",
25534
+ stdio: ["ignore", "pipe", "ignore"]
25535
+ }));
25388
25536
  var ours = (path) => !existsSync3(path) || readFileSync3(path, "utf8").includes(MARK);
25389
25537
  function runOne(command, run2, retryMs) {
25390
25538
  const tries = command[0] === "launchctl" && command[1] === "bootstrap" ? LOAD_TRIES : 1;
@@ -25407,6 +25555,24 @@ function runAll(commands, options) {
25407
25555
  runOne(command, run2, options.retryMs ?? 250);
25408
25556
  }
25409
25557
  var uidOf = (options) => options.uid ?? process.getuid?.() ?? 0;
25558
+ function lingers(options) {
25559
+ const run2 = runOf(options);
25560
+ const uid = String(uidOf(options));
25561
+ try {
25562
+ const on = run2(["loginctl", "show-user", uid, "--property=Linger", "--value"]);
25563
+ if (typeof on === "string" && on.trim() === "yes")
25564
+ return true;
25565
+ } catch {
25566
+ }
25567
+ try {
25568
+ run2(["loginctl", "enable-linger", uid]);
25569
+ return true;
25570
+ } catch {
25571
+ return false;
25572
+ }
25573
+ }
25574
+ var sayOf = (options) => options.say ?? ((line) => void process.stderr.write(`${line}
25575
+ `));
25410
25576
  function keepRunning(spec, options = {}) {
25411
25577
  if (machineOf(options.machine).env["CHIPS_KEEP"] === "off")
25412
25578
  return { state: "off" };
@@ -25421,6 +25587,9 @@ function keepRunning(spec, options = {}) {
25421
25587
  writeFileSync2(file3.path, file3.contents, { mode: 384 });
25422
25588
  chmodSync2(file3.path, 384);
25423
25589
  runAll(startCommands(file3.kind, spec.label, file3.path, uidOf(options)), options);
25590
+ if (file3.kind === "systemd" && !lingers(options)) {
25591
+ sayOf(options)("It stops when you log out. To keep it running on a machine nobody logs in to, run this once: sudo loginctl enable-linger $USER");
25592
+ }
25424
25593
  return { state: "kept", path: file3.path, logFile: file3.logFile };
25425
25594
  }
25426
25595
  function stopKeeping(label2, options = {}) {
@@ -25467,7 +25636,270 @@ function fileStore(path) {
25467
25636
 
25468
25637
  // src/bridge.ts
25469
25638
  import { mkdirSync as mkdirSync4 } from "node:fs";
25470
- import { join as join5 } from "node:path";
25639
+ import { homedir as homedir4 } from "node:os";
25640
+ import { join as join6 } from "node:path";
25641
+
25642
+ // src/context.ts
25643
+ import { createHash as createHash2 } from "node:crypto";
25644
+ import { globSync, readFileSync as readFileSync5, statSync } from "node:fs";
25645
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
25646
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative, sep as sep2 } from "node:path";
25647
+ var AGENTS_MD = "AGENTS.md";
25648
+ function placesOf(env, home3) {
25649
+ const config3 = env["OPENCODE_CONFIG_DIR"] || join5(env["XDG_CONFIG_HOME"] || join5(home3, ".config"), "opencode");
25650
+ return { home: home3, config: config3 };
25651
+ }
25652
+ function hashOf(text2) {
25653
+ return createHash2("sha256").update(text2, "utf8").digest("hex");
25654
+ }
25655
+ function isFile(path) {
25656
+ try {
25657
+ return statSync(path).isFile();
25658
+ } catch {
25659
+ return false;
25660
+ }
25661
+ }
25662
+ function textOf(path) {
25663
+ try {
25664
+ return readFileSync5(path, "utf8");
25665
+ } catch {
25666
+ return void 0;
25667
+ }
25668
+ }
25669
+ var within = (path, dir) => path === dir || path.startsWith(`${dir}${sep2}`);
25670
+ function upTo(dir, stop2) {
25671
+ const dirs = [dir];
25672
+ for (let at = dir; within(at, stop2) && at !== stop2 && dirname4(at) !== at; ) {
25673
+ at = dirname4(at);
25674
+ dirs.push(at);
25675
+ }
25676
+ return dirs;
25677
+ }
25678
+ function namedFiles(sources, folder2, root, home3) {
25679
+ const found = [];
25680
+ const glob = (pattern, cwd) => {
25681
+ try {
25682
+ return globSync(pattern, { cwd }).map((match) => join5(cwd, match));
25683
+ } catch {
25684
+ return [];
25685
+ }
25686
+ };
25687
+ for (const source2 of sources) {
25688
+ if (/^https?:\/\//i.test(source2)) continue;
25689
+ const expanded = source2.startsWith("~/") ? join5(home3, source2.slice(2)) : source2;
25690
+ 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));
25691
+ for (const path of matches.sort()) if (isFile(path) && !found.includes(path)) found.push(path);
25692
+ }
25693
+ return found;
25694
+ }
25695
+ function before(version3, than) {
25696
+ const parts = (each) => each.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
25697
+ const [a, b] = [parts(version3), parts(than)];
25698
+ for (let at = 0; at < 3; at += 1) if ((a[at] ?? 0) !== (b[at] ?? 0)) return (a[at] ?? 0) < (b[at] ?? 0);
25699
+ return false;
25700
+ }
25701
+ var READS_NAMED = "2.0.30";
25702
+ var ContextFiles = class {
25703
+ constructor(places, folders) {
25704
+ this.places = places;
25705
+ this.folders = folders;
25706
+ }
25707
+ places;
25708
+ folders;
25709
+ /** `~/chips-opencode/agents/AGENTS.md`, or the whole path where it isn't in the home. */
25710
+ tilde(path) {
25711
+ const { home: home3 } = this.places;
25712
+ return within(path, home3) ? `~${path.slice(home3.length)}` : path;
25713
+ }
25714
+ /** A shared file's name: where it is, a step at a time (`~ › .config › opencode › AGENTS.md`), with no slash. */
25715
+ crumbs(path) {
25716
+ const { home: home3 } = this.places;
25717
+ const steps = within(path, home3) ? ["~", ...relative(home3, path).split(sep2)] : ["/", ...path.split(sep2)];
25718
+ return steps.filter((step) => step !== "").join(" \u203A ").slice(0, 255);
25719
+ }
25720
+ /**
25721
+ * The harness's own set, for agents whose folders read as `readings` says: every agent's
25722
+ * `AGENTS.md`, those above it, the person's own, and the named files not in an agent's folder.
25723
+ */
25724
+ sharedSpots(readings) {
25725
+ const { places, folders } = this;
25726
+ const spots = [];
25727
+ const add = (spot) => {
25728
+ if (!spots.some((each) => each.path === spot.path)) spots.push(spot);
25729
+ };
25730
+ const reaches = (dir) => (folder2) => within(folder2, dir) && within(dir, readings.get(folder2)?.stop ?? folder2);
25731
+ if (folders !== void 0 && within(folders, places.home)) {
25732
+ const path = join5(folders, AGENTS_MD);
25733
+ add({
25734
+ path,
25735
+ name: this.crumbs(path),
25736
+ title: "For every OpenCode agent",
25737
+ about: this.tilde(path),
25738
+ editable: true,
25739
+ id: "every-agent",
25740
+ over: reaches(folders)
25741
+ });
25742
+ }
25743
+ const yours = join5(places.config, AGENTS_MD);
25744
+ for (const [folder2, reading] of readings) {
25745
+ for (const dir of upTo(folder2, reading.stop).slice(1)) {
25746
+ const path = join5(dir, AGENTS_MD);
25747
+ if (path === yours || !isFile(path)) continue;
25748
+ add({
25749
+ path,
25750
+ name: this.crumbs(path),
25751
+ title: `From ${this.tilde(dir)}`.slice(0, 80),
25752
+ about: this.tilde(path),
25753
+ editable: false,
25754
+ whyReadOnly: "It sits above the agents' folders.",
25755
+ id: `above-${hashOf(path).slice(0, 12)}`,
25756
+ over: reaches(dir)
25757
+ });
25758
+ }
25759
+ }
25760
+ if (isFile(yours)) {
25761
+ add({
25762
+ path: yours,
25763
+ name: this.crumbs(yours),
25764
+ title: "For all your OpenCode",
25765
+ about: this.tilde(yours),
25766
+ editable: false,
25767
+ whyReadOnly: "Your own OpenCode reads it too.",
25768
+ id: "yours"
25769
+ });
25770
+ }
25771
+ for (const [folder2, reading] of readings) {
25772
+ for (const path of reading.named) {
25773
+ if (within(path, folder2)) continue;
25774
+ add({
25775
+ path,
25776
+ name: this.crumbs(path),
25777
+ title: "Named in OpenCode's config",
25778
+ about: this.tilde(path),
25779
+ editable: false,
25780
+ whyReadOnly: "OpenCode's config names it for every session.",
25781
+ id: `named-${hashOf(path).slice(0, 12)}`,
25782
+ over: (other) => readings.get(other)?.named.includes(path) === true
25783
+ });
25784
+ }
25785
+ }
25786
+ return spots;
25787
+ }
25788
+ /** An agent's own files: its folder's `AGENTS.md`, and the named files in its folder. */
25789
+ ownSpots(folder2, reading) {
25790
+ const named = reading.named.filter((path) => within(path, folder2) && path !== join5(folder2, AGENTS_MD));
25791
+ return [
25792
+ { path: join5(folder2, AGENTS_MD), name: AGENTS_MD, title: "How it works", editable: true },
25793
+ ...named.map((path) => ({
25794
+ path,
25795
+ name: relative(folder2, path).split(sep2).join(" \u203A ").slice(0, 255),
25796
+ title: "Named in OpenCode's config",
25797
+ editable: true
25798
+ }))
25799
+ ];
25800
+ }
25801
+ /** The shared files an agent in `folder` reads. */
25802
+ readBy(folder2, reading) {
25803
+ return this.sharedSpots(/* @__PURE__ */ new Map([[folder2, reading]])).filter((spot) => spot.over === void 0 || spot.over(folder2));
25804
+ }
25805
+ /** An agent's copy: its own files and the shared ones it reads, eight at most. */
25806
+ agentCopy(folder2, reading) {
25807
+ const rows = [];
25808
+ const notShown = [];
25809
+ const add = (row, text2) => {
25810
+ const named = { name: row["name"], title: row["title"] };
25811
+ if (text2 !== void 0 && contextBytes(text2) > CONTEXT_FILE_BYTES) notShown.push({ ...named, reason: "too_large" });
25812
+ else if (rows.length >= CONTEXT_FILES_SHOWN) notShown.push({ ...named, reason: "count" });
25813
+ else rows.push(row);
25814
+ };
25815
+ for (const spot of this.ownSpots(folder2, reading)) {
25816
+ const text2 = textOf(spot.path) ?? "";
25817
+ add({ name: spot.name, title: spot.title, role: "instructions", text: text2, hash: hashOf(text2), editable: true }, text2);
25818
+ }
25819
+ for (const spot of this.readBy(folder2, reading)) {
25820
+ add({
25821
+ name: spot.name,
25822
+ title: spot.title,
25823
+ role: "instructions",
25824
+ ...spot.about === void 0 ? {} : { about: spot.about },
25825
+ shared: spot.id,
25826
+ replaceable: false,
25827
+ editable: false
25828
+ });
25829
+ }
25830
+ return { files: rows, ...notShown.length === 0 ? {} : { not_shown: notShown } };
25831
+ }
25832
+ /** The harness's own set, each file with how many of its agents (by their folders' readings) read it. */
25833
+ harnessCopy(readings, agentFolders) {
25834
+ const shared = [];
25835
+ const notShown = [];
25836
+ for (const spot of this.sharedSpots(readings)) {
25837
+ const text2 = textOf(spot.path) ?? "";
25838
+ const named = { name: spot.name, title: spot.title };
25839
+ if (contextBytes(text2) > CONTEXT_FILE_BYTES) {
25840
+ notShown.push({ ...named, reason: "too_large" });
25841
+ continue;
25842
+ }
25843
+ if (shared.length >= CONTEXT_FILES_SHOWN) {
25844
+ notShown.push({ ...named, reason: "count" });
25845
+ continue;
25846
+ }
25847
+ const over = spot.over;
25848
+ shared.push({
25849
+ id: spot.id,
25850
+ name: spot.name,
25851
+ title: spot.title,
25852
+ role: "instructions",
25853
+ ...spot.about === void 0 ? {} : { about: spot.about },
25854
+ text: text2,
25855
+ hash: hashOf(text2),
25856
+ editable: spot.editable,
25857
+ ...spot.whyReadOnly === void 0 ? {} : { why_read_only: spot.whyReadOnly },
25858
+ used_by: over === void 0 ? agentFolders.length : agentFolders.filter(over).length
25859
+ });
25860
+ }
25861
+ return { shared, ...notShown.length === 0 ? {} : { not_shown: notShown } };
25862
+ }
25863
+ /**
25864
+ * A person's edit of one of an agent's files, saved where OpenCode reads it, only if it is still
25865
+ * the version they changed. Refused, with the copy as it is, for a file that isn't the agent's own
25866
+ * to change, and as `changed` for one changed since.
25867
+ */
25868
+ async saveAgent(folder2, reading, edit) {
25869
+ const now = () => this.agentCopy(folder2, reading);
25870
+ const spot = this.ownSpots(folder2, reading).find((each) => each.name === edit.name);
25871
+ if (spot === void 0) {
25872
+ const shared = this.readBy(folder2, reading).some((each) => each.name === edit.name);
25873
+ throw new Refuse(
25874
+ shared ? "Every agent reads this one. Change it on OpenCode's page." : `There is no file called ${edit.name} here.`,
25875
+ now(),
25876
+ "failed"
25877
+ );
25878
+ }
25879
+ if (edit.base_hash === null) throw new Refuse("This file is the agent's own already.", now(), "failed");
25880
+ await this.write(spot, edit, now);
25881
+ }
25882
+ /** A person's edit of one of the harness's own files, read by its agents beside their own. */
25883
+ async saveShared(readings, agentFolders, edit) {
25884
+ const now = () => this.harnessCopy(readings, agentFolders);
25885
+ const spot = this.sharedSpots(readings).find((each) => each.name === edit.name);
25886
+ if (spot === void 0) throw new Refuse(`There is no file called ${edit.name} here.`, now(), "failed");
25887
+ if (!spot.editable) throw new Refuse(spot.whyReadOnly ?? "It can't be changed from Chips.", now(), "failed");
25888
+ if (edit.base_hash === null) {
25889
+ throw new Refuse("Every agent reads this file beside its own, so none can have its own in its place.", now(), "failed");
25890
+ }
25891
+ await this.write(spot, edit, now);
25892
+ }
25893
+ async write(spot, edit, now) {
25894
+ if (hashOf(textOf(spot.path) ?? "") !== edit.base_hash) throw new Refuse("", now(), "changed");
25895
+ try {
25896
+ await mkdir2(dirname4(spot.path), { recursive: true });
25897
+ await writeFile2(spot.path, edit.text, "utf8");
25898
+ } catch (error62) {
25899
+ throw new Refuse(`It couldn't be saved: ${error62 instanceof Error ? error62.message : String(error62)}`.slice(0, 300), now(), "failed");
25900
+ }
25901
+ }
25902
+ };
25471
25903
 
25472
25904
  // src/opencode.ts
25473
25905
  var OpenCodeError = class extends Error {
@@ -25507,12 +25939,47 @@ var OpenCodeClient = class {
25507
25939
  return false;
25508
25940
  }
25509
25941
  }
25942
+ /** Which OpenCode answers: `GET /api/info`'s `version`, `2.0.25`. */
25943
+ async version() {
25944
+ const info = await this.call("GET", "/api/info");
25945
+ return typeof info["version"] === "string" ? info["version"] : "";
25946
+ }
25947
+ /** The root of the project a folder is in, as OpenCode has it: the folder itself, outside any repository. */
25948
+ async projectRoot(directory) {
25949
+ const read = await this.call("GET", `/api/location${whereIs(directory)}`);
25950
+ const project = read["project"] ?? {};
25951
+ return typeof project["directory"] === "string" ? project["directory"] : directory;
25952
+ }
25953
+ /**
25954
+ * The instruction files the config documents for a folder name (`instructions`), each document's
25955
+ * in turn, each named once: a path, a glob or a URL, as written.
25956
+ */
25957
+ async instructions(directory) {
25958
+ const answer = await this.fetcher(`${this.root}/api/config${whereIs(directory)}`, { headers: { authorization: this.auth } });
25959
+ if (!answer.ok) throw await failureOf(answer);
25960
+ const entries = await answer.json();
25961
+ const named = (Array.isArray(entries) ? entries : []).filter((entry) => entry["type"] === "document").flatMap((entry) => (entry["info"] ?? {})["instructions"]).filter((each) => typeof each === "string");
25962
+ return [...new Set(named)];
25963
+ }
25510
25964
  async agents() {
25511
25965
  return await this.catalogue("agent");
25512
25966
  }
25513
25967
  async models() {
25514
25968
  return await this.catalogue("model");
25515
25969
  }
25970
+ /** The model a session with none of its own runs on, when the server has one. */
25971
+ async defaultModel() {
25972
+ const read = await this.call("GET", `/api/model/default${this.where()}`);
25973
+ const data = read["data"];
25974
+ return typeof data === "object" && data !== null ? data : void 0;
25975
+ }
25976
+ /**
25977
+ * The model a session runs on from its next model call. OpenCode takes any model and level
25978
+ * here, one it can't run among them, so the bridge asks only for those of its catalogue.
25979
+ */
25980
+ async setModel(sessionId, model) {
25981
+ await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/model`, { model });
25982
+ }
25516
25983
  /**
25517
25984
  * A project's catalogue. OpenCode 2.0 answers the first ask for a project it hasn't loaded with
25518
25985
  * an empty list, and the next with the whole one: a server always has its `build` agent, so an
@@ -25577,6 +26044,13 @@ var OpenCodeClient = class {
25577
26044
  if (typeof id2 !== "string") throw new OpenCodeError("OpenCode took the words and named no inbox item", 502);
25578
26045
  return id2;
25579
26046
  }
26047
+ /**
26048
+ * Compact a session: its inbox takes the ask, which starts at once when it is idle and at its next
26049
+ * step when it is working. `session.compaction.started` and `.ended` or `.failed` say how it goes.
26050
+ */
26051
+ async compact(sessionId) {
26052
+ await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/compact`, {});
26053
+ }
25580
26054
  /** Words the session hasn't taken yet, taken back. */
25581
26055
  async withdraw(sessionId, item) {
25582
26056
  await this.call("DELETE", `/api/session/${encodeURIComponent(sessionId)}/inbox/${encodeURIComponent(item)}`);
@@ -25626,6 +26100,22 @@ var OpenCodeClient = class {
25626
26100
  const content = Array.isArray(newest?.["content"]) ? newest["content"] : [];
25627
26101
  return content.filter((part) => part["type"] === "text" && typeof part["text"] === "string").map((part) => String(part["text"])).join("\n\n");
25628
26102
  }
26103
+ /** The tokens and model of a session's newest model call, and when: nothing for a session with none. */
26104
+ async lastCall(sessionId) {
26105
+ const read = await this.call(
26106
+ "GET",
26107
+ `/api/session/${encodeURIComponent(sessionId)}/message?type=assistant&order=desc&limit=1`
26108
+ );
26109
+ const newest = (Array.isArray(read["data"]) ? read["data"] : [])[0];
26110
+ const tokens2 = newest?.["tokens"];
26111
+ if (typeof tokens2 !== "object" || tokens2 === null) return void 0;
26112
+ const time3 = newest?.["time"] ?? {};
26113
+ return {
26114
+ tokens: tokens2,
26115
+ ...typeof newest?.["model"] === "object" && newest["model"] !== null ? { model: newest["model"] } : {},
26116
+ at: typeof time3["created"] === "number" ? time3["created"] : 0
26117
+ };
26118
+ }
25629
26119
  /** Every event the server sends, each handed to `hear` as it comes, until the stream ends. */
25630
26120
  async events(hear, signal) {
25631
26121
  const answer = await this.fetcher(`${this.root}/api/event`, {
@@ -25649,7 +26139,7 @@ var OpenCodeClient = class {
25649
26139
  /** `?location[directory]=…` for the catalogues, which are per project. */
25650
26140
  where() {
25651
26141
  const directory = this.address.directory;
25652
- return directory === void 0 ? "" : `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
26142
+ return directory === void 0 ? "" : whereIs(directory);
25653
26143
  }
25654
26144
  async call(method, path, body) {
25655
26145
  const answer = await this.fetcher(`${this.root}${path}`, {
@@ -25665,6 +26155,9 @@ var OpenCodeClient = class {
25665
26155
  return text2 === "" ? {} : JSON.parse(text2);
25666
26156
  }
25667
26157
  };
26158
+ function whereIs(directory) {
26159
+ return `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
26160
+ }
25668
26161
  function eventOf(block) {
25669
26162
  const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
25670
26163
  if (data === "") return void 0;
@@ -25696,15 +26189,131 @@ async function failureOf(answer) {
25696
26189
  return new OpenCodeError(message, answer.status, tag, kind);
25697
26190
  }
25698
26191
 
26192
+ // src/models.ts
26193
+ var MODEL_CHOICES_MAX2 = 40;
26194
+ var EFFORT_LEVELS_MAX2 = 8;
26195
+ var LEVEL_WORDS = {
26196
+ none: "None",
26197
+ minimal: "Minimal",
26198
+ low: "Low",
26199
+ medium: "Medium",
26200
+ high: "High",
26201
+ xhigh: "Extra high",
26202
+ max: "Max",
26203
+ thinking: "Thinking"
26204
+ };
26205
+ function writtenOf(model) {
26206
+ return `${model.providerID}/${model.id}`;
26207
+ }
26208
+ function refOf(pick2) {
26209
+ return { ...modelRefOf(pick2.model), ...pick2.effort === void 0 ? {} : { variant: pick2.effort } };
26210
+ }
26211
+ function levelOf(model) {
26212
+ return model?.variant === void 0 || model.variant === "default" ? void 0 : model.variant;
26213
+ }
26214
+ function sameModel(a, b) {
26215
+ return a?.providerID === b?.providerID && a?.id === b?.id && levelOf(a) === levelOf(b);
26216
+ }
26217
+ function levelWords(id2) {
26218
+ return (LEVEL_WORDS[id2] ?? `${id2.charAt(0).toUpperCase()}${id2.slice(1)}`).slice(0, 40);
26219
+ }
26220
+ function runnable(catalogue) {
26221
+ return catalogue.filter((model) => model.enabled !== false && model.status !== "deprecated");
26222
+ }
26223
+ function levelsOf(model) {
26224
+ const seen = /* @__PURE__ */ new Set();
26225
+ const each = (model?.variants ?? []).filter((variant) => !seen.has(variant.id) && seen.add(variant.id));
26226
+ return each.slice(-EFFORT_LEVELS_MAX2).map((variant) => ({ id: variant.id, label: levelWords(variant.id) }));
26227
+ }
26228
+ var fitting = (choice, effort) => choice?.efforts.some((level) => level.id === effort) === true ? effort : void 0;
26229
+ function modelCopy(input2) {
26230
+ const { pick: pick2, fallback } = input2;
26231
+ const runsOn = pick2 === void 0 ? fallback?.model : refOf(pick2);
26232
+ if (runsOn === void 0) return void 0;
26233
+ const offered2 = runnable(input2.catalogue);
26234
+ const names = /* @__PURE__ */ new Map();
26235
+ for (const model of offered2) names.set(model.name, (names.get(model.name) ?? 0) + 1);
26236
+ const choices = offered2.map((model) => ({
26237
+ id: writtenOf(model),
26238
+ label: ((names.get(model.name) ?? 0) > 1 ? `${model.name} \xB7 ${model.providerID}` : model.name || model.id).slice(0, 80),
26239
+ efforts: levelsOf(model)
26240
+ }));
26241
+ const named = (ref) => {
26242
+ const id2 = writtenOf(ref);
26243
+ const known = choices.find((choice2) => choice2.id === id2);
26244
+ if (known !== void 0) return known;
26245
+ const level = levelOf(ref);
26246
+ const choice = { id: id2, label: ref.id.slice(0, 80), efforts: level === void 0 ? [] : [{ id: level, label: levelWords(level) }] };
26247
+ choices.push(choice);
26248
+ return choice;
26249
+ };
26250
+ const now = named(runsOn);
26251
+ const otherwise = fallback === void 0 ? void 0 : named(fallback.model);
26252
+ const effort = fitting(now, levelOf(runsOn));
26253
+ const fallbackEffort = fitting(otherwise, levelOf(fallback?.model));
26254
+ const keep = /* @__PURE__ */ new Set([now.id, ...otherwise === void 0 ? [] : [otherwise.id]]);
26255
+ let others = MODEL_CHOICES_MAX2 - keep.size;
26256
+ return {
26257
+ model: now.id,
26258
+ ...effort === void 0 ? {} : { effort },
26259
+ own: pick2 !== void 0,
26260
+ ...fallback === void 0 || otherwise === void 0 ? {} : {
26261
+ default: {
26262
+ label: fallback.label.slice(0, 80),
26263
+ model: otherwise.id,
26264
+ ...fallbackEffort === void 0 ? {} : { effort: fallbackEffort }
26265
+ }
26266
+ },
26267
+ choices: choices.filter((choice) => keep.has(choice.id) || others-- > 0),
26268
+ editable: true
26269
+ };
26270
+ }
26271
+ function pickOf(change, catalogue) {
26272
+ if (change.model === null) return null;
26273
+ const model = runnable(catalogue).find((each) => writtenOf(each) === change.model);
26274
+ if (model === void 0) throw new Error(`OpenCode doesn't run ${change.model} here.`);
26275
+ if (change.effort === null) return { model: change.model };
26276
+ if (!(model.variants ?? []).some((variant) => variant.id === change.effort)) {
26277
+ throw new Error(`${model.name || model.id} has no ${levelWords(change.effort)} level.`);
26278
+ }
26279
+ return { model: change.model, effort: change.effort };
26280
+ }
26281
+
26282
+ // src/windows.ts
26283
+ var OWN_LIMIT = "OpenCode compacts it itself when it is nearly full, as its config says.";
26284
+ function usedOf(tokens2) {
26285
+ return tokens2.input + tokens2.cache.read + tokens2.cache.write + tokens2.output + tokens2.reasoning;
26286
+ }
26287
+ function compactedTo(data) {
26288
+ const tokens2 = data["tokens"];
26289
+ const summary = typeof tokens2?.output === "number" ? tokens2.output : 0;
26290
+ const recent = typeof data["recent"] === "string" ? data["recent"] : "";
26291
+ return Math.round(summary + recent.length / 4);
26292
+ }
26293
+ function windowCopy(kept, compacting) {
26294
+ return {
26295
+ used: Math.max(0, Math.round(kept.used)),
26296
+ window: Math.max(1, Math.round(kept.window)),
26297
+ ...kept.model === void 0 ? {} : { model: kept.model.slice(0, 120) },
26298
+ as_of: kept.asOf,
26299
+ ...compacting === void 0 ? {} : { compacting },
26300
+ ...kept.last === void 0 ? {} : { last: kept.last },
26301
+ ...kept.longTurn === void 0 ? {} : { long_turn: { ...kept.longTurn, folds: Math.min(kept.longTurn.folds, LONG_TURN_FOLDS_MAX) } },
26302
+ compacts: true,
26303
+ editable: false,
26304
+ why_read_only: OWN_LIMIT
26305
+ };
26306
+ }
26307
+
25699
26308
  // src/bridge.ts
25700
- var WANTS = ["agents", "new_agents", "forks"];
26309
+ var WANTS = ["agents", "new_spaces", "new_agents", "forks", "models", "context_files", "context_windows"];
25701
26310
  function wantsFor(ownServer2) {
25702
26311
  return ownServer2 ? [...WANTS, "restarts"] : WANTS;
25703
26312
  }
25704
26313
  function freeFolder(root, name, taken) {
25705
26314
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
25706
26315
  for (let count = 1; ; count += 1) {
25707
- const folder2 = join5(root, count === 1 ? slug : `${slug}-${count}`);
26316
+ const folder2 = join6(root, count === 1 ? slug : `${slug}-${count}`);
25708
26317
  if (!taken.includes(folder2)) return folder2;
25709
26318
  }
25710
26319
  }
@@ -25735,12 +26344,17 @@ var Bridge = class {
25735
26344
  constructor(options) {
25736
26345
  this.options = options;
25737
26346
  this.log = options.log ?? (() => void 0);
26347
+ this.places = options.places ?? placesOf(process.env, homedir4());
26348
+ const known = options.folders !== void 0 || options.directory !== void 0;
26349
+ this.files = known ? new ContextFiles(this.places, options.folders) : void 0;
25738
26350
  }
25739
26351
  options;
25740
26352
  agents = [];
25741
26353
  rooms = /* @__PURE__ */ new Map();
25742
26354
  /** The turn going in each OpenCode session. */
25743
26355
  turns = /* @__PURE__ */ new Map();
26356
+ /** The room's rules each session holds (Chips ADR 0250): said once, and again after it compacts. */
26357
+ rules = new SessionRules();
25744
26358
  /**
25745
26359
  * What a session said while it was being given a turn's words, before the turn was known: OpenCode
25746
26360
  * may say it took them before it answers that it has them. Heard by the turn once it is.
@@ -25770,6 +26384,15 @@ var Bridge = class {
25770
26384
  restarting;
25771
26385
  /** Why OpenCode's server isn't there, where the bridge knows more than that it doesn't answer. */
25772
26386
  why;
26387
+ /** The files OpenCode builds each agent from, where the bridge knows each agent's folder. */
26388
+ files;
26389
+ places;
26390
+ /** Each folder's project root, as OpenCode said it. */
26391
+ roots = /* @__PURE__ */ new Map();
26392
+ /** Whether the server reads the files its config's `instructions` names, asked once a start or a restart. */
26393
+ readsNamed;
26394
+ /** Agents compacting now, by whose sessions they are: waiting for their session's next step, or under way, and the ask from Chips. */
26395
+ compacting = /* @__PURE__ */ new Map();
25773
26396
  /** Register the agents, start hearing OpenCode and what is said in Chips, and say how it is. */
25774
26397
  async start() {
25775
26398
  const { chips: chips2 } = this.options;
@@ -25789,9 +26412,17 @@ var Bridge = class {
25789
26412
  chips2.on("agent.fork", (fork) => {
25790
26413
  this.copy(fork).catch((error62) => this.log(`a copy went wrong: ${messageOf(error62)}`));
25791
26414
  });
26415
+ chips2.onSpaceAsked((asked) => {
26416
+ this.claim(asked).catch((error62) => this.log(`a space from the + went wrong: ${messageOf(error62)}`));
26417
+ });
25792
26418
  chips2.on("feedback", (feedback2) => {
25793
26419
  this.notRight(feedback2).catch((error62) => this.log(`a second look went wrong: ${messageOf(error62)}`));
25794
26420
  });
26421
+ for (const agent of await chips2.agents.list().catch(() => [])) {
26422
+ if (agent.state !== "archived") await this.sendModel(agent.agentId);
26423
+ }
26424
+ await this.sendContexts();
26425
+ await this.sendWindows();
25795
26426
  await this.sayHowItIs();
25796
26427
  this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
25797
26428
  this.health.unref();
@@ -25823,6 +26454,7 @@ var Bridge = class {
25823
26454
  try {
25824
26455
  await again2();
25825
26456
  this.why = void 0;
26457
+ this.readsNamed = void 0;
25826
26458
  this.wake?.();
25827
26459
  } catch (error62) {
25828
26460
  const why = messageOf(error62).replace(/\s+/g, " ").trim();
@@ -25844,16 +26476,16 @@ var Bridge = class {
25844
26476
  */
25845
26477
  async seatMain(config3, known) {
25846
26478
  const { chips: chips2 } = this.options;
25847
- const before = known.find((agent) => agent.lead);
26479
+ const before2 = known.find((agent) => agent.lead);
25848
26480
  const made = known.some((agent) => agent.key === chips2.harnessId);
25849
- if (!made && before !== void 0) {
25850
- await this.goHome(before, known);
25851
- return before;
26481
+ if (!made && before2 !== void 0) {
26482
+ await this.goHome(before2, known);
26483
+ return before2;
25852
26484
  }
25853
26485
  const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
25854
26486
  await this.goHome(main2, known);
25855
- if (before !== void 0 && before.agentId !== main2.agentId) {
25856
- await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
26487
+ if (before2 !== void 0 && before2.agentId !== main2.agentId) {
26488
+ await chips2.agents.register({ key: before2.key, name: before2.name, state: "archived" }).catch((error62) => this.log(`${before2.name} stays: ${String(error62)}`));
25857
26489
  }
25858
26490
  return main2;
25859
26491
  }
@@ -25915,6 +26547,47 @@ var Bridge = class {
25915
26547
  });
25916
26548
  this.ours.add(agent.agentId);
25917
26549
  this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
26550
+ await this.sendModel(agent.agentId);
26551
+ await this.sendContexts();
26552
+ }
26553
+ /**
26554
+ * A space a person asked for from Chips's + (Chips ADR 0241), or beside another harness: claimed
26555
+ * under a key of its own, named from its first words, and Chips makes the new agent the key is
26556
+ * for and puts it in the space. That agent answers the first words, which are the ask's and are
26557
+ * not heard again as a message, as the first OpenCode agent, in a session and a folder of its
26558
+ * own. A claim heard again finds the space claimed already, and the agent the same.
26559
+ */
26560
+ async claim(asked) {
26561
+ const { chips: chips2 } = this.options;
26562
+ const { spaceId, firstMessage } = asked;
26563
+ const key = `opencode:space:${spaceId}`;
26564
+ await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
26565
+ const agent = (await chips2.agents.list()).find((each) => each.key === key);
26566
+ if (agent === void 0) {
26567
+ this.log(`claimed ${spaceId}, and Chips put no agent there`);
26568
+ return;
26569
+ }
26570
+ this.ours.add(agent.agentId);
26571
+ this.log(`${agent.name} claimed ${spaceId}`);
26572
+ await this.sendModel(agent.agentId);
26573
+ await this.sendContexts();
26574
+ const context = await chips2.roomContext(spaceId, { limit: 20 }).catch(() => void 0);
26575
+ const said = context?.lines.find((line) => line.message_id === firstMessage.messageId)?.from;
26576
+ const message = {
26577
+ type: "message.new",
26578
+ space_id: spaceId,
26579
+ message_id: firstMessage.messageId,
26580
+ text: firstMessage.text,
26581
+ from: {
26582
+ participant_id: asked.askedBy,
26583
+ kind: "person",
26584
+ display_name: said?.name ?? "Someone",
26585
+ role: said?.role ?? "member",
26586
+ ...said?.workspace_role === void 0 ? {} : { workspace_role: said.workspace_role }
26587
+ },
26588
+ for: [agent.agentId]
26589
+ };
26590
+ await this.hear(message);
25918
26591
  }
25919
26592
  /** A person's Fork, made once however often it is heard at a time. */
25920
26593
  copy(fork) {
@@ -25976,6 +26649,8 @@ var Bridge = class {
25976
26649
  this.ours.add(copy.agentId);
25977
26650
  if (kept.session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] = kept.session;
25978
26651
  if (kept.folder !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: kept.folder };
26652
+ const pick2 = state2.models?.[who];
26653
+ if (pick2 !== void 0) state2.models = { ...state2.models, [copy.agentId]: pick2 };
25979
26654
  saveState(state2);
25980
26655
  if (copied !== void 0) {
25981
26656
  await opencode2.update(copied.id, {
@@ -25986,6 +26661,7 @@ var Bridge = class {
25986
26661
  this.log(
25987
26662
  `${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
26663
  );
26664
+ void this.sendModel(copy.agentId).then(() => this.sendContexts());
25989
26665
  }
25990
26666
  /** The session of an agent's to copy: its room's, or where it is in none, the last it had. */
25991
26667
  sessionToCopy(who, room) {
@@ -25993,6 +26669,305 @@ var Bridge = class {
25993
26669
  if (room !== void 0 && room !== null) return sessions[`${who}|${room}`];
25994
26670
  return Object.entries(sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
25995
26671
  }
26672
+ // ------------------------------------------------------------------ what a person changes from an agent's page
26673
+ /**
26674
+ * Which OpenCode agent a Chips agent of the harness's runs as, and whose its sessions and its
26675
+ * pick are, as `roomOf` says: the key of one configured here, else its Chips id. Nothing for an
26676
+ * agent not the harness's.
26677
+ */
26678
+ async whoseIs(agentId2) {
26679
+ const agent = await this.runnerOf(agentId2);
26680
+ if (agent === void 0 || agentId2 === this.options.chips.harnessId) return void 0;
26681
+ return { agent, who: agentId2 === agent.agentId ? agent.key : agentId2 };
26682
+ }
26683
+ /**
26684
+ * What an agent runs on with no pick of its own: its model in the bridge's config, else its
26685
+ * OpenCode agent's, else the server's default.
26686
+ */
26687
+ async fallbackOf(agent) {
26688
+ if (agent.model !== void 0) return { model: agent.model, label: "As in config.json" };
26689
+ const { opencode: opencode2 } = this.options;
26690
+ const own2 = (await opencode2.agents().catch(() => [])).find((each) => each.id === agent.config.agent)?.model;
26691
+ if (own2 !== void 0) return { model: own2, label: "OpenCode's default" };
26692
+ const model = await opencode2.defaultModel();
26693
+ return model === void 0 ? void 0 : { model: { providerID: model.providerID, id: model.id }, label: "OpenCode's default" };
26694
+ }
26695
+ /** An agent's model, as OpenCode and the bridge have it now: nothing for an agent not the harness's. */
26696
+ async modelCopyOf(agentId2) {
26697
+ const whose = await this.whoseIs(agentId2);
26698
+ if (whose === void 0) return void 0;
26699
+ const [catalogue, fallback] = await Promise.all([this.options.opencode.models(), this.fallbackOf(whose.agent)]);
26700
+ return modelCopy({ catalogue, pick: this.options.state.models?.[whose.who], fallback });
26701
+ }
26702
+ /** An agent's model sent, where it changed since it last was (Chips takes the same copy once). */
26703
+ async sendModel(agentId2) {
26704
+ try {
26705
+ const copy = await this.modelCopyOf(agentId2);
26706
+ if (copy !== void 0) await this.options.chips.settings.model(agentId2, copy);
26707
+ } catch (error62) {
26708
+ this.log(`the model of ${agentId2} not sent: ${messageOf(error62)}`);
26709
+ }
26710
+ }
26711
+ /**
26712
+ * A person's pick of an agent's model and level from its page (Chips ADR 0241): kept with the
26713
+ * bridge's state, and given to each of its sessions before its next message there. Only a model
26714
+ * OpenCode runs, at one of its levels; `null` is its default again. Answered with its copy.
26715
+ */
26716
+ async modelChanged(change) {
26717
+ const whose = await this.whoseIs(change.agent_id);
26718
+ if (whose === void 0) throw new Refuse("This harness has no such agent.");
26719
+ const { opencode: opencode2, state: state2, saveState } = this.options;
26720
+ let pick2;
26721
+ try {
26722
+ pick2 = pickOf(change, await opencode2.models());
26723
+ } catch (error62) {
26724
+ throw new Refuse(messageOf(error62), await this.modelCopyOf(change.agent_id).catch(() => void 0));
26725
+ }
26726
+ const models = { ...state2.models };
26727
+ if (pick2 === null) delete models[whose.who];
26728
+ else models[whose.who] = pick2;
26729
+ state2.models = models;
26730
+ saveState(state2);
26731
+ const name = await this.nameOf(whose);
26732
+ this.log(`${name} runs on ${pick2 === null ? "its default" : `${pick2.model}${pick2.effort === void 0 ? "" : ` at ${pick2.effort}`}`}, from Chips`);
26733
+ const copy = await this.modelCopyOf(change.agent_id);
26734
+ if (copy === void 0) throw new Refuse("OpenCode names no model for it.");
26735
+ return copy;
26736
+ }
26737
+ /**
26738
+ * The session a message goes to, put on the model its agent runs on, where it isn't on it
26739
+ * already (Chips ADR 0241): its pick, else its model in the bridge's config. A session that was
26740
+ * put on another before, with neither now, goes back to its OpenCode agent's or the server's.
26741
+ */
26742
+ async modelled(sessionId, { who, agent }) {
26743
+ const { opencode: opencode2, state: state2 } = this.options;
26744
+ const pick2 = state2.models?.[who];
26745
+ try {
26746
+ const has = (await opencode2.session(sessionId))["model"];
26747
+ const want = pick2 !== void 0 ? refOf(pick2) : agent.model ?? (has === void 0 ? void 0 : (await this.fallbackOf(agent))?.model);
26748
+ if (want === void 0 || sameModel(want, has)) return;
26749
+ await opencode2.setModel(sessionId, want);
26750
+ this.log(`${sessionId} runs on ${writtenOf(want)}${want.variant === void 0 ? "" : ` at ${want.variant}`} from now`);
26751
+ } catch (error62) {
26752
+ if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) {
26753
+ this.log(`${sessionId} stays on its model: ${messageOf(error62)}`);
26754
+ }
26755
+ }
26756
+ }
26757
+ /** The folder an agent's sessions work in, its own or the one project's; none where the bridge knows neither. */
26758
+ async agentFolder(whose) {
26759
+ return await this.folderFor(whose) ?? this.options.directory;
26760
+ }
26761
+ /**
26762
+ * What OpenCode reads for a folder beside its `AGENTS.md` files: where its walk up stops, and
26763
+ * from 2.0.30 the files its config's `instructions` names. A folder whose project OpenCode can't
26764
+ * say is its own project.
26765
+ */
26766
+ async readingOf(folder2) {
26767
+ const { opencode: opencode2 } = this.options;
26768
+ const { home: home3 } = this.places;
26769
+ let root = this.roots.get(folder2);
26770
+ if (root === void 0) {
26771
+ root = await opencode2.projectRoot(folder2).catch(() => folder2);
26772
+ this.roots.set(folder2, root);
26773
+ }
26774
+ this.readsNamed ??= opencode2.version().then(
26775
+ (version3) => !before(version3, READS_NAMED),
26776
+ () => false
26777
+ );
26778
+ const sources = await this.readsNamed ? await opencode2.instructions(folder2).catch(() => []) : [];
26779
+ return { stop: within(folder2, home3) ? home3 : root, named: namedFiles(sources, folder2, root, home3) };
26780
+ }
26781
+ /** Each agent of the harness's, with the folder its sessions work in and what OpenCode reads there. */
26782
+ async agentReadings() {
26783
+ const read = [];
26784
+ for (const agent of await this.options.chips.agents.list().catch(() => [])) {
26785
+ if (agent.state === "archived") continue;
26786
+ const whose = await this.whoseIs(agent.agentId);
26787
+ const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
26788
+ if (folder2 !== void 0) read.push({ agentId: agent.agentId, folder: folder2, reading: await this.readingOf(folder2) });
26789
+ }
26790
+ return read;
26791
+ }
26792
+ /** The harness's own set and each agent's files, as they are on disk now: each sent where it changed. */
26793
+ async sendContexts() {
26794
+ const files = this.files;
26795
+ if (files === void 0) return;
26796
+ try {
26797
+ const read = await this.agentReadings();
26798
+ const readings = new Map(read.map((each) => [each.folder, each.reading]));
26799
+ await this.options.chips.settings.harnessContext(files.harnessCopy(readings, read.map((each) => each.folder)));
26800
+ for (const { agentId: agentId2, folder: folder2, reading } of read) {
26801
+ await this.options.chips.settings.context(agentId2, files.agentCopy(folder2, reading));
26802
+ }
26803
+ } catch (error62) {
26804
+ this.log(`the agents' files not sent: ${messageOf(error62)}`);
26805
+ }
26806
+ }
26807
+ /** One agent's files sent, where they changed. */
26808
+ async sendContext(agentId2) {
26809
+ const whose = await this.whoseIs(agentId2);
26810
+ const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
26811
+ if (this.files === void 0 || folder2 === void 0) return;
26812
+ try {
26813
+ await this.options.chips.settings.context(agentId2, this.files.agentCopy(folder2, await this.readingOf(folder2)));
26814
+ } catch (error62) {
26815
+ this.log(`the files of ${agentId2} not sent: ${messageOf(error62)}`);
26816
+ }
26817
+ }
26818
+ /**
26819
+ * A person's edit of a file from Chips (Chips ADR 0134): saved where OpenCode reads it, only if
26820
+ * it is still the version they changed, and answered with the copy as it is now. OpenCode reads
26821
+ * it again before the agent's next model call, and tells the agent what changed: every agent
26822
+ * working in that folder, or for the harness's own set every agent that reads the file.
26823
+ */
26824
+ async contextEdited(edit) {
26825
+ const files = this.files;
26826
+ if (files === void 0) throw new Refuse("This bridge doesn't know where its agents' files are.");
26827
+ if (edit.agent_id === void 0) {
26828
+ const read = await this.agentReadings();
26829
+ const readings = new Map(read.map((each) => [each.folder, each.reading]));
26830
+ const folders = read.map((each) => each.folder);
26831
+ await files.saveShared(readings, folders, edit);
26832
+ this.log(`OpenCode's ${edit.name} saved from Chips`);
26833
+ return files.harnessCopy(readings, folders);
26834
+ }
26835
+ const whose = await this.whoseIs(edit.agent_id);
26836
+ const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
26837
+ if (whose === void 0 || folder2 === void 0) throw new Refuse("This harness has no such agent.");
26838
+ const reading = await this.readingOf(folder2);
26839
+ await files.saveAgent(folder2, reading, edit);
26840
+ this.log(`${await this.nameOf(whose)}'s ${edit.name} saved from Chips`);
26841
+ for (const other of await this.agentReadings()) {
26842
+ if (other.agentId !== edit.agent_id && other.folder === folder2) void this.sendContext(other.agentId);
26843
+ }
26844
+ return files.agentCopy(folder2, reading);
26845
+ }
26846
+ // ------------------------------------------------------------------ how full each agent is (Chips ADRs 0146, 0241)
26847
+ /** The Chips agent whose sessions are `who`'s: a configured agent's, or the agent itself. */
26848
+ agentIdOf(who) {
26849
+ return this.agents.find((agent) => agent.key === who)?.agentId ?? who;
26850
+ }
26851
+ /** Whose a session is, by the rooms the bridge keeps: nothing for one it didn't make. */
26852
+ whoseSession(sessionId) {
26853
+ const id2 = Object.entries(this.options.state.sessions).find(([, each]) => each === sessionId)?.[0];
26854
+ return id2?.slice(0, id2.lastIndexOf("|"));
26855
+ }
26856
+ /** An agent's window copy sent, and kept with the bridge's state: nothing until it has said how full it is. */
26857
+ async sendWindow(who) {
26858
+ const kept = this.options.state.windows?.[who];
26859
+ if (kept === void 0) return;
26860
+ this.options.saveState(this.options.state);
26861
+ 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)}`));
26862
+ }
26863
+ /**
26864
+ * Each agent's window as it last said, at start. One the bridge kept nothing of, from before it
26865
+ * kept them, is read from the newest model call of its sessions.
26866
+ */
26867
+ async sendWindows() {
26868
+ const { opencode: opencode2, state: state2 } = this.options;
26869
+ const whose = new Set(Object.keys(state2.sessions).map((id2) => id2.slice(0, id2.lastIndexOf("|"))));
26870
+ for (const who of whose) {
26871
+ if (state2.windows?.[who] === void 0) {
26872
+ let newest;
26873
+ for (const [id2, sessionId] of Object.entries(state2.sessions)) {
26874
+ if (!id2.startsWith(`${who}|`)) continue;
26875
+ const call = await opencode2.lastCall(sessionId).catch(() => void 0);
26876
+ if (call !== void 0 && (newest === void 0 || call.at > newest.call.at)) newest = { sessionId, call };
26877
+ }
26878
+ const model = newest?.call.model === void 0 ? void 0 : writtenOf(newest.call.model);
26879
+ const window = model === void 0 ? void 0 : await this.windowOf(model);
26880
+ if (newest === void 0 || model === void 0 || window === void 0) continue;
26881
+ const asOf = new Date(newest.call.at > 0 ? newest.call.at : Date.now()).toISOString();
26882
+ state2.windows = { ...state2.windows, [who]: { used: usedOf(newest.call.tokens), window, model, asOf, session: newest.sessionId } };
26883
+ }
26884
+ await this.sendWindow(who);
26885
+ }
26886
+ }
26887
+ /** A turn ended: how full it got, where that passed half the window, and the copy as it is now. */
26888
+ async turnHeld(turn) {
26889
+ const kept = this.options.state.windows?.[turn.who];
26890
+ if (kept === void 0) return;
26891
+ if (turn.peak > kept.window / 2) {
26892
+ kept.longTurn = { at: (/* @__PURE__ */ new Date()).toISOString(), peak: Math.round(turn.peak), folds: turn.folds };
26893
+ }
26894
+ await this.sendWindow(turn.who);
26895
+ }
26896
+ /**
26897
+ * What OpenCode says of a compaction of one of the agents' sessions: under way, done, with how
26898
+ * much it held before and after, or failed. A Compact now asked from Chips that failed is
26899
+ * refused after all, in OpenCode's words.
26900
+ */
26901
+ compactionHeard(sessionId, event) {
26902
+ if (event.type === "session.compaction.started") this.rules.forget(sessionId);
26903
+ const who = this.whoseSession(sessionId);
26904
+ const kept = who === void 0 ? void 0 : this.options.state.windows?.[who];
26905
+ if (who === void 0 || kept === void 0) return;
26906
+ const asked = this.compacting.get(who);
26907
+ if (event.type === "session.compaction.started") {
26908
+ if (asked === void 0) this.compacting.set(who, { state: "running" });
26909
+ else asked.state = "running";
26910
+ void this.sendWindow(who);
26911
+ return;
26912
+ }
26913
+ if (event.type === "session.compaction.ended") {
26914
+ this.compacting.delete(who);
26915
+ const to = compactedTo(event.data);
26916
+ const by = event.data["reason"] === "manual" ? "asked" : "itself";
26917
+ kept.last = { at: (/* @__PURE__ */ new Date()).toISOString(), from: Math.round(kept.used), to, by };
26918
+ kept.used = to;
26919
+ kept.asOf = kept.last.at;
26920
+ kept.session = sessionId;
26921
+ const turn = this.turns.get(sessionId);
26922
+ if (turn !== void 0 && by === "itself") turn.folds += 1;
26923
+ this.log(`${who} compacted (${by}), ${kept.last.from} to ${to} tokens`);
26924
+ void this.sendWindow(who);
26925
+ return;
26926
+ }
26927
+ if (event.type === "session.compaction.failed") {
26928
+ this.compacting.delete(who);
26929
+ const error62 = event.data["error"] ?? {};
26930
+ const why = text(error62["message"]) ?? "OpenCode didn't compact it.";
26931
+ this.log(`${who} not compacted: ${why}`);
26932
+ if (asked?.changeId === void 0) {
26933
+ void this.sendWindow(who);
26934
+ return;
26935
+ }
26936
+ 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)}`));
26937
+ }
26938
+ }
26939
+ /**
26940
+ * A person's Compact now (Chips ADR 0146): asked of OpenCode for the session the agent said it
26941
+ * was in last, which takes it at once when idle and at its next step when working, and answered
26942
+ * with the copy saying so. How it goes comes in OpenCode's own events.
26943
+ */
26944
+ async windowCompact(ask2) {
26945
+ const whose = await this.whoseIs(ask2.agent_id);
26946
+ if (whose === void 0) throw new Refuse("This harness has no such agent.");
26947
+ const { who } = whose;
26948
+ const kept = this.options.state.windows?.[who];
26949
+ if (kept === void 0) throw new Refuse(`${await this.nameOf(whose)} hasn't said how full it is yet.`);
26950
+ if (this.compacting.has(who)) throw new Refuse("It is compacting already.", windowCopy(kept, this.compacting.get(who)?.state));
26951
+ const { opencode: opencode2 } = this.options;
26952
+ const busy = this.turns.has(kept.session) || (await opencode2.active().catch(() => /* @__PURE__ */ new Set())).has(kept.session);
26953
+ this.compacting.set(who, { state: busy ? "queued" : "running", changeId: ask2.change_id });
26954
+ try {
26955
+ await opencode2.compact(kept.session);
26956
+ } catch (error62) {
26957
+ this.compacting.delete(who);
26958
+ throw new Refuse(`OpenCode didn't take it: ${messageOf(error62)}`, windowCopy(kept));
26959
+ }
26960
+ this.log(`${await this.nameOf(whose)} compacts ${busy ? "at its session's next step" : "now"}, from Chips`);
26961
+ return windowCopy(kept, this.compacting.get(who)?.state);
26962
+ }
26963
+ /** A person's limit: OpenCode keeps its own, set in its config, and the copy says so. */
26964
+ async windowChange(ask2) {
26965
+ const whose = await this.whoseIs(ask2.agent_id);
26966
+ const kept = whose === void 0 ? void 0 : this.options.state.windows?.[whose.who];
26967
+ if (whose === void 0) throw new Refuse("This harness has no such agent.");
26968
+ const copy = kept === void 0 ? void 0 : windowCopy(kept, this.compacting.get(whose.who)?.state);
26969
+ throw new Refuse(copy?.why_read_only ?? "OpenCode keeps its own limit.", copy);
26970
+ }
25996
26971
  // ------------------------------------------------------------------ what is said to it
25997
26972
  /** A message for some of its agents: each takes a turn in its room, one after another there. */
25998
26973
  async hear(message, own2 = {}) {
@@ -26082,16 +27057,18 @@ var Bridge = class {
26082
27057
  const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
26083
27058
  const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
26084
27059
  const readied = await this.projectFor(room, speakerId ?? agent.agentId);
26085
- const text2 = promptFor(message, {
27060
+ const asked = {
26086
27061
  ...speakerId === void 0 ? {} : { taker: speakerId },
26087
27062
  sayWho: saysWho !== true,
26088
27063
  sendsFiles: true,
26089
27064
  project: readied?.facts
26090
- });
27065
+ };
27066
+ const rules = rulesFor(message, asked);
27067
+ const said = (session) => promptFor(message, { ...asked, rulesHeld: this.rules.holds(session, rules) });
26091
27068
  let sessionId;
26092
27069
  let given;
26093
27070
  try {
26094
- ({ sessionId, given } = await this.give(room, text2));
27071
+ ({ sessionId, given } = await this.give(room, said, rules));
26095
27072
  } catch (error62) {
26096
27073
  await this.failed(run2, `${agent.config.name} couldn't reach OpenCode: ${messageOf(error62)}`, true);
26097
27074
  return;
@@ -26104,6 +27081,10 @@ var Bridge = class {
26104
27081
  run: run2,
26105
27082
  speaker,
26106
27083
  agent,
27084
+ who: room.who,
27085
+ agentId: speakerId ?? agent.agentId,
27086
+ peak: 0,
27087
+ folds: 0,
26107
27088
  sessionId,
26108
27089
  waiting: /* @__PURE__ */ new Set(),
26109
27090
  taken: /* @__PURE__ */ new Set(),
@@ -26135,9 +27116,12 @@ var Bridge = class {
26135
27116
  if (this.turns.get(sessionId) === turn) this.turns.delete(sessionId);
26136
27117
  if (last.outcome === "succeeded") await this.finish(turn);
26137
27118
  else await this.ended(turn, last);
27119
+ await this.sendModel(turn.agentId);
27120
+ await this.sendContext(turn.agentId);
27121
+ await this.turnHeld(turn);
26138
27122
  }
26139
27123
  /** The words into the room's session: the one it had, or a new one when OpenCode no longer has it. */
26140
- async give({ id: id2, who, agent, spaceId }, text2) {
27124
+ async give({ id: id2, who, agent, spaceId }, said, rules) {
26141
27125
  const { opencode: opencode2, state: state2, saveState } = this.options;
26142
27126
  let kept = state2.sessions[id2];
26143
27127
  if (kept === void 0 && this.copying.size > 0) {
@@ -26146,22 +27130,24 @@ var Bridge = class {
26146
27130
  }
26147
27131
  if (kept !== void 0) {
26148
27132
  try {
26149
- return { sessionId: kept, given: await this.prompted(kept, text2) };
27133
+ await this.modelled(kept, { who, agent });
27134
+ return { sessionId: kept, given: await this.prompted(kept, said(kept), rules) };
26150
27135
  } catch (error62) {
26151
27136
  if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) throw error62;
26152
27137
  this.log(`${agent.config.name}'s session for ${spaceId} is gone from OpenCode: a new one`);
26153
27138
  }
26154
27139
  }
27140
+ const pick2 = state2.models?.[who];
26155
27141
  const made = await opencode2.newSession({
26156
27142
  title: `Chips space ${spaceId}`,
26157
27143
  agent: agent.config.agent,
26158
- model: agent.model,
27144
+ model: pick2 === void 0 ? agent.model : refOf(pick2),
26159
27145
  directory: await this.folderFor({ who, agent }),
26160
27146
  metadata: { chips_space: spaceId, chips_agent: who }
26161
27147
  });
26162
27148
  state2.sessions[id2] = made;
26163
27149
  saveState(state2);
26164
- return { sessionId: made, given: await this.prompted(made, text2) };
27150
+ return { sessionId: made, given: await this.prompted(made, said(made), rules) };
26165
27151
  }
26166
27152
  /**
26167
27153
  * The folder an agent's new sessions work in, its own: made the first time, named after it, and
@@ -26231,12 +27217,18 @@ var Bridge = class {
26231
27217
  return known.find((each) => each.agentId === who)?.name ?? "agent";
26232
27218
  }
26233
27219
  /** A turn's first words into a session, what it says meanwhile held for the turn. */
26234
- async prompted(sessionId, text2) {
27220
+ /**
27221
+ * A turn's words given to a session, which is told the room's rules they carry as they are
27222
+ * given, so a compaction heard during the turn forgets them (Chips ADR 0250).
27223
+ */
27224
+ async prompted(sessionId, text2, rules) {
26235
27225
  this.early.set(sessionId, []);
27226
+ this.rules.told(sessionId, rules);
26236
27227
  try {
26237
27228
  return await this.options.opencode.prompt(sessionId, text2);
26238
27229
  } catch (error62) {
26239
27230
  this.early.delete(sessionId);
27231
+ this.rules.forget(sessionId);
26240
27232
  throw error62;
26241
27233
  }
26242
27234
  }
@@ -26292,6 +27284,11 @@ var Bridge = class {
26292
27284
  return;
26293
27285
  }
26294
27286
  if (event.type === "session.forked") return;
27287
+ if (event.type.startsWith("session.compaction.")) {
27288
+ const going = this.turns.get(sessionId);
27289
+ if (going === void 0) this.compactionHeard(sessionId, event);
27290
+ else going.chain = going.chain.then(() => this.compactionHeard(sessionId, event));
27291
+ }
26295
27292
  const own2 = this.turns.get(sessionId);
26296
27293
  if (own2 === void 0 && this.early.has(sessionId)) {
26297
27294
  this.early.get(sessionId)?.push(event);
@@ -26463,6 +27460,9 @@ var Bridge = class {
26463
27460
  const limit = await this.windowOf(model);
26464
27461
  if (limit === void 0) return;
26465
27462
  await turn.run.context({ usedTokens: prompt + written, limitTokens: limit, model }).catch(() => void 0);
27463
+ const windows = this.options.state.windows ??= {};
27464
+ windows[turn.who] = { ...windows[turn.who], used: prompt + written, window: limit, model, asOf: (/* @__PURE__ */ new Date()).toISOString(), session: turn.sessionId };
27465
+ turn.peak = Math.max(turn.peak, prompt + written);
26466
27466
  }
26467
27467
  /** A model's context window, from OpenCode's catalogue, read once (and once more for each it lacked). */
26468
27468
  async windowOf(model) {
@@ -26639,6 +27639,21 @@ var Bridge = class {
26639
27639
  if (retryable) void this.sayHowItIs();
26640
27640
  }
26641
27641
  };
27642
+ function settingsFor(bridge2) {
27643
+ const active = () => {
27644
+ const running = bridge2();
27645
+ if (running === void 0) throw new Refuse("The harness is still starting.");
27646
+ return running;
27647
+ };
27648
+ return {
27649
+ models: (change) => active().modelChanged(change),
27650
+ context: (edit) => active().contextEdited(edit),
27651
+ contextWindows: {
27652
+ compact: (ask2) => active().windowCompact(ask2),
27653
+ change: (ask2) => active().windowChange(ask2)
27654
+ }
27655
+ };
27656
+ }
26642
27657
  function decisionOf(decision, command = false) {
26643
27658
  if (!decision.allowed) return "reject";
26644
27659
  return decision.scope === "always" && !command ? "always" : "once";
@@ -26727,6 +27742,12 @@ function spoken(words4) {
26727
27742
  return end2 === -1 ? words4 : words4.slice(end2 + THOUGHT_ENDS.length);
26728
27743
  }
26729
27744
  var THOUGHT_ENDS = "</think>";
27745
+ function titleFrom(text2) {
27746
+ const line = text2.trim().split("\n")[0] ?? "";
27747
+ if (line.length <= 48) return line;
27748
+ const cut2 = line.slice(0, 48);
27749
+ return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
27750
+ }
26730
27751
  function forkName(original) {
26731
27752
  return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
26732
27753
  }
@@ -26791,12 +27812,12 @@ function lastLine(said) {
26791
27812
  }
26792
27813
 
26793
27814
  // src/keep.ts
26794
- import { homedir as homedir4 } from "node:os";
26795
- import { basename as basename3, join as join6, resolve as resolve2 } from "node:path";
27815
+ import { homedir as homedir5 } from "node:os";
27816
+ import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
26796
27817
  import { fileURLToPath as fileURLToPath2 } from "node:url";
26797
27818
  var flags = process.argv.slice(2);
26798
27819
  var words3 = flags.filter((word) => !word.startsWith("--"));
26799
- var whole = (word) => resolve2(word.startsWith("~/") ? join6(homedir4(), word.slice(2)) : word);
27820
+ var whole = (word) => resolve2(word.startsWith("~/") ? join7(homedir5(), word.slice(2)) : word);
26800
27821
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
26801
27822
  var self = fileURLToPath2(import.meta.url);
26802
27823
  var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
@@ -26814,7 +27835,7 @@ function stopIfAsked(label2) {
26814
27835
  if (stopped2.state === "theirs") end(`${stopped2.path} wasn't written by this bridge: it was left as it is.`, 1);
26815
27836
  end(`Nothing called ${label2} was kept running here.`, 0);
26816
27837
  }
26817
- function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
27838
+ function keepIfAsked(label2, url2, names, log2, before2 = () => void 0) {
26818
27839
  if (!flags.includes("--keep")) return;
26819
27840
  const node2 = keptNode();
26820
27841
  const kept = keepRunning({
@@ -26824,7 +27845,7 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
26824
27845
  environment: keepEnvironment(url2, process.env, names, node2)
26825
27846
  });
26826
27847
  if (kept.state === "kept") {
26827
- before();
27848
+ before2();
26828
27849
  end(
26829
27850
  `Kept running as ${label2}: it starts at login, and again whenever it stops. What it says goes to ${kept.logFile}. \`${stopLine}\` stops it.`,
26830
27851
  0
@@ -26844,9 +27865,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
26844
27865
  // src/server.ts
26845
27866
  import { spawn } from "node:child_process";
26846
27867
  import { randomBytes } from "node:crypto";
26847
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
27868
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
26848
27869
  import { createServer } from "node:net";
26849
- import { delimiter as delimiter2, join as join7 } from "node:path";
27870
+ import { delimiter as delimiter2, join as join8 } from "node:path";
26850
27871
  import { createInterface } from "node:readline";
26851
27872
  var ServerError = class extends Error {
26852
27873
  constructor(message, missing = false) {
@@ -26860,7 +27881,7 @@ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
26860
27881
  var onPath = (command, env) => command.includes("/") ? existsSync5(command) : (env["PATH"] ?? "").split(delimiter2).some((folder2) => {
26861
27882
  if (folder2 === "") return false;
26862
27883
  try {
26863
- accessSync2(join7(folder2, command), constants2.X_OK);
27884
+ accessSync2(join8(folder2, command), constants2.X_OK);
26864
27885
  return true;
26865
27886
  } catch {
26866
27887
  return false;
@@ -26896,7 +27917,7 @@ async function answers2(url2, password) {
26896
27917
  function keptIn(path) {
26897
27918
  if (!existsSync5(path)) return void 0;
26898
27919
  try {
26899
- const kept = JSON.parse(readFileSync5(path, "utf8"));
27920
+ const kept = JSON.parse(readFileSync6(path, "utf8"));
26900
27921
  return typeof kept.port === "number" && typeof kept.password === "string" ? kept : void 0;
26901
27922
  } catch {
26902
27923
  return void 0;
@@ -26925,15 +27946,15 @@ async function ownServer(options) {
26925
27946
  const env = options.env ?? process.env;
26926
27947
  const command = options.command ?? "opencode";
26927
27948
  const log2 = options.log ?? (() => void 0);
26928
- const path = join7(options.folder, "server.json");
27949
+ const path = join8(options.folder, "server.json");
26929
27950
  let kept = keptIn(path);
26930
- let before = false;
27951
+ let before2 = false;
26931
27952
  if (kept !== void 0) {
26932
27953
  const now = await answers2(`http://127.0.0.1:${kept.port}`, kept.password);
26933
- before = now === "ok";
27954
+ before2 = now === "ok";
26934
27955
  if (now === "refused") kept = { port: await freePort(), password: kept.password };
26935
27956
  }
26936
- if (!before && !onPath(command, env)) {
27957
+ if (!before2 && !onPath(command, env)) {
26937
27958
  throw new ServerError("OpenCode isn't installed on this machine: install it (opencode.ai), then run this again.", true);
26938
27959
  }
26939
27960
  kept ??= { port: await freePort(), password: randomBytes(24).toString("base64url") };
@@ -27016,7 +28037,7 @@ async function ownServer(options) {
27016
28037
  }
27017
28038
  await ended(info["pid"], stopMs);
27018
28039
  };
27019
- if (before) log2(`OpenCode's server from before is still answering at ${url2}`);
28040
+ if (before2) log2(`OpenCode's server from before is still answering at ${url2}`);
27020
28041
  else {
27021
28042
  await up(false);
27022
28043
  log2(`OpenCode's own server answering at ${url2}`);
@@ -27049,9 +28070,9 @@ async function ownServer(options) {
27049
28070
  }
27050
28071
 
27051
28072
  // src/setup.ts
27052
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
27053
- import { homedir as homedir5 } from "node:os";
27054
- import { join as join8, resolve as resolve3 } from "node:path";
28073
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
28074
+ import { homedir as homedir6 } from "node:os";
28075
+ import { join as join9, resolve as resolve3 } from "node:path";
27055
28076
  var SetupError = class extends Error {
27056
28077
  constructor(message, words4 = {}) {
27057
28078
  super(message);
@@ -27068,9 +28089,9 @@ var hostOf = (url2) => {
27068
28089
  }
27069
28090
  };
27070
28091
  var STILL_GOOD = "The link is still good: run the same command again once that's done.";
27071
- var home = (path) => path.startsWith("~/") ? join8(homedir5(), path.slice(2)) : path;
28092
+ var home = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
27072
28093
  function readConfig(path, env = process.env) {
27073
- const raw = existsSync6(path) ? JSON.parse(readFileSync6(path, "utf8")) : {};
28094
+ const raw = existsSync6(path) ? JSON.parse(readFileSync7(path, "utf8")) : {};
27074
28095
  const passwordEnv = typeof raw["passwordEnv"] === "string" ? raw["passwordEnv"] : "OPENCODE_PASSWORD";
27075
28096
  const password = filledIn(env[passwordEnv]) ? env[passwordEnv] : void 0;
27076
28097
  const ownServer2 = password === void 0 && typeof raw["url"] !== "string";
@@ -27125,8 +28146,12 @@ async function checkOpenCode(config3, client) {
27125
28146
  }
27126
28147
  }
27127
28148
 
28149
+ // src/version.ts
28150
+ import { readFileSync as readFileSync8 } from "node:fs";
28151
+ var VERSION = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8")).version;
28152
+
27128
28153
  // src/main.ts
27129
- var home2 = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
28154
+ var home2 = (path) => path.startsWith("~/") ? join10(homedir7(), path.slice(2)) : path;
27130
28155
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
27131
28156
  var LABEL = "chat.chips.opencode";
27132
28157
  stopIfAsked(LABEL);
@@ -27146,13 +28171,13 @@ try {
27146
28171
  } catch (error62) {
27147
28172
  config2 = stop(error62);
27148
28173
  }
27149
- var folder = dirname4(configPath);
28174
+ var folder = dirname5(configPath);
27150
28175
  mkdirSync5(folder, { recursive: true, mode: 448 });
27151
- var statePath = join9(folder, "sessions.json");
28176
+ var statePath = join10(folder, "sessions.json");
27152
28177
  var server;
27153
28178
  if (link !== void 0) reportStep("checking", { detail: "Looking for OpenCode" });
27154
28179
  if (config2.ownServer) {
27155
- const cwd = config2.directory ?? dirname4(config2.folders);
28180
+ const cwd = config2.directory ?? dirname5(config2.folders);
27156
28181
  mkdirSync5(cwd, { recursive: true });
27157
28182
  try {
27158
28183
  server = await ownServer({ folder, cwd, log });
@@ -27192,21 +28217,23 @@ var joined = await join3({
27192
28217
  link,
27193
28218
  url: process.env["CHIPS_URL"],
27194
28219
  name: config2.name,
27195
- software: "chips-opencode 1.0.0",
28220
+ software: `chips-opencode ${VERSION}`,
27196
28221
  wants: wantsFor(server !== void 0),
27197
28222
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
27198
28223
  }).catch(refused2);
27199
28224
  keepIfAsked(LABEL, joined.url, [config2.passwordEnv], log, () => server?.stop());
27200
- var state = existsSync7(statePath) ? JSON.parse(readFileSync7(statePath, "utf8")) : { sessions: {} };
28225
+ var state = existsSync7(statePath) ? JSON.parse(readFileSync9(statePath, "utf8")) : { sessions: {} };
27201
28226
  var bridge;
27202
28227
  var chips = await connect({
27203
28228
  url: joined.url,
27204
28229
  token: joined.token,
27205
- client: { name: "chips-opencode", version: "1.0.0" },
27206
- store: fileStore(join9(folder, "chips-state.json")),
28230
+ client: { name: "chips-opencode", version: VERSION },
28231
+ store: fileStore(join10(folder, "chips-state.json")),
27207
28232
  // A person's Restart (Chips ADR 0094), heard only where the bridge runs OpenCode's server: one a
27208
28233
  // 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() }
28234
+ ...server === void 0 ? {} : { onRestart: () => bridge?.restart() },
28235
+ // What a person changes of an agent from its page (Chips ADR 0241): its model and its files.
28236
+ settings: settingsFor(() => bridge)
27210
28237
  });
27211
28238
  var git = await checkGit();
27212
28239
  if (!git.ok) log(`rooms' projects are off: ${git.why}`);
@@ -27221,7 +28248,7 @@ bridge = new Bridge({
27221
28248
  `, { mode: 384 }),
27222
28249
  log,
27223
28250
  restartServer: server?.restart,
27224
- ...config2.directory === void 0 ? { folders: config2.folders } : {}
28251
+ ...config2.directory === void 0 ? { folders: config2.folders } : { directory: config2.directory }
27225
28252
  });
27226
28253
  await bridge.start();
27227
28254
  log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through OpenCode at ${address.url}`);