@chipschat/opencode 1.0.7 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@ server (`opencode serve`, 2.0).
21
21
  A bridge that joins with a code asks for all of these. One that joined from Chips's gallery does
22
22
  what its card grants: a link carries what the person chose on it, not what the bridge asks for.
23
23
 
24
- Apache-2.0. Not published: a local repository until Chips's SDK is.
24
+ Apache-2.0, published on npm as `@chipschat/opencode`.
25
25
 
26
26
  ## What Chips shows
27
27
 
@@ -144,22 +144,22 @@ gets Restart once you turn it on on its page.
144
144
 
145
145
  ## From Chips's gallery
146
146
 
147
- The card's line is `chips connect opencode '<link>'` (Chips ADR 0218). It downloads this bridge as the npm package
147
+ The card's line is `CHIPS_JOIN_LINK='<link>' chips connect opencode` (Chips ADR 0218). It downloads this bridge as the npm package
148
148
  `@chipschat/opencode`, the newest 1.x, checks it against the registry's checksum, and runs it on the Node
149
149
  that Chips carries, so the machine needs no Node or npm of its own. The bridge checks OpenCode,
150
150
  joins, and keeps itself running from login. `chips disconnect opencode` stops it. On another computer,
151
- `curl -fsSL https://chipschat.ai/install | sh -s -- connect opencode '<link>'` installs Chips there
151
+ `curl -fsSL https://chipschat.ai/install | CHIPS_JOIN_LINK='<link>' sh -s -- connect opencode` installs Chips there
152
152
  first. The steps below run the bridge from this checkout instead, to work on it.
153
153
 
154
154
  ## Run it
155
155
 
156
156
  1. Install OpenCode (opencode.ai), and `npm install` once in this repository.
157
157
 
158
- 2. Join Chips once, with a link from Chips's gallery (Add an agent → OpenCode), whose card gives
158
+ 2. Join Chips once, with a link from Chips's gallery (Agents → Connect a harness → OpenCode), whose card gives
159
159
  this line with the link in it:
160
160
 
161
161
  ```
162
- cd ~/Work/Chips/chips-opencode && CHIPS_JOIN_LINK='<link>' npm run keep
162
+ cd chips-opencode && CHIPS_JOIN_LINK='<link>' npm run keep
163
163
  ```
164
164
 
165
165
  The bridge runs OpenCode's server itself (`opencode serve` on `127.0.0.1`, 4096 when it is
@@ -213,7 +213,9 @@ the key it kept. The scratch Chips, the link and the run come from Chips's `tool
213
213
  ## Release
214
214
 
215
215
  `npm run release` builds `dist/main.js`, one file with the SDK and everything else bundled in, and
216
- `dist/THIRD-PARTY-NOTICES.md` beside it; runs the tests; and packs `chipschat-opencode-<version>.tgz`. `npm publish`
217
- publishes it. A version's major is the Chips protocol it speaks, and Chips's card names that major
218
- (Chips ADR 0218): a fix is a minor or a patch, and reaches every card of its major the day it is
219
- published.
216
+ `dist/THIRD-PARTY-NOTICES.md` beside it; runs the tests; and packs `chipschat-opencode-<version>.tgz`. To publish,
217
+ change `version` in package.json (`npm version <x> --no-git-tag-version`), commit, and push to main:
218
+ `.github/workflows/publish.yml` runs the tests and publishes that version from GitHub with npm's
219
+ trusted publishing, and skips a version npm already has. A version's major is the Chips protocol it
220
+ speaks, and Chips's card names that major (Chips ADR 0218): a fix is a minor or a patch, and reaches
221
+ every card of its major the day it is published.
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- `dist/main.js` in @chipschat/opencode 1.0.7 carries these packages, bundled in. Each one's
3
+ `dist/main.js` in @chipschat/opencode 1.0.10 carries these packages, bundled in. Each one's
4
4
  licence follows.
5
5
 
6
6
  ## @chipschat/harness 0.0.0 (Apache-2.0)
package/dist/main.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  };
9
9
 
10
10
  // src/main.ts
11
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
11
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
12
12
  import { homedir as homedir7 } from "node:os";
13
13
  import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
14
14
 
@@ -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 = {}) {
@@ -26184,6 +26353,8 @@ var Bridge = class {
26184
26353
  rooms = /* @__PURE__ */ new Map();
26185
26354
  /** The turn going in each OpenCode session. */
26186
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();
26187
26358
  /**
26188
26359
  * What a session said while it was being given a turn's words, before the turn was known: OpenCode
26189
26360
  * may say it took them before it answers that it has them. Heard by the turn once it is.
@@ -26728,6 +26899,7 @@ var Bridge = class {
26728
26899
  * refused after all, in OpenCode's words.
26729
26900
  */
26730
26901
  compactionHeard(sessionId, event) {
26902
+ if (event.type === "session.compaction.started") this.rules.forget(sessionId);
26731
26903
  const who = this.whoseSession(sessionId);
26732
26904
  const kept = who === void 0 ? void 0 : this.options.state.windows?.[who];
26733
26905
  if (who === void 0 || kept === void 0) return;
@@ -26885,16 +27057,18 @@ var Bridge = class {
26885
27057
  const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
26886
27058
  const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
26887
27059
  const readied = await this.projectFor(room, speakerId ?? agent.agentId);
26888
- const text2 = promptFor(message, {
27060
+ const asked = {
26889
27061
  ...speakerId === void 0 ? {} : { taker: speakerId },
26890
27062
  sayWho: saysWho !== true,
26891
27063
  sendsFiles: true,
26892
27064
  project: readied?.facts
26893
- });
27065
+ };
27066
+ const rules = rulesFor(message, asked);
27067
+ const said = (session) => promptFor(message, { ...asked, rulesHeld: this.rules.holds(session, rules) });
26894
27068
  let sessionId;
26895
27069
  let given;
26896
27070
  try {
26897
- ({ sessionId, given } = await this.give(room, text2));
27071
+ ({ sessionId, given } = await this.give(room, said, rules));
26898
27072
  } catch (error62) {
26899
27073
  await this.failed(run2, `${agent.config.name} couldn't reach OpenCode: ${messageOf(error62)}`, true);
26900
27074
  return;
@@ -26947,7 +27121,7 @@ var Bridge = class {
26947
27121
  await this.turnHeld(turn);
26948
27122
  }
26949
27123
  /** The words into the room's session: the one it had, or a new one when OpenCode no longer has it. */
26950
- async give({ id: id2, who, agent, spaceId }, text2) {
27124
+ async give({ id: id2, who, agent, spaceId }, said, rules) {
26951
27125
  const { opencode: opencode2, state: state2, saveState } = this.options;
26952
27126
  let kept = state2.sessions[id2];
26953
27127
  if (kept === void 0 && this.copying.size > 0) {
@@ -26957,7 +27131,7 @@ var Bridge = class {
26957
27131
  if (kept !== void 0) {
26958
27132
  try {
26959
27133
  await this.modelled(kept, { who, agent });
26960
- return { sessionId: kept, given: await this.prompted(kept, text2) };
27134
+ return { sessionId: kept, given: await this.prompted(kept, said(kept), rules) };
26961
27135
  } catch (error62) {
26962
27136
  if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) throw error62;
26963
27137
  this.log(`${agent.config.name}'s session for ${spaceId} is gone from OpenCode: a new one`);
@@ -26973,7 +27147,7 @@ var Bridge = class {
26973
27147
  });
26974
27148
  state2.sessions[id2] = made;
26975
27149
  saveState(state2);
26976
- return { sessionId: made, given: await this.prompted(made, text2) };
27150
+ return { sessionId: made, given: await this.prompted(made, said(made), rules) };
26977
27151
  }
26978
27152
  /**
26979
27153
  * The folder an agent's new sessions work in, its own: made the first time, named after it, and
@@ -27043,12 +27217,18 @@ var Bridge = class {
27043
27217
  return known.find((each) => each.agentId === who)?.name ?? "agent";
27044
27218
  }
27045
27219
  /** A turn's first words into a session, what it says meanwhile held for the turn. */
27046
- 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) {
27047
27225
  this.early.set(sessionId, []);
27226
+ this.rules.told(sessionId, rules);
27048
27227
  try {
27049
27228
  return await this.options.opencode.prompt(sessionId, text2);
27050
27229
  } catch (error62) {
27051
27230
  this.early.delete(sessionId);
27231
+ this.rules.forget(sessionId);
27052
27232
  throw error62;
27053
27233
  }
27054
27234
  }
@@ -27966,6 +28146,10 @@ async function checkOpenCode(config3, client) {
27966
28146
  }
27967
28147
  }
27968
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
+
27969
28153
  // src/main.ts
27970
28154
  var home2 = (path) => path.startsWith("~/") ? join10(homedir7(), path.slice(2)) : path;
27971
28155
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
@@ -28033,17 +28217,17 @@ var joined = await join3({
28033
28217
  link,
28034
28218
  url: process.env["CHIPS_URL"],
28035
28219
  name: config2.name,
28036
- software: "chips-opencode 1.0.0",
28220
+ software: `chips-opencode ${VERSION}`,
28037
28221
  wants: wantsFor(server !== void 0),
28038
28222
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
28039
28223
  }).catch(refused2);
28040
28224
  keepIfAsked(LABEL, joined.url, [config2.passwordEnv], log, () => server?.stop());
28041
- var state = existsSync7(statePath) ? JSON.parse(readFileSync8(statePath, "utf8")) : { sessions: {} };
28225
+ var state = existsSync7(statePath) ? JSON.parse(readFileSync9(statePath, "utf8")) : { sessions: {} };
28042
28226
  var bridge;
28043
28227
  var chips = await connect({
28044
28228
  url: joined.url,
28045
28229
  token: joined.token,
28046
- client: { name: "chips-opencode", version: "1.0.0" },
28230
+ client: { name: "chips-opencode", version: VERSION },
28047
28231
  store: fileStore(join10(folder, "chips-state.json")),
28048
28232
  // A person's Restart (Chips ADR 0094), heard only where the bridge runs OpenCode's server: one a
28049
28233
  // person runs themselves isn't the bridge's to restart, so this connection doesn't say it hears them.
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@chipschat/opencode",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "OpenCode as a Chips harness, on its V2 server (opencode serve): each OpenCode agent a Chips agent, each room an OpenCode session.",
5
5
  "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/arjungupta99/chips-opencode.git"
9
+ },
6
10
  "type": "module",
7
11
  "engines": {
8
12
  "node": ">=22.19.0"