@chipschat/opencode 1.0.0 → 1.0.2

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.
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- `dist/main.js` in @chipschat/opencode 1.0.0 carries these packages, bundled in. Each one's
3
+ `dist/main.js` in @chipschat/opencode 1.0.2 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
@@ -20231,6 +20231,29 @@ var projectToken = external_exports.object({
20231
20231
  expires_at: isoTimestamp,
20232
20232
  path: external_exports.string().describe("The project's path on this server, which the device puts its own address in front of.")
20233
20233
  });
20234
+ var projectFolderOpenAnswer = external_exports.object({
20235
+ macs: external_exports.int().nonnegative().describe("How many of the person's Macs were asked."),
20236
+ opened_app: external_exports.literal(true).optional().describe("No Chips for Mac of theirs was open, so the server opened the one on its own machine and waited for it (ADR 0227).")
20237
+ });
20238
+ var projectRun = external_exports.object({
20239
+ state: external_exports.enum(["never", "starting", "running", "ended", "failed", "stopped", "opened"]).describe("Never run on that Mac; starting (setup, or waiting for it to answer); running; ended by itself; failed; stopped by the person; or opened, for pages that need no starting."),
20240
+ command: external_exports.string().max(1100).optional().describe("What ran: `setup`, a newline, then `run`, or `open` and the file; the bar compares it with the recipe's."),
20241
+ since: isoTimestamp.optional(),
20242
+ ended: isoTimestamp.optional(),
20243
+ code: external_exports.int().optional().describe("How it ended, as its shell said."),
20244
+ opened: external_exports.string().max(600).optional().describe("What it opened, once it answered."),
20245
+ lines: external_exports.array(external_exports.string().max(400)).max(20).optional().describe("Its last lines, when it ended, or while it runs and something went wrong."),
20246
+ why: external_exports.string().max(400).optional().describe("Why the Mac didn't run it when it was last asked, in its words: no run file, or a different one."),
20247
+ trouble: external_exports.string().max(400).optional().describe("What went wrong with the run itself, in plain words (ADR 0230): a port in use, a program the Mac doesn't have, nothing answering, the browser refusing its address.")
20248
+ });
20249
+ var heldRun = projectRun.extend({
20250
+ mac: external_exports.string().min(1).max(120).describe("The name of the Mac it runs on."),
20251
+ said_at: isoTimestamp
20252
+ });
20253
+ var projectRunAnswer = external_exports.object({
20254
+ run: heldRun.optional(),
20255
+ macs: external_exports.int().nonnegative()
20256
+ });
20234
20257
  var commitId = external_exports.string().regex(/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/).describe("A commit, by its full git object id.");
20235
20258
  var PROJECT_CHANGED_FILES = 200;
20236
20259
  var projectChangedFile = external_exports.object({
@@ -20261,8 +20284,9 @@ function isSecretsFile(path) {
20261
20284
  }
20262
20285
  var SECRET_SHAPES = [
20263
20286
  { what: "a Chips token", source: "chips_(?:at|inv|j|pt)_[A-Za-z0-9_-]{20,}" },
20264
- { what: "an Anthropic key", source: "sk-ant-[A-Za-z0-9_-]{20,}" },
20265
- { what: "an OpenAI key", source: "sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{40,}" },
20287
+ // `sk-` begins a word, so a link's words ("…/elon-musk-terafab-project-…") are never a key.
20288
+ { what: "an Anthropic key", source: "\\bsk-ant-[A-Za-z0-9_-]{20,}" },
20289
+ { what: "an OpenAI key", source: "\\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{40,}" },
20266
20290
  { what: "an AWS access key", source: "\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b" },
20267
20291
  {
20268
20292
  what: "a GitHub token",
@@ -20332,6 +20356,7 @@ function asks(facts, options) {
20332
20356
  "When your file and another's use different names for the same thing, the one that reached the project first keeps its names, and the other changes its names to match."
20333
20357
  );
20334
20358
  }
20359
+ lines2.push("When what you make is to be read (pages, PDFs, pictures), keep a start page at .chips/index.html that links to the documents worth seeing (its links start ../), with the documents themselves at the top or in folders of their own, and bring it up to date whenever you add, rename or replace one. Chips opens it in the app, and its links open there too. One document needs no start page.", `When what you make has to run on the person's Mac (a server, a script, an app), keep .chips/run.json so Run in Chips can start it from the project's folder: {"run": "the one shell command that starts it", "opens": "http://localhost:<port>, to open once it answers", "setup": "a command run first when it changes, like npm install"}. Have it print the address it listens on, like http://localhost:4173, which is what Run opens; "opens" is for a program that prints none. Everything it runs belongs in the project: Run refuses a command that goes anywhere outside the project's folder (a cd elsewhere, a path in a home folder). Start an app by its own program rather than with open, so Stop can close it, and leave starting it to Run rather than starting it yourself. Leave out what it doesn't need, and check it works in your copy before you say it does.`);
20335
20360
  if (facts.markdown)
20336
20361
  lines2.push("In Markdown, put each sentence on a line of its own.");
20337
20362
  if (facts.pages === true) {
@@ -21562,7 +21587,7 @@ var clientHelloFrame = external_exports.object({
21562
21587
  autonomy: autonomyLevel.optional().describe("The level the harness believes it has.")
21563
21588
  })).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."),
21564
21589
  since: external_exports.int().nonnegative().optional().describe("Not read by Chips: a stream resumes from `subscribe`'s `since`."),
21565
- 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); one that says nothing is sent what it was always sent, since an unknown frame is dropped in silence."),
21590
+ 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."),
21566
21591
  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.")
21567
21592
  }).describe("The first frame a client sends.");
21568
21593
  var subscribeFrame = external_exports.object({
@@ -21623,6 +21648,26 @@ var restartFrame = external_exports.object({
21623
21648
  asked_by: id("person").describe("The person who asked."),
21624
21649
  asked_at: isoTimestamp.describe("When they asked.")
21625
21650
  }).describe("A person asks the harness to restart (ADR 0094). Sent to a harness's socket that said `restart` in its `hello`, now or never: it is not in the log, so a reconnect never hears it again. What runs is the harness's own restart.");
21651
+ var folderOpenFrame = external_exports.object({
21652
+ frame: external_exports.literal("folder.open"),
21653
+ space_id: id("space").describe("The room whose folder, or page, to open."),
21654
+ what: external_exports.enum(["folder", "page"]).optional().describe("`page`: open the room's start page, or its one document, in Chips for Mac (ADR 0227). Left out, the folder in Finder."),
21655
+ asked_from: external_exports.string().min(1).max(120).describe("The name of the device that asked: `iPhone`."),
21656
+ asked_at: isoTimestamp.describe("When they asked.")
21657
+ }).describe("A person asks their own Mac to open a room's folder in Finder (ADR 0225), or its page in Chips for Mac (ADR 0227), from another of their devices. Sent to that person's sockets that said `folders` in their `hello`, now or never: it is not in the log. The Mac finds the folder and the page itself: nothing here is a path.");
21658
+ var folderRunFrame = external_exports.object({
21659
+ frame: external_exports.literal("folder.run"),
21660
+ space_id: id("space").describe("The room whose project to run, or stop."),
21661
+ op: external_exports.enum(["run", "stop"]),
21662
+ command: external_exports.string().min(1).max(1100).optional().describe("For `run`, what the asking device showed would run: the Mac runs nothing else, and says so when its folder's run file says something different."),
21663
+ asked_from: external_exports.string().min(1).max(120).describe("The name of the device that asked: `iPhone`."),
21664
+ asked_at: isoTimestamp.describe("When they asked.")
21665
+ }).describe("A person asks their own Mac to run a room's project as its run file says, or to stop it (ADR 0226), from another of their devices. Sent to that person's sockets that said `folders` in their `hello`, now or never: it is not in the log. Nothing here is a command: the Mac reads it from the room's folder.");
21666
+ var folderRunStateFrame = external_exports.object({
21667
+ frame: external_exports.literal("folder.run_state"),
21668
+ space_id: id("space"),
21669
+ run: heldRun
21670
+ }).describe("How a person's run of a room's project on their Mac stands (ADR 0226), as that Mac said it. Sent to that person's sockets that said `folder_runs` in their `hello`, never to anyone else's, now or never: `GET /v1/spaces/:id/project/run` reads it again.");
21626
21671
  var joinFrame = external_exports.object({
21627
21672
  frame: external_exports.literal("join"),
21628
21673
  request: joinRequestView
@@ -21665,6 +21710,9 @@ var serverFrame = external_exports.discriminatedUnion("frame", [
21665
21710
  presenceFrame,
21666
21711
  restartFrame,
21667
21712
  joinFrame,
21713
+ folderOpenFrame,
21714
+ folderRunFrame,
21715
+ folderRunStateFrame,
21668
21716
  contextEditFrame,
21669
21717
  modelChangeFrame,
21670
21718
  heartbeatChangeFrame,
@@ -21948,6 +21996,7 @@ import { existsSync } from "node:fs";
21948
21996
  import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
21949
21997
  import { join } from "node:path";
21950
21998
  var READ_UP_TO = 4 * 1024 * 1024;
21999
+ var MARKED_LINES = 14;
21951
22000
  var TRIES = 4;
21952
22001
  var MARKS = /^(<<<<<<<|>>>>>>>) /m;
21953
22002
  var REJECTED = /\[rejected\]|fetch first|non-fast-forward/;
@@ -21986,6 +22035,27 @@ function pushedInWords(pushed) {
21986
22035
  return `not sent: ${pushed.why}`;
21987
22036
  }
21988
22037
  }
22038
+ var stopped = (words4) => `${words4.trim().replace(/[.\s]+$/, "")}.`;
22039
+ function heldNote(pushed, name) {
22040
+ switch (pushed.state) {
22041
+ case "sent":
22042
+ case "nothing":
22043
+ return void 0;
22044
+ case "clash": {
22045
+ const whose = pushed.with.length === 0 ? "another change" : `${namesInWords(pushed.with)}'s change`;
22046
+ return `(Not in the room's project yet: ${namesInWords(pushed.files)} clashed with ${whose}. ${name} is asked to settle it next turn.)`;
22047
+ }
22048
+ case "keys": {
22049
+ const [first] = pushed.keys;
22050
+ const held = pushed.keys.length === 1 && first !== void 0 ? `${first.path} looks like it holds ${first.what.replace(/, by its name$/, "")}` : `${namesInWords(pushed.keys.map((key) => key.path))} look like they hold keys`;
22051
+ return `(Not in the room's project: ${held}, so nothing was sent.)`;
22052
+ }
22053
+ case "refused":
22054
+ return `(Not in the room's project: it was turned back: ${stopped(pushed.why)})`;
22055
+ case "failed":
22056
+ return `(Not in the room's project: it couldn't be sent: ${stopped(pushed.why)})`;
22057
+ }
22058
+ }
21989
22059
  async function checkGit(git2 = "git", path = process.env["PATH"] ?? "/usr/bin:/bin") {
21990
22060
  const [major, minor] = versionIn(PROJECT_GIT_VERSION) ?? [0, 0];
21991
22061
  return new Promise((resolve5) => {
@@ -22005,7 +22075,7 @@ var Project = class {
22005
22075
  remote;
22006
22076
  constructor(options) {
22007
22077
  this.options = options;
22008
- this.copy = join(options.folder, "room");
22078
+ this.copy = options.person === true ? options.folder : join(options.folder, "room");
22009
22079
  this.remote = `${options.url.replace(/\/$/, "").replace(/^ws/, "http")}${projectPath(options.spaceId)}`;
22010
22080
  }
22011
22081
  /** Before a turn: the copy made or brought up to date, and what the turn is to be told of it. */
@@ -22059,6 +22129,121 @@ var Project = class {
22059
22129
  }
22060
22130
  return { state: "failed", why: "others kept sending first." };
22061
22131
  }
22132
+ /**
22133
+ * A person's folder made, or brought up to the room's `main` while nothing in it is theirs: no
22134
+ * file changed or added, nothing waiting to be sent, no clash (ADR 0224). Once something is
22135
+ * theirs, nothing in the folder changes; what others changed since is told instead.
22136
+ */
22137
+ async look() {
22138
+ await this.ready();
22139
+ const fetched = await this.git(["fetch", "--quiet", "origin"]);
22140
+ const trouble = fetched.ok ? void 0 : remoteWords(fetched.err);
22141
+ const remote = await this.head("refs/remotes/origin/main");
22142
+ await this.settledAsTheirs(remote);
22143
+ const brought = await this.bringUntouched(remote);
22144
+ return { ...await this.standing(remote), ...brought, trouble };
22145
+ }
22146
+ /** The folder moved on to the room's `main` while nothing in it is theirs: what came in, whose. */
22147
+ async bringUntouched(remote) {
22148
+ const mine = await this.head("HEAD");
22149
+ const none = { came: [], cameBy: [] };
22150
+ if (remote === "" || mine === remote || !await this.untouched(mine, remote))
22151
+ return none;
22152
+ const moved = mine === "" ? await this.git(["checkout", "--quiet", "-B", PROJECT_BRANCH, remote]) : await this.git(["merge", "--quiet", "--ff-only", remote]);
22153
+ if (!moved.ok)
22154
+ return none;
22155
+ const from = mine === "" ? await this.emptyTree() : mine;
22156
+ return {
22157
+ came: lines((await this.git(["diff", "--name-only", from, remote])).out),
22158
+ cameBy: await this.authors(mine === "" ? remote : `${mine}..${remote}`)
22159
+ };
22160
+ }
22161
+ /**
22162
+ * A clash the person settled by taking the room's side whole: nothing of theirs is left to send,
22163
+ * so the folder goes back to the room's `main` and is kept up to date again. Every tracked file
22164
+ * is already the room's, and a file git doesn't track is never touched.
22165
+ */
22166
+ async settledAsTheirs(remote) {
22167
+ if (remote === "" || !existsSync(join(this.copy, ".git", "MERGE_HEAD")))
22168
+ return;
22169
+ if ((await this.marked()).length > 0)
22170
+ return;
22171
+ if (!(await this.git(["diff", "--quiet", remote])).ok)
22172
+ return;
22173
+ await this.git(["reset", "--quiet", "--hard", remote]);
22174
+ }
22175
+ /** Whether nothing in the folder is the person's own: no change, no unsent commit, no clash. */
22176
+ async untouched(mine, remote) {
22177
+ if (existsSync(join(this.copy, ".git", "MERGE_HEAD")))
22178
+ return false;
22179
+ const status = await this.git(["status", "--porcelain", "--untracked-files=all"]);
22180
+ if (!status.ok || status.out.trim() !== "")
22181
+ return false;
22182
+ if (mine === "")
22183
+ return true;
22184
+ return (await this.git(["merge-base", "--is-ancestor", mine, remote])).ok;
22185
+ }
22186
+ /** What the person changed, a clash, and what others changed that the folder hasn't. */
22187
+ async standing(remote) {
22188
+ const mine = await this.head("HEAD");
22189
+ const base = await this.baseOf(mine, remote);
22190
+ const counted = lines((await this.git(["diff", "--numstat", "--no-renames", "-z", base])).out.replace(/\0/g, "\n")).map((line) => {
22191
+ const [added2 = "0", removed = "0", ...path] = line.split(" ");
22192
+ return { path: path.join(" "), added: Number(added2) || 0, removed: Number(removed) || 0 };
22193
+ });
22194
+ const added = lines((await this.git(["ls-files", "--others", "--exclude-standard", "-z"])).out.replace(/\0/g, "\n"));
22195
+ const fresh = await Promise.all(added.map(async (path) => ({ path, added: await this.lineCount(path), removed: 0 })));
22196
+ const changed = [...counted, ...fresh].filter((change) => change.path !== "").sort((a, b) => a.path.localeCompare(b.path));
22197
+ const clashes = await this.marked();
22198
+ const clashedWith = await this.mergingWith();
22199
+ const first = clashes[0];
22200
+ const marked = first === void 0 ? void 0 : await this.markedLines(first);
22201
+ const since = remote === "" || remote === base ? [] : lines((await this.git(["diff", "--name-only", base, remote])).out);
22202
+ const by = since.length === 0 ? [] : await this.authors(mine === "" ? remote : `${base}..${remote}`);
22203
+ return { changed, clashes, clashedWith, marked, since, by };
22204
+ }
22205
+ /**
22206
+ * What the person's changes are measured from: where the folder and the room's `main` parted.
22207
+ * In a clash the room's side is already in the folder, so it is `main` itself: what differs from
22208
+ * it is what Send would change.
22209
+ */
22210
+ async baseOf(mine, remote) {
22211
+ if (mine === "")
22212
+ return this.emptyTree();
22213
+ if (remote === "")
22214
+ return mine;
22215
+ if (existsSync(join(this.copy, ".git", "MERGE_HEAD")))
22216
+ return remote;
22217
+ return (await this.git(["merge-base", mine, remote])).out.trim() || mine;
22218
+ }
22219
+ /** How many lines a file the person added has: the lines it adds. None for a picture. */
22220
+ async lineCount(path) {
22221
+ try {
22222
+ const at = join(this.copy, path);
22223
+ if ((await stat(at)).size > READ_UP_TO)
22224
+ return 0;
22225
+ const words4 = await readFile(at, "utf8");
22226
+ if (words4.includes("\0"))
22227
+ return 0;
22228
+ return words4 === "" ? 0 : words4.split("\n").length - (words4.endsWith("\n") ? 1 : 0);
22229
+ } catch {
22230
+ return 0;
22231
+ }
22232
+ }
22233
+ /** The first clash in a file, from the line before its first mark to the line after its last. */
22234
+ async markedLines(path) {
22235
+ try {
22236
+ const all = (await readFile(join(this.copy, path), "utf8")).split("\n");
22237
+ const start = all.findIndex((line) => line.startsWith("<<<<<<< "));
22238
+ const end2 = all.findIndex((line, at) => at > start && line.startsWith(">>>>>>> "));
22239
+ if (start < 0 || end2 < 0)
22240
+ return void 0;
22241
+ const shown = all.slice(Math.max(0, start - 1), end2 + 2);
22242
+ return shown.length <= MARKED_LINES ? shown.join("\n") : [...shown.slice(0, MARKED_LINES - 1), "\u2026"].join("\n");
22243
+ } catch {
22244
+ return void 0;
22245
+ }
22246
+ }
22062
22247
  /**
22063
22248
  * One try at sending: sent, nothing to send, or turned back; or, when somebody sent first, the
22064
22249
  * agent's commits replayed on top of theirs, for the next try, unless they clashed.
@@ -22111,6 +22296,10 @@ ${sent.err}`;
22111
22296
  const pointed = (await this.git(["remote", "get-url", "origin"])).out.trim();
22112
22297
  if (pointed === this.remote)
22113
22298
  return false;
22299
+ if (this.options.person === true) {
22300
+ await this.git(["remote", "set-url", "origin", this.remote]);
22301
+ return false;
22302
+ }
22114
22303
  const was = /\/v1\/spaces\/([^/]+)\/project\.git$/.exec(pointed)?.[1] ?? "before";
22115
22304
  await rename(this.copy, join(this.options.folder, `room-${was}`));
22116
22305
  }
@@ -22264,9 +22453,12 @@ ${sent.err}`;
22264
22453
  }
22265
22454
  git(args) {
22266
22455
  const { options } = this;
22456
+ const person = options.person === true;
22457
+ const email3 = `${options.agentId}@${person ? "people" : "agents"}.chips.invalid`;
22267
22458
  const pairs = [
22268
22459
  ["http.extraHeader", `Authorization: Bearer ${options.token}`],
22269
- ["http.extraHeader", `${PROJECT_ACTOR_HEADER}: ${options.agentId}`],
22460
+ // A person's project token says who they are; an agent's harness token needs the agent.
22461
+ ...person ? [] : [["http.extraHeader", `${PROJECT_ACTOR_HEADER}: ${options.agentId}`]],
22270
22462
  ["core.hooksPath", "/dev/null"],
22271
22463
  ["commit.gpgSign", "false"],
22272
22464
  ["merge.conflictStyle", "merge"],
@@ -22279,9 +22471,9 @@ ${sent.err}`;
22279
22471
  GIT_CONFIG_GLOBAL: "/dev/null",
22280
22472
  GIT_TERMINAL_PROMPT: "0",
22281
22473
  GIT_AUTHOR_NAME: options.name,
22282
- GIT_AUTHOR_EMAIL: `${options.agentId}@agents.chips.invalid`,
22474
+ GIT_AUTHOR_EMAIL: email3,
22283
22475
  GIT_COMMITTER_NAME: options.name,
22284
- GIT_COMMITTER_EMAIL: `${options.agentId}@agents.chips.invalid`,
22476
+ GIT_COMMITTER_EMAIL: email3,
22285
22477
  GIT_EDITOR: "true",
22286
22478
  GIT_CONFIG_COUNT: String(pairs.length)
22287
22479
  };
@@ -23990,7 +24182,11 @@ import { readFile as readFile3, realpath, stat as stat2 } from "node:fs/promises
23990
24182
  import { homedir as homedir2 } from "node:os";
23991
24183
  import { basename as basename2, isAbsolute, resolve, sep } from "node:path";
23992
24184
  import { fileURLToPath } from "node:url";
23993
- var SENDING_FILES = "A file reaches this room only when you send it: put MEDIA:<path> on a line of its own in your answer, one line for each file, from your own folder. A path, or a link to this machine, reaches nobody: the people and agents here cannot open your folder.";
24185
+ function sendingFiles(options = {}) {
24186
+ const which = options.project === true ? "A file that isn't in the room's project" : "A file";
24187
+ return `${which} reaches this room only when you send it: put MEDIA:<path> on a line of its own in your answer, one line for each file, from your own folder. A path, or a link to this machine, reaches nobody: the people and agents here cannot open your folder.`;
24188
+ }
24189
+ var SENDING_FILES = sendingFiles();
23994
24190
  var MOST_FILES = 20;
23995
24191
  var MOST_FILE_BYTES = 100 * 1024 * 1024;
23996
24192
  function filesNamedIn(answer) {
@@ -24098,7 +24294,7 @@ function promptFor(message, options = {}) {
24098
24294
  const who = options.sayWho === false ? "" : `${withRole(message.from.display_name, message.from)}: `;
24099
24295
  const files = (message.attachments ?? []).map(renderAttachment);
24100
24296
  const words4 = [`${who}${message.text}`, ...files].join("\n");
24101
- const sending = options.sendsFiles === true ? SENDING_FILES : void 0;
24297
+ const sending = options.sendsFiles === true ? sendingFiles({ project: options.project !== void 0 }) : void 0;
24102
24298
  const replying = renderReplyingTo(message.replying_to, {
24103
24299
  sender: { id: message.from.participant_id, name: message.from.display_name },
24104
24300
  taker,
@@ -25061,18 +25257,22 @@ var Bridge = class {
25061
25257
  }
25062
25258
  /**
25063
25259
  * What the turn changed in its copy, sent to the room's project before its words are posted: the
25064
- * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one.
25260
+ * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
25261
+ * the change didn't go, the line the reply says so in (Chips AP-6).
25065
25262
  */
25066
25263
  async sendProject(turn, words4) {
25067
- if (turn.project === void 0) return void 0;
25264
+ if (turn.project === void 0) return {};
25068
25265
  const { copy, name } = turn.project;
25069
25266
  try {
25070
25267
  const pushed = await copy.push({ words: words4 });
25071
25268
  this.log(`${name}'s project: ${pushedInWords(pushed)}`);
25072
- return pushed.state === "sent" ? pushed.commit : void 0;
25269
+ if (pushed.state === "sent") return { change: pushed.commit };
25270
+ const note = heldNote(pushed, name);
25271
+ return note === void 0 ? {} : { note };
25073
25272
  } catch (error61) {
25074
25273
  this.log(`${name}'s project couldn't be sent: ${messageOf(error61)}`);
25075
- return void 0;
25274
+ const note = heldNote({ state: "failed", why: messageOf(error61) }, name);
25275
+ return note === void 0 ? {} : { note };
25076
25276
  }
25077
25277
  }
25078
25278
  /** Whose session a room's is, by name: the configured agent's, or one Chips knows of. */
@@ -25341,8 +25541,8 @@ var Bridge = class {
25341
25541
  lasts: "once",
25342
25542
  ...own2 && toolCall !== void 0 && turn.tools.get(toolCall)?.open === true ? { stepId: toolCall } : {}
25343
25543
  });
25344
- const stopped = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
25345
- const decision = await Promise.race([asking, stopped]);
25544
+ const stopped2 = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
25545
+ const decision = await Promise.race([asking, stopped2]);
25346
25546
  const pending = turn.approvals.get(requestId);
25347
25547
  if (decision === void 0 || pending === void 0 || pending.answered) return;
25348
25548
  pending.answered = true;
@@ -25384,8 +25584,8 @@ var Bridge = class {
25384
25584
  return;
25385
25585
  }
25386
25586
  const asking = turn.run.question({ questions });
25387
- const stopped = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
25388
- const answer = await Promise.race([asking, stopped]);
25587
+ const stopped2 = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
25588
+ const answer = await Promise.race([asking, stopped2]);
25389
25589
  if (answer === void 0 || answer.timedOut) return;
25390
25590
  const filled = {};
25391
25591
  for (const field of fields) {
@@ -25435,10 +25635,11 @@ var Bridge = class {
25435
25635
  }
25436
25636
  turn.allowed.clear();
25437
25637
  const answer = spoken(turn.recovered ?? lastWordsOf(turn.words)).trim();
25438
- const change = await this.sendProject(turn, answer);
25638
+ const { change, note } = await this.sendProject(turn, answer);
25439
25639
  const folder2 = await this.folderOf(turn.sessionId);
25440
25640
  const { chips: chips2 } = this.options;
25441
- const post = await sendAnswer(answer, {
25641
+ const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
25642
+ const post = await sendAnswer(words4, {
25442
25643
  folders: folder2 === void 0 ? [] : [folder2],
25443
25644
  upload: (input2) => chips2.upload(input2)
25444
25645
  });
@@ -25654,9 +25855,9 @@ var end = (line, code) => {
25654
25855
  };
25655
25856
  function stopIfAsked(label2) {
25656
25857
  if (!flags.includes("--stop")) return;
25657
- const stopped = stopKeeping(label2);
25658
- if (stopped.state === "stopped") end(`Stopped ${label2}, and removed ${stopped.path}.`, 0);
25659
- if (stopped.state === "theirs") end(`${stopped.path} wasn't written by this bridge: it was left as it is.`, 1);
25858
+ const stopped2 = stopKeeping(label2);
25859
+ if (stopped2.state === "stopped") end(`Stopped ${label2}, and removed ${stopped2.path}.`, 0);
25860
+ if (stopped2.state === "theirs") end(`${stopped2.path} wasn't written by this bridge: it was left as it is.`, 1);
25660
25861
  end(`Nothing called ${label2} was kept running here.`, 0);
25661
25862
  }
25662
25863
  function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipschat/opencode",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenCode as a Chips harness, on its V2 server (opencode serve): each OpenCode agent a Chips agent, each room an OpenCode session.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",