@chipschat/hermes 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -8,9 +8,11 @@ var __export = (target, all) => {
8
8
  };
9
9
 
10
10
  // src/main.ts
11
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
12
- import { homedir as homedir6 } from "node:os";
13
- import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
11
+ import { randomBytes as randomBytes2 } from "node:crypto";
12
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
13
+ import { createServer as createServer2 } from "node:net";
14
+ import { homedir as homedir8 } from "node:os";
15
+ import { dirname as dirname6, join as join12, resolve as resolve6 } from "node:path";
14
16
 
15
17
  // node_modules/zod/v4/classic/external.js
16
18
  var external_exports = {};
@@ -845,10 +847,10 @@ function promiseAllObject(promisesObj) {
845
847
  return resolvedObj;
846
848
  });
847
849
  }
848
- function randomString(length = 10) {
850
+ function randomString(length2 = 10) {
849
851
  const chars = "abcdefghijklmnopqrstuvwxyz";
850
852
  let str = "";
851
- for (let i = 0; i < length; i++) {
853
+ for (let i = 0; i < length2; i++) {
852
854
  str += chars[Math.floor(Math.random() * chars.length)];
853
855
  }
854
856
  return str;
@@ -1996,8 +1998,8 @@ var ulid = /^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$/;
1996
1998
  var xid = /^[0-9a-vA-V]{20}$/;
1997
1999
  var ksuid = /^[A-Za-z0-9]{27}$/;
1998
2000
  var nanoid = /^[a-zA-Z0-9_-]{21}$/;
1999
- function nanoidOfLength(length) {
2000
- return new RegExp(`^[a-zA-Z0-9_-]{${length}}$`);
2001
+ function nanoidOfLength(length2) {
2002
+ return new RegExp(`^[a-zA-Z0-9_-]{${length2}}$`);
2001
2003
  }
2002
2004
  var duration = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
2003
2005
  var extendedDuration = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/;
@@ -2022,8 +2024,8 @@ function emoji() {
2022
2024
  }
2023
2025
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
2024
2026
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
2025
- var mac = (delimiter2) => {
2026
- const escapedDelim = escapeRegex(delimiter2 ?? ":");
2027
+ var mac = (delimiter3) => {
2028
+ const escapedDelim = escapeRegex(delimiter3 ?? ":");
2027
2029
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
2028
2030
  };
2029
2031
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -2075,8 +2077,8 @@ var hex = /^[0-9a-fA-F]*$/;
2075
2077
  function fixedBase64(bodyLength, padding) {
2076
2078
  return new RegExp(`^[A-Za-z0-9+/]{${bodyLength}}${padding}$`);
2077
2079
  }
2078
- function fixedBase64url(length) {
2079
- return new RegExp(`^[A-Za-z0-9_-]{${length}}$`);
2080
+ function fixedBase64url(length2) {
2081
+ return new RegExp(`^[A-Za-z0-9_-]{${length2}}$`);
2080
2082
  }
2081
2083
  var md5_hex = /^[0-9a-fA-F]{32}$/;
2082
2084
  var md5_base64 = /* @__PURE__ */ fixedBase64(22, "==");
@@ -2339,8 +2341,8 @@ var $ZodCheckMaxLength = /* @__PURE__ */ $constructor("$ZodCheckMaxLength", (ins
2339
2341
  inst._zod.check = (payload) => {
2340
2342
  const input2 = payload.value;
2341
2343
  const units = input2.length;
2342
- const length = typeof input2 === "string" && units > def.maximum ? codePointLength(input2) : units;
2343
- if (length <= def.maximum)
2344
+ const length2 = typeof input2 === "string" && units > def.maximum ? codePointLength(input2) : units;
2345
+ if (length2 <= def.maximum)
2344
2346
  return;
2345
2347
  const origin = getLengthableOrigin(input2);
2346
2348
  payload.issues.push({
@@ -2361,8 +2363,8 @@ var $ZodCheckMinLength = /* @__PURE__ */ $constructor("$ZodCheckMinLength", (ins
2361
2363
  inst._zod.check = (payload) => {
2362
2364
  const input2 = payload.value;
2363
2365
  const units = input2.length;
2364
- const length = typeof input2 === "string" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input2) : units;
2365
- if (length >= def.minimum)
2366
+ const length2 = typeof input2 === "string" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input2) : units;
2367
+ if (length2 >= def.minimum)
2366
2368
  return;
2367
2369
  const origin = getLengthableOrigin(input2);
2368
2370
  payload.issues.push({
@@ -2383,11 +2385,11 @@ var $ZodCheckLengthEquals = /* @__PURE__ */ $constructor("$ZodCheckLengthEquals"
2383
2385
  inst._zod.check = (payload) => {
2384
2386
  const input2 = payload.value;
2385
2387
  const units = input2.length;
2386
- const length = typeof input2 === "string" && units >= def.length && units <= def.length * 2 ? codePointLength(input2) : units;
2387
- if (length === def.length)
2388
+ const length2 = typeof input2 === "string" && units >= def.length && units <= def.length * 2 ? codePointLength(input2) : units;
2389
+ if (length2 === def.length)
2388
2390
  return;
2389
2391
  const origin = getLengthableOrigin(input2);
2390
- const tooBig = length > def.length;
2392
+ const tooBig = length2 > def.length;
2391
2393
  payload.issues.push({
2392
2394
  origin,
2393
2395
  ...tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
@@ -2597,8 +2599,8 @@ var Doc = class {
2597
2599
  const lines2 = content.split("\n").filter((x) => x);
2598
2600
  const minIndent = Math.min(...lines2.map((x) => x.length - x.trimStart().length));
2599
2601
  const dedented = lines2.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x);
2600
- for (const line of dedented) {
2601
- this.content.push(line);
2602
+ for (const line2 of dedented) {
2603
+ this.content.push(line2);
2602
2604
  }
2603
2605
  }
2604
2606
  compile() {
@@ -3079,11 +3081,11 @@ var $ZodE164 = /* @__PURE__ */ $constructor("$ZodE164", (inst, def) => {
3079
3081
  });
3080
3082
  var CC_SANITIZE = /[- ]/g;
3081
3083
  function isLuhnAlgo(digits) {
3082
- let length = digits.length;
3084
+ let length2 = digits.length;
3083
3085
  let bit = 1;
3084
3086
  let sum = 0;
3085
- while (length) {
3086
- const value = digits.charCodeAt(--length) - 48;
3087
+ while (length2) {
3088
+ const value = digits.charCodeAt(--length2) - 48;
3087
3089
  bit ^= 1;
3088
3090
  sum += bit ? [0, 2, 4, 6, 8, 1, 3, 5, 7, 9][value] : value;
3089
3091
  }
@@ -5079,7 +5081,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5079
5081
  var NONE = 0;
5080
5082
  var ASSUMED = 1;
5081
5083
  var PROVEN = 2;
5082
- function isRecursive(inst, stack, resolve5) {
5084
+ function isRecursive(inst, stack, resolve7) {
5083
5085
  const cached2 = recursive.get(inst);
5084
5086
  if (cached2 !== void 0)
5085
5087
  return cached2 ? PROVEN : NONE;
@@ -5089,7 +5091,7 @@ function isRecursive(inst, stack, resolve5) {
5089
5091
  let result = NONE;
5090
5092
  const check2 = (child) => {
5091
5093
  if (result !== PROVEN && child?._zod) {
5092
- const answer = isRecursive(child, stack, resolve5);
5094
+ const answer = isRecursive(child, stack, resolve7);
5093
5095
  if (answer > result)
5094
5096
  result = answer;
5095
5097
  }
@@ -5100,7 +5102,7 @@ function isRecursive(inst, stack, resolve5) {
5100
5102
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5101
5103
  if (spread && !desc.enumerable)
5102
5104
  continue;
5103
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
5105
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
5104
5106
  if (child > answer)
5105
5107
  answer = child;
5106
5108
  }
@@ -5164,7 +5166,7 @@ function isRecursive(inst, stack, resolve5) {
5164
5166
  break;
5165
5167
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5166
5168
  case "lazy": {
5167
- const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
5169
+ const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
5168
5170
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5169
5171
  break;
5170
5172
  }
@@ -15157,11 +15159,11 @@ function _minLength(minimum, params) {
15157
15159
  });
15158
15160
  }
15159
15161
  // @__NO_SIDE_EFFECTS__
15160
- function _length(length, params) {
15162
+ function _length(length2, params) {
15161
15163
  return new $ZodCheckLengthEquals({
15162
15164
  check: "length_equals",
15163
15165
  ...normalizeParams(params),
15164
- length
15166
+ length: length2
15165
15167
  });
15166
15168
  }
15167
15169
  // @__NO_SIDE_EFFECTS__
@@ -15728,8 +15730,8 @@ function encodeJSONPointerSegment(segment) {
15728
15730
  return segment.replace(/~/g, "~0").replace(/\//g, "~1");
15729
15731
  }
15730
15732
  function extractDefs(ctx, schema) {
15731
- const root = ctx.seen.get(schema);
15732
- if (!root)
15733
+ const root2 = ctx.seen.get(schema);
15734
+ if (!root2)
15733
15735
  throw new Error("Unprocessed schema. This is a bug in Zod.");
15734
15736
  if (ctx.external && ctx.sharedDefsExtractedFor === ctx.external)
15735
15737
  return;
@@ -15758,7 +15760,7 @@ function extractDefs(ctx, schema) {
15758
15760
  }
15759
15761
  const uriPrefix = `#`;
15760
15762
  const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
15761
- if (entry[1] === root && !entry[1].schema.id) {
15763
+ if (entry[1] === root2 && !entry[1].schema.id) {
15762
15764
  return { ref: uriPrefix };
15763
15765
  }
15764
15766
  const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
@@ -15929,8 +15931,8 @@ function foldIntersection(json2) {
15929
15931
  assignProps(json2, folded);
15930
15932
  }
15931
15933
  function finalize(ctx, schema) {
15932
- const root = ctx.seen.get(schema);
15933
- if (!root)
15934
+ const root2 = ctx.seen.get(schema);
15935
+ if (!root2)
15934
15936
  throw new Error("Unprocessed schema. This is a bug in Zod.");
15935
15937
  const flattenRef = (zodSchema) => {
15936
15938
  const seen = ctx.seen.get(zodSchema);
@@ -16041,7 +16043,7 @@ function finalize(ctx, schema) {
16041
16043
  throw new Error("Schema is missing an `id` property");
16042
16044
  result.$id = ctx.external.uri(id2);
16043
16045
  }
16044
- assignProps(result, root.defId ? root.schema : root.def ?? root.schema);
16046
+ assignProps(result, root2.defId ? root2.schema : root2.def ?? root2.schema);
16045
16047
  const rootMetaId = ctx.metadataRegistry.get(schema)?.id;
16046
16048
  if (rootMetaId !== void 0 && result.id === rootMetaId)
16047
16049
  delete result.id;
@@ -20533,31 +20535,31 @@ function withRole(name2, from) {
20533
20535
  function flat(text) {
20534
20536
  return text.replace(/\s*\n\s*/g, " ").trim();
20535
20537
  }
20536
- function wordsOf(line) {
20537
- const text = flat(line.text ?? "");
20538
- const heard = line.transcript === void 0 ? "" : flat(line.transcript);
20538
+ function wordsOf(line2) {
20539
+ const text = flat(line2.text ?? "");
20540
+ const heard = line2.transcript === void 0 ? "" : flat(line2.transcript);
20539
20541
  const parts = [text];
20540
20542
  if (heard !== "")
20541
20543
  parts.push(text === "" ? `(voice note) ${heard}` : `(voice note: ${heard})`);
20542
- if (heard === "" && line.files !== void 0)
20543
- parts.push(`(sent ${line.files.join(", ")})`);
20544
- if (line.held === true)
20544
+ if (heard === "" && line2.files !== void 0)
20545
+ parts.push(`(sent ${line2.files.join(", ")})`);
20546
+ if (line2.held === true)
20545
20547
  parts.push("(held by Chips until a person acts)");
20546
20548
  return parts.filter((part) => part !== "").join(" ");
20547
20549
  }
20548
- function renderLine(line) {
20549
- const name2 = line.from.name;
20550
- switch (line.kind) {
20550
+ function renderLine(line2) {
20551
+ const name2 = line2.from.name;
20552
+ switch (line2.kind) {
20551
20553
  case "said":
20552
- return `${withRole(name2, line.from)}: ${wordsOf(line)}`;
20554
+ return `${withRole(name2, line2.from)}: ${wordsOf(line2)}`;
20553
20555
  case "joined":
20554
20556
  return `(${name2} joined the space)`;
20555
20557
  case "left":
20556
20558
  return `(${name2} left the space)`;
20557
20559
  case "renamed":
20558
- if (line.title !== void 0)
20559
- return `(${name2} renamed the space "${line.title}")`;
20560
- return `(${line.previous ?? name2} is now called ${line.name ?? name2})`;
20560
+ if (line2.title !== void 0)
20561
+ return `(${name2} renamed the space "${line2.title}")`;
20562
+ return `(${line2.previous ?? name2} is now called ${line2.name ?? name2})`;
20561
20563
  case "archived":
20562
20564
  return `(${name2} archived the space)`;
20563
20565
  default:
@@ -20567,8 +20569,8 @@ function renderLine(line) {
20567
20569
  function renderContext(context) {
20568
20570
  if (context === void 0)
20569
20571
  return "";
20570
- const said = context.lines.length === 0 ? "" : [CONTEXT_HEADING, ...context.lines.map(renderLine)].join("\n");
20571
- return [said, renderRoomFiles(context.files)].filter((part) => part !== "").join("\n\n");
20572
+ const said2 = context.lines.length === 0 ? "" : [CONTEXT_HEADING, ...context.lines.map(renderLine)].join("\n");
20573
+ return [said2, renderRoomFiles(context.files)].filter((part) => part !== "").join("\n\n");
20572
20574
  }
20573
20575
  function renderAttachment(file3) {
20574
20576
  const heard = file3.transcript === void 0 ? "" : `, which says: "${file3.transcript}"`;
@@ -20582,17 +20584,17 @@ function renderReplyingTo(replying, reading) {
20582
20584
  const whose = author === reading.taker ? "your message" : author === reading.sender.id ? "their own message" : `${withRole(replying.from.name, replying.from)}'s message`;
20583
20585
  const gone = replying.deleted === true ? ", which has since been deleted" : "";
20584
20586
  const replier = withRole(reading.sender.name, reading.sender);
20585
- const said = [
20587
+ const said2 = [
20586
20588
  `${replier} is replying to ${whose} from ${saidWhen(replying.at, reading)}${gone}.`
20587
20589
  ];
20588
20590
  if (replying.before !== void 0 && replying.before.length > 0) {
20589
- said.push(REPLY_BEFORE_HEADING, ...replying.before.map(renderLine));
20591
+ said2.push(REPLY_BEFORE_HEADING, ...replying.before.map(renderLine));
20590
20592
  }
20591
20593
  if (replying.deleted === true)
20592
- return said.join("\n");
20594
+ return said2.join("\n");
20593
20595
  const edited = replying.edited === true ? " (edited)" : "";
20594
- said.push(`${whose[0]?.toUpperCase()}${whose.slice(1)}${edited}:`, ...quoteLines(replying));
20595
- return said.join("\n");
20596
+ said2.push(`${whose[0]?.toUpperCase()}${whose.slice(1)}${edited}:`, ...quoteLines(replying));
20597
+ return said2.join("\n");
20596
20598
  }
20597
20599
  function quoteLines(replying) {
20598
20600
  const text = replying.text ?? "";
@@ -20608,7 +20610,7 @@ function quoteLines(replying) {
20608
20610
  body.push(`(sent ${replying.files.join(", ")})`);
20609
20611
  }
20610
20612
  }
20611
- return body.map((line) => line === "" ? ">" : `> ${line}`);
20613
+ return body.map((line2) => line2 === "" ? ">" : `> ${line2}`);
20612
20614
  }
20613
20615
  var DAY_MS = 864e5;
20614
20616
  function saidWhen(at, reading) {
@@ -20698,12 +20700,14 @@ var contextAnswer = {
20698
20700
  message: external_exports.string().min(1).max(300).optional().describe("What went wrong, for the person.")
20699
20701
  }).optional().describe("An edit not made. The copy beside it is the file as it is now.")
20700
20702
  };
20703
+ 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
20704
  function uniqueNames(files) {
20702
20705
  return new Set(files.map((file3) => file3.name)).size === files.length;
20703
20706
  }
20704
20707
  var agentContextPut = external_exports.object({
20705
20708
  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
20709
  not_shown: external_exports.array(notShownFile).max(CONTEXT_NOT_SHOWN_MAX).optional(),
20710
+ read_note: readNote.optional(),
20707
20711
  ...contextAnswer
20708
20712
  }).describe("`PUT /v1/agents/:id/context`: a harness's copy of one of its agents' files.");
20709
20713
  var harnessContextPut = external_exports.object({
@@ -20736,6 +20740,7 @@ var agentContextView = external_exports.object({
20736
20740
  harness_id: id("harness"),
20737
20741
  files: external_exports.array(contextFile),
20738
20742
  not_shown: external_exports.array(notShownFile),
20743
+ read_note: readNote.optional(),
20739
20744
  read_at: isoTimestamp.describe("When the harness last sent its copy."),
20740
20745
  away: external_exports.boolean().describe("No socket of the harness hears edits now: read, but not saved.")
20741
20746
  }).describe("`GET /v1/agents/:id/context`, for the harness's owner.");
@@ -22401,6 +22406,89 @@ var EVENT_TYPES = EVENTS.map((event) => event.type);
22401
22406
  var byType = new Map(EVENTS.map((event) => [event.type, event]));
22402
22407
  var schemas = new Map(EVENTS.map((event) => [event.type, eventSchema(event)]));
22403
22408
 
22409
+ // node_modules/@chipschat/protocol/dist/src/names.js
22410
+ var LOOK_LIKE = {
22411
+ A: "\u0410\u0391",
22412
+ // Cyrillic А, Greek Α
22413
+ B: "\u0412\u0392",
22414
+ // Cyrillic В, Greek Β
22415
+ C: "\u0421",
22416
+ // Cyrillic С
22417
+ E: "\u0415\u0395",
22418
+ // Cyrillic Е, Greek Ε
22419
+ H: "\u041D\u0397",
22420
+ // Cyrillic Н, Greek Η
22421
+ I: "\u0406\u04C0\u0399",
22422
+ // Cyrillic І and Ӏ, Greek Ι
22423
+ J: "\u0408",
22424
+ // Cyrillic Ј
22425
+ K: "\u041A\u039A",
22426
+ // Cyrillic К, Greek Κ
22427
+ M: "\u041C\u039C",
22428
+ // Cyrillic М, Greek Μ
22429
+ N: "\u039D",
22430
+ // Greek Ν
22431
+ O: "\u041E\u039F",
22432
+ // Cyrillic О, Greek Ο
22433
+ P: "\u0420\u03A1",
22434
+ // Cyrillic Р, Greek Ρ
22435
+ Q: "\u051A",
22436
+ // Cyrillic Ԛ
22437
+ S: "\u0405",
22438
+ // Cyrillic Ѕ
22439
+ T: "\u0422\u03A4",
22440
+ // Cyrillic Т, Greek Τ
22441
+ W: "\u051C",
22442
+ // Cyrillic Ԝ
22443
+ X: "\u0425\u03A7",
22444
+ // Cyrillic Х, Greek Χ
22445
+ Y: "\u0423\u04AE\u03A5",
22446
+ // Cyrillic У and Ү, Greek Υ
22447
+ Z: "\u0396",
22448
+ // Greek Ζ
22449
+ a: "\u0430\u03B1",
22450
+ // Cyrillic а, Greek α
22451
+ c: "\u0441",
22452
+ // Cyrillic с
22453
+ d: "\u0501",
22454
+ // Cyrillic ԁ
22455
+ e: "\u0435",
22456
+ // Cyrillic е
22457
+ h: "\u04BB",
22458
+ // Cyrillic һ
22459
+ i: "\u0456\u03B9\u0131",
22460
+ // Cyrillic і, Greek ι, Latin dotless ı
22461
+ j: "\u0458",
22462
+ // Cyrillic ј
22463
+ k: "\u03BA",
22464
+ // Greek κ
22465
+ l: "\u04CF",
22466
+ // Cyrillic ӏ
22467
+ o: "\u043E\u03BF",
22468
+ // Cyrillic о, Greek ο
22469
+ p: "\u0440\u03C1",
22470
+ // Cyrillic р, Greek ρ
22471
+ q: "\u051B",
22472
+ // Cyrillic ԛ
22473
+ s: "\u0455",
22474
+ // Cyrillic ѕ
22475
+ u: "\u03C5",
22476
+ // Greek υ
22477
+ v: "\u03BD",
22478
+ // Greek ν
22479
+ w: "\u051D",
22480
+ // Cyrillic ԝ
22481
+ x: "\u0445\u03C7",
22482
+ // Cyrillic х, Greek χ
22483
+ y: "\u0443\u04AF"
22484
+ // Cyrillic у and ү
22485
+ };
22486
+ var LATIN_OF = new Map(Object.entries(LOOK_LIKE).flatMap(([latin, others]) => [...others].map((other) => [other, latin])));
22487
+ var HIDDEN = "\\p{Cc}\\p{Cf}\\u034f\\u115f\\u1160\\u3164\\uffa0\\u2800";
22488
+ var UNSEEN = new RegExp(`[${HIDDEN}\\ufe00-\\ufe0f\\u{e0100}-\\u{e01ef}]`, "gu");
22489
+ var HIDDEN_ONE = new RegExp(`[${HIDDEN}]`, "u");
22490
+ var ALPHABETS = [new RegExp("\\p{Script=Latin}", "u"), new RegExp("\\p{Script=Cyrillic}", "u"), new RegExp("\\p{Script=Greek}", "u")];
22491
+
22404
22492
  // node_modules/@chipschat/protocol/dist/src/first-names.js
22405
22493
  var AGENT_FIRST_NAMES = [
22406
22494
  "Abel Aldo Amir Amos Anders Ansel Anton Ari Arlo Arne Aron Arvid Asher Avi Axel Aziz",
@@ -22429,7 +22517,7 @@ var AGENT_FIRST_NAMES = [
22429
22517
  "Xander Xavi Xavier",
22430
22518
  "Yann Yaro Yosef Yuri Yusuf Yves",
22431
22519
  "Zaid Zane Zeke Zeno Zev Ziggy"
22432
- ].flatMap((line) => line.split(" "));
22520
+ ].flatMap((line2) => line2.split(" "));
22433
22521
 
22434
22522
  // node_modules/@chipschat/protocol/dist/src/frames.js
22435
22523
  var clientHelloFrame = external_exports.object({
@@ -22758,8 +22846,8 @@ function createOutbox() {
22758
22846
  const waiting = /* @__PURE__ */ new Map();
22759
22847
  return {
22760
22848
  add(key, event) {
22761
- return new Promise((resolve5, reject) => {
22762
- waiting.set(key, { event, resolve: resolve5, reject });
22849
+ return new Promise((resolve7, reject) => {
22850
+ waiting.set(key, { event, resolve: resolve7, reject });
22763
22851
  });
22764
22852
  },
22765
22853
  unanswered: () => [...waiting.values()].map((entry) => entry.event),
@@ -22835,7 +22923,7 @@ async function inPieces(call, path, bytes) {
22835
22923
  async function afterLoss(error62, failures) {
22836
22924
  if (error62 instanceof RefusedError || failures >= PIECE_TRIES)
22837
22925
  throw error62;
22838
- await new Promise((resolve5) => setTimeout(resolve5, 250 * failures));
22926
+ await new Promise((resolve7) => setTimeout(resolve7, 250 * failures));
22839
22927
  }
22840
22928
  function sendPiece(call, path, bytes, offset) {
22841
22929
  const piece = bytes.subarray(offset, Math.min(offset + PIECE_BYTES, bytes.byteLength));
@@ -22861,20 +22949,20 @@ var TRIES = 4;
22861
22949
  var MARKS = /^(<<<<<<<|>>>>>>>) /m;
22862
22950
  var REJECTED = /\[rejected\]|fetch first|non-fast-forward/;
22863
22951
  var OWN_CONFIG = /^(core\.(repositoryformatversion|filemode|bare|logallrefupdates|ignorecase|precomposeunicode|symlinks)|remote\.origin\.(url|fetch)|branch\..+\.(remote|merge)|user\.(name|email))$/;
22864
- var lines = (said) => said.split("\n").filter((line) => line !== "");
22952
+ var lines = (said2) => said2.split("\n").filter((line2) => line2 !== "");
22865
22953
  var unique = (names) => [...new Set(names)];
22866
22954
  function subjectOf(words4) {
22867
- const first = words4.split("\n").find((line) => line.trim() !== "")?.trim() ?? "";
22955
+ const first = words4.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
22868
22956
  if (first === "")
22869
22957
  return "A change to the project";
22870
22958
  return first.length <= 72 ? first : `${first.slice(0, 71)}\u2026`;
22871
22959
  }
22872
22960
  function remoteWords(err) {
22873
- const said = lines(err).filter((line) => line.startsWith("remote: ")).map((line) => line.slice("remote: ".length).trim()).filter((line) => line !== "" && !/^error: hook declined/.test(line));
22874
- return said.length > 0 ? said.join(" ") : lines(err)[0] ?? "git said nothing";
22961
+ const said2 = lines(err).filter((line2) => line2.startsWith("remote: ")).map((line2) => line2.slice("remote: ".length).trim()).filter((line2) => line2 !== "" && !/^error: hook declined/.test(line2));
22962
+ return said2.length > 0 ? said2.join(" ") : lines(err)[0] ?? "git said nothing";
22875
22963
  }
22876
- function versionIn(said) {
22877
- const found = /(\d+)\.(\d+)/.exec(said);
22964
+ function versionIn(said2) {
22965
+ const found = /(\d+)\.(\d+)/.exec(said2);
22878
22966
  return found === null ? void 0 : [Number(found[1]), Number(found[2])];
22879
22967
  }
22880
22968
  function pushedInWords(pushed) {
@@ -22919,13 +23007,13 @@ function heldNote(pushed, name2) {
22919
23007
  }
22920
23008
  async function checkGit(git2 = "git", path = process.env["PATH"] ?? "/usr/bin:/bin") {
22921
23009
  const [major, minor] = versionIn(PROJECT_GIT_VERSION) ?? [0, 0];
22922
- return new Promise((resolve5) => {
23010
+ return new Promise((resolve7) => {
22923
23011
  execFile(git2, ["--version"], { env: { PATH: path } }, (error62, stdout) => {
22924
23012
  if (error62 !== null)
22925
- return resolve5({ ok: false, why: "git was not found on this machine" });
23013
+ return resolve7({ ok: false, why: "git was not found on this machine" });
22926
23014
  const mine = versionIn(stdout);
22927
23015
  const enough = mine !== void 0 && (mine[0] > major || mine[0] === major && mine[1] >= minor);
22928
- resolve5(enough ? { ok: true, version: mine.join(".") } : { ok: false, why: `${stdout.trim()} is older than ${PROJECT_GIT_VERSION}` });
23016
+ resolve7(enough ? { ok: true, version: mine.join(".") } : { ok: false, why: `${stdout.trim()} is older than ${PROJECT_GIT_VERSION}` });
22929
23017
  });
22930
23018
  });
22931
23019
  }
@@ -23054,8 +23142,8 @@ var Project = class {
23054
23142
  async standing(remote) {
23055
23143
  const mine = await this.head("HEAD");
23056
23144
  const base = await this.baseOf(mine, remote);
23057
- const counted = lines((await this.git(["diff", "--numstat", "--no-renames", "-z", base])).out.replace(/\0/g, "\n")).map((line) => {
23058
- const [added2 = "0", removed = "0", ...path] = line.split(" ");
23145
+ const counted = lines((await this.git(["diff", "--numstat", "--no-renames", "-z", base])).out.replace(/\0/g, "\n")).map((line2) => {
23146
+ const [added2 = "0", removed = "0", ...path] = line2.split(" ");
23059
23147
  return { path: path.join(" "), added: Number(added2) || 0, removed: Number(removed) || 0 };
23060
23148
  });
23061
23149
  const added = lines((await this.git(["ls-files", "--others", "--exclude-standard", "-z"])).out.replace(/\0/g, "\n"));
@@ -23101,8 +23189,8 @@ var Project = class {
23101
23189
  async markedLines(path) {
23102
23190
  try {
23103
23191
  const all = (await readFile(join(this.copy, path), "utf8")).split("\n");
23104
- const start = all.findIndex((line) => line.startsWith("<<<<<<< "));
23105
- const end2 = all.findIndex((line, at) => at > start && line.startsWith(">>>>>>> "));
23192
+ const start = all.findIndex((line2) => line2.startsWith("<<<<<<< "));
23193
+ const end2 = all.findIndex((line2, at) => at > start && line2.startsWith(">>>>>>> "));
23106
23194
  if (start < 0 || end2 < 0)
23107
23195
  return void 0;
23108
23196
  const shown2 = all.slice(Math.max(0, start - 1), end2 + 2);
@@ -23141,10 +23229,10 @@ var Project = class {
23141
23229
  const files = lines((await this.git(["diff", "--name-only", from, commit])).out);
23142
23230
  return { state: "sent", commit, files, over: earlier };
23143
23231
  }
23144
- const said = `${sent.out}
23232
+ const said2 = `${sent.out}
23145
23233
  ${sent.err}`;
23146
- if (!REJECTED.test(said))
23147
- return { state: "refused", why: remoteWords(said) };
23234
+ if (!REJECTED.test(said2))
23235
+ return { state: "refused", why: remoteWords(said2) };
23148
23236
  await this.fetch();
23149
23237
  const theirs = await this.head("refs/remotes/origin/main");
23150
23238
  const over = unique([...earlier, ...await this.authors(`HEAD..${theirs}`)]);
@@ -23371,8 +23459,8 @@ ${sent.err}`;
23371
23459
  env[`GIT_CONFIG_KEY_${at}`] = key;
23372
23460
  env[`GIT_CONFIG_VALUE_${at}`] = value;
23373
23461
  });
23374
- return new Promise((resolve5) => {
23375
- execFile(options.git ?? "git", [...args], { cwd: this.copy, env, maxBuffer: 64 * 1024 * 1024 }, (error62, stdout, stderr) => resolve5({ ok: error62 === null, out: stdout, err: stderr }));
23462
+ return new Promise((resolve7) => {
23463
+ execFile(options.git ?? "git", [...args], { cwd: this.copy, env, maxBuffer: 64 * 1024 * 1024 }, (error62, stdout, stderr) => resolve7({ ok: error62 === null, out: stdout, err: stderr }));
23376
23464
  });
23377
23465
  }
23378
23466
  };
@@ -23395,8 +23483,8 @@ var Lane = class {
23395
23483
  constructor(id2, wire) {
23396
23484
  this.id = id2;
23397
23485
  this.wire = wire;
23398
- this.result = new Promise((resolve5) => {
23399
- this.settle = resolve5;
23486
+ this.result = new Promise((resolve7) => {
23487
+ this.settle = resolve7;
23400
23488
  });
23401
23489
  this.stop = wire.hear(id2, (event) => this.onUpdated(event));
23402
23490
  }
@@ -23539,7 +23627,7 @@ var Run = class {
23539
23627
  /** Resolve once the run is not paused. A stopped run resolves too: check `over` after. */
23540
23628
  async whenRunning() {
23541
23629
  while (this.held && !this.ended) {
23542
- await new Promise((resolve5) => setTimeout(resolve5, 50));
23630
+ await new Promise((resolve7) => setTimeout(resolve7, 50));
23543
23631
  }
23544
23632
  }
23545
23633
  status(input2) {
@@ -24323,8 +24411,8 @@ var Chips = class _Chips {
24323
24411
  }
24324
24412
  /** Resolve with the first event that matches, now or later. */
24325
24413
  waitFor(match) {
24326
- return new Promise((resolve5) => {
24327
- this.waiters.add({ match, resolve: resolve5 });
24414
+ return new Promise((resolve7) => {
24415
+ this.waiters.add({ match, resolve: resolve7 });
24328
24416
  });
24329
24417
  }
24330
24418
  close() {
@@ -24351,7 +24439,7 @@ var Chips = class _Chips {
24351
24439
  return `${base}/v1/ws?ticket=${encodeURIComponent(String(ticket))}`;
24352
24440
  }
24353
24441
  open(first) {
24354
- return new Promise((resolve5, reject) => {
24442
+ return new Promise((resolve7, reject) => {
24355
24443
  this.setState("connecting");
24356
24444
  const failed = (what, error62) => {
24357
24445
  if (first)
@@ -24369,13 +24457,13 @@ var Chips = class _Chips {
24369
24457
  }
24370
24458
  this.socket = socket;
24371
24459
  this.listen(socket, {
24372
- settled: (hello2) => this.settled(socket, hello2, first, resolve5, reject)
24460
+ settled: (hello2) => this.settled(socket, hello2, first, resolve7, reject)
24373
24461
  });
24374
24462
  }, (error62) => failed("could not get a socket ticket", error62));
24375
24463
  });
24376
24464
  }
24377
24465
  /** The handshake's outcome: the hello, a refusal, or the socket gone before either. */
24378
- settled(socket, hello2, first, resolve5, reject) {
24466
+ settled(socket, hello2, first, resolve7, reject) {
24379
24467
  if (hello2 === void 0) {
24380
24468
  if (first)
24381
24469
  reject(new ConnectError("the server closed the socket before saying hello"));
@@ -24389,7 +24477,7 @@ var Chips = class _Chips {
24389
24477
  return;
24390
24478
  }
24391
24479
  this.onHello(socket, hello2);
24392
- void this.subscribed(socket).then(resolve5);
24480
+ void this.subscribed(socket).then(resolve7);
24393
24481
  }
24394
24482
  /** The socket's four events. `settled` fires once: with the hello, a refusal, or nothing. */
24395
24483
  listen(socket, on) {
@@ -24484,15 +24572,15 @@ var Chips = class _Chips {
24484
24572
  subscribed(socket) {
24485
24573
  this.pings += 1;
24486
24574
  const ts = Date.now() + this.pings;
24487
- return new Promise((resolve5) => {
24575
+ return new Promise((resolve7) => {
24488
24576
  const timer = setTimeout(() => {
24489
24577
  this.pongs.delete(ts);
24490
- resolve5();
24578
+ resolve7();
24491
24579
  }, 2e3);
24492
24580
  this.pongs.set(ts, () => {
24493
24581
  clearTimeout(timer);
24494
24582
  this.pongs.delete(ts);
24495
- resolve5();
24583
+ resolve7();
24496
24584
  });
24497
24585
  socket.send(JSON.stringify({ frame: "ping", ts }));
24498
24586
  });
@@ -24875,7 +24963,7 @@ function openKeyStore(env, platform = process.platform) {
24875
24963
  // node_modules/@chipschat/harness/dist/steps.js
24876
24964
  import { writeSync } from "node:fs";
24877
24965
  var STEP_MARK = "chips-step ";
24878
- function reportStep(step, detail = {}, env = process.env, write = (line) => void writeSync(1, line)) {
24966
+ function reportStep(step, detail = {}, env = process.env, write = (line2) => void writeSync(1, line2)) {
24879
24967
  if (env["CHIPS_STEPS"] !== "json")
24880
24968
  return;
24881
24969
  try {
@@ -24896,8 +24984,8 @@ var JoinError = class extends Error {
24896
24984
  };
24897
24985
  function serverJsonUrl(env) {
24898
24986
  try {
24899
- const said = JSON.parse(readFileSync2(joinPath(configHome(env), "server.json"), "utf8"));
24900
- return typeof said.url === "string" && said.url !== "" ? said.url : void 0;
24987
+ const said2 = JSON.parse(readFileSync2(joinPath(configHome(env), "server.json"), "utf8"));
24988
+ return typeof said2.url === "string" && said2.url !== "" ? said2.url : void 0;
24901
24989
  } catch {
24902
24990
  return void 0;
24903
24991
  }
@@ -24956,8 +25044,8 @@ async function refused(response) {
24956
25044
  const body = await response.json().catch(() => ({}));
24957
25045
  return new JoinError(body.error?.refusal ?? body.error?.code ?? `http_${response.status}`, body.error?.message ?? `Chips answered ${response.status}.`);
24958
25046
  }
24959
- var wait = (ms, signal) => new Promise((resolve5, reject) => {
24960
- const timer = setTimeout(resolve5, ms);
25047
+ var wait = (ms, signal) => new Promise((resolve7, reject) => {
25048
+ const timer = setTimeout(resolve7, ms);
24961
25049
  signal?.addEventListener("abort", () => {
24962
25050
  clearTimeout(timer);
24963
25051
  reject(new JoinError("stopped", "Stopped before a person decided."));
@@ -25081,8 +25169,8 @@ var MOST_FILES = 20;
25081
25169
  var MOST_FILE_BYTES = 100 * 1024 * 1024;
25082
25170
  function filesNamedIn(answer) {
25083
25171
  const named = [];
25084
- const kept = answer.split("\n").filter((line) => {
25085
- const media = /^\s*MEDIA:\s*(\S.*?)\s*$/.exec(line);
25172
+ const kept = answer.split("\n").filter((line2) => {
25173
+ const media = /^\s*MEDIA:\s*(\S.*?)\s*$/.exec(line2);
25086
25174
  if (media?.[1] === void 0)
25087
25175
  return true;
25088
25176
  if (!named.includes(media[1]))
@@ -25097,21 +25185,21 @@ function filesNamedIn(answer) {
25097
25185
  function inside(path, folder2) {
25098
25186
  return path === folder2 || path.startsWith(folder2.endsWith(sep) ? folder2 : folder2 + sep);
25099
25187
  }
25100
- function pathOf(said) {
25101
- if (/^https?:\/\//i.test(said))
25102
- return { kind: "link", url: said };
25103
- if (/^file:/i.test(said)) {
25188
+ function pathOf(said2) {
25189
+ if (/^https?:\/\//i.test(said2))
25190
+ return { kind: "link", url: said2 };
25191
+ if (/^file:/i.test(said2)) {
25104
25192
  try {
25105
- return fileURLToPath(said);
25193
+ return fileURLToPath(said2);
25106
25194
  } catch {
25107
- return { kind: "refused", name: said, why: "its address is not a file's" };
25195
+ return { kind: "refused", name: said2, why: "its address is not a file's" };
25108
25196
  }
25109
25197
  }
25110
- return said === "~" || said.startsWith("~/") ? homedir2() + said.slice(1) : said;
25198
+ return said2 === "~" || said2.startsWith("~/") ? homedir2() + said2.slice(1) : said2;
25111
25199
  }
25112
25200
  async function refusalOf2(real, folders) {
25113
25201
  const roots = await Promise.all(folders.map((folder2) => realpath(folder2).catch(() => void 0)));
25114
- if (!roots.some((root) => root !== void 0 && inside(real, root))) {
25202
+ if (!roots.some((root2) => root2 !== void 0 && inside(real, root2))) {
25115
25203
  return "it is outside the agent's folder";
25116
25204
  }
25117
25205
  const info = await stat2(real);
@@ -25124,11 +25212,11 @@ async function refusalOf2(real, folders) {
25124
25212
  return void 0;
25125
25213
  }
25126
25214
  async function readSendable(reference, folders) {
25127
- const said = reference.trim().replace(/^<(.*)>$/, "$1");
25128
- const path = pathOf(said);
25215
+ const said2 = reference.trim().replace(/^<(.*)>$/, "$1");
25216
+ const path = pathOf(said2);
25129
25217
  if (typeof path !== "string")
25130
25218
  return path;
25131
- const name2 = basename2(path) || said;
25219
+ const name2 = basename2(path) || said2;
25132
25220
  const first = folders[0];
25133
25221
  if (first === void 0) {
25134
25222
  return { kind: "refused", name: name2, why: "this agent has no folder its files are sent from" };
@@ -25466,8 +25554,812 @@ function fileStore(path) {
25466
25554
  }
25467
25555
 
25468
25556
  // src/bridge.ts
25469
- import { mkdirSync as mkdirSync4 } from "node:fs";
25470
- import { join as join5 } from "node:path";
25557
+ import { mkdirSync as mkdirSync5 } from "node:fs";
25558
+ import { join as join8 } from "node:path";
25559
+
25560
+ // src/context.ts
25561
+ import { spawn } from "node:child_process";
25562
+ import { createHash as createHash2, randomBytes } from "node:crypto";
25563
+ import {
25564
+ closeSync,
25565
+ existsSync as existsSync6,
25566
+ fsyncSync,
25567
+ lstatSync,
25568
+ mkdirSync as mkdirSync4,
25569
+ openSync,
25570
+ readFileSync as readFileSync6,
25571
+ realpathSync,
25572
+ renameSync as renameSync2,
25573
+ rmSync as rmSync3,
25574
+ statSync as statSync2,
25575
+ writeSync as writeSync2
25576
+ } from "node:fs";
25577
+ import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
25578
+
25579
+ // src/home.ts
25580
+ import { execFileSync as execFileSync3 } from "node:child_process";
25581
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
25582
+ import { homedir as homedir4 } from "node:os";
25583
+ import { join as join5, resolve as resolve2 } from "node:path";
25584
+ function hermesRoot(env = process.env) {
25585
+ return resolve2(env["HERMES_HOME"] || join5(homedir4(), ".hermes"));
25586
+ }
25587
+ function profileHome(root2, profile) {
25588
+ const name2 = (profile ?? "default").trim().toLowerCase();
25589
+ return name2 === "" || name2 === "default" ? root2 : join5(root2, "profiles", name2);
25590
+ }
25591
+ function profileName(profile) {
25592
+ const name2 = (profile ?? "default").trim().toLowerCase();
25593
+ return name2 === "" ? "default" : name2;
25594
+ }
25595
+ function envValue(home2, name2) {
25596
+ const path = join5(home2, ".env");
25597
+ if (!existsSync5(path)) return void 0;
25598
+ let found;
25599
+ for (const line2 of readFileSync5(path, "utf8").split("\n")) {
25600
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line2);
25601
+ if (match?.[1] !== name2) continue;
25602
+ const raw = match[2] ?? "";
25603
+ const quoted2 = /^(["'])(.*)\1/.exec(raw);
25604
+ found = quoted2 !== null ? quoted2[2] ?? "" : raw.replace(/\s+#.*$/, "");
25605
+ }
25606
+ return found;
25607
+ }
25608
+ function configText(home2) {
25609
+ const path = join5(home2, "config.yaml");
25610
+ try {
25611
+ return existsSync5(path) ? readFileSync5(path, "utf8") : "";
25612
+ } catch {
25613
+ return "";
25614
+ }
25615
+ }
25616
+ function uncommented(line2) {
25617
+ const indent = line2.length - line2.trimStart().length;
25618
+ let quote;
25619
+ for (let at = indent; at < line2.length; at += 1) {
25620
+ const char = line2[at];
25621
+ if (quote !== void 0) {
25622
+ if (char === quote) quote = void 0;
25623
+ } else if (char === '"' || char === "'") {
25624
+ quote = char;
25625
+ } else if (char === "#" && (at === indent || /\s/.test(line2[at - 1] ?? ""))) {
25626
+ return { indent, text: line2.slice(indent, at).trimEnd() };
25627
+ }
25628
+ }
25629
+ return { indent, text: line2.slice(indent).trimEnd() };
25630
+ }
25631
+ function scalar(text) {
25632
+ const trimmed = text.trim();
25633
+ const quoted2 = /^(["'])(.*)\1$/.exec(trimmed);
25634
+ return quoted2 !== null ? quoted2[2] ?? "" : trimmed;
25635
+ }
25636
+ function flowPairs(text) {
25637
+ const inner = /^\{(.*)\}$/.exec(text.trim())?.[1];
25638
+ if (inner === void 0) return void 0;
25639
+ const pairs = /* @__PURE__ */ new Map();
25640
+ for (const part of inner.split(",")) {
25641
+ if (part.trim() === "") continue;
25642
+ const at = part.indexOf(":");
25643
+ if (at === -1) return void 0;
25644
+ pairs.set(part.slice(0, at).trim(), scalar(part.slice(at + 1)));
25645
+ }
25646
+ return pairs;
25647
+ }
25648
+ function yamlValue(text, section, key) {
25649
+ const lines2 = text.split("\n");
25650
+ for (let at = 0; at < lines2.length; at += 1) {
25651
+ const { indent, text: line2 } = uncommented(lines2[at] ?? "");
25652
+ if (indent !== 0 || !line2.startsWith(`${section}:`)) continue;
25653
+ const rest = line2.slice(section.length + 1).trim();
25654
+ if (rest !== "") return flowPairs(rest)?.get(key);
25655
+ let child;
25656
+ for (let below = at + 1; below < lines2.length; below += 1) {
25657
+ const next = uncommented(lines2[below] ?? "");
25658
+ if (next.text === "") continue;
25659
+ if (next.indent === 0) return void 0;
25660
+ child ??= next.indent;
25661
+ if (next.indent !== child) continue;
25662
+ const match = /^([A-Za-z0-9_.-]+):(.*)$/.exec(next.text);
25663
+ if (match?.[1] === key) return scalar(match[2] ?? "");
25664
+ }
25665
+ return void 0;
25666
+ }
25667
+ return void 0;
25668
+ }
25669
+ function yamlBool(value) {
25670
+ if (value === void 0) return void 0;
25671
+ const word = value.toLowerCase();
25672
+ if (["true", "yes", "on", "y"].includes(word)) return true;
25673
+ if (["false", "no", "off", "n"].includes(word)) return false;
25674
+ return void 0;
25675
+ }
25676
+ function yamlNumber(value) {
25677
+ if (value === void 0 || value === "" || value === "null" || value === "~") return void 0;
25678
+ const number4 = Number(value.replace(/_/g, ""));
25679
+ return Number.isFinite(number4) ? number4 : void 0;
25680
+ }
25681
+ function expanded(path) {
25682
+ return resolve2(path === "~" || path.startsWith("~/") ? homedir4() + path.slice(1) : path);
25683
+ }
25684
+ function isFolder(path) {
25685
+ try {
25686
+ return statSync(path).isDirectory();
25687
+ } catch {
25688
+ return false;
25689
+ }
25690
+ }
25691
+ function workFolder(home2, env = process.env) {
25692
+ const configured = yamlValue(configText(home2), "terminal", "cwd");
25693
+ if (configured !== void 0 && configured !== "" && configured !== ".") {
25694
+ const folder2 = expanded(configured);
25695
+ if (isFolder(folder2)) return folder2;
25696
+ }
25697
+ for (const name2 of ["TERMINAL_CWD", "MESSAGING_CWD"]) {
25698
+ const said2 = envValue(home2, name2) ?? env[name2];
25699
+ if (said2 !== void 0 && said2 !== "" && isFolder(expanded(said2))) return expanded(said2);
25700
+ }
25701
+ return gatewayFolder(home2);
25702
+ }
25703
+ var gateways = /* @__PURE__ */ new Map();
25704
+ function gatewayFolder(home2) {
25705
+ let raw;
25706
+ try {
25707
+ raw = readFileSync5(join5(home2, "gateway.pid"), "utf8").trim();
25708
+ } catch {
25709
+ return void 0;
25710
+ }
25711
+ const known = gateways.get(home2);
25712
+ if (known?.pidFile === raw) return known.folder;
25713
+ const folder2 = folderOfPid(raw);
25714
+ gateways.set(home2, { pidFile: raw, folder: folder2 });
25715
+ return folder2;
25716
+ }
25717
+ function folderOfPid(raw) {
25718
+ let pid;
25719
+ try {
25720
+ const said2 = raw.startsWith("{") ? JSON.parse(raw).pid : Number(raw);
25721
+ pid = typeof said2 === "number" && Number.isInteger(said2) && said2 > 0 ? said2 : void 0;
25722
+ } catch {
25723
+ return void 0;
25724
+ }
25725
+ return pid === void 0 ? void 0 : processFolder(pid);
25726
+ }
25727
+ function processFolder(pid) {
25728
+ try {
25729
+ if (process.platform === "linux") return readlinkSync(`/proc/${pid}/cwd`);
25730
+ const out = execFileSync3("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
25731
+ encoding: "utf8",
25732
+ timeout: 3e3,
25733
+ stdio: ["ignore", "pipe", "ignore"]
25734
+ });
25735
+ const line2 = out.split("\n").find((each) => each.startsWith("n"));
25736
+ return line2 === void 0 ? void 0 : line2.slice(1);
25737
+ } catch {
25738
+ return void 0;
25739
+ }
25740
+ }
25741
+
25742
+ // src/context.ts
25743
+ var FILES_SHOWN = 8;
25744
+ var FILE_BYTES = 65536;
25745
+ var READ_NOTE = "Hermes reads these when a conversation starts. A space already talking picks up a change after it next compacts.";
25746
+ var ENTRY_DELIMITER = "\n\xA7\n";
25747
+ var MEMORY_LIMIT = 2200;
25748
+ var USER_LIMIT = 1375;
25749
+ var PROJECT_FILES = [
25750
+ ".hermes.md",
25751
+ "HERMES.md",
25752
+ "AGENTS.override.md",
25753
+ "AGENTS.md",
25754
+ "agents.md",
25755
+ "CLAUDE.md",
25756
+ "claude.md",
25757
+ ".cursorrules"
25758
+ ];
25759
+ var ROWS = {
25760
+ soul: { title: "Who it is", role: "identity" },
25761
+ project: { title: "Project instructions", role: "instructions" },
25762
+ user: { title: "About you", role: "about_you" },
25763
+ memory: { title: "Memory", role: "memory" }
25764
+ };
25765
+ var ORDER = ["soul", "project", "user", "memory"];
25766
+ function readText(path) {
25767
+ try {
25768
+ return readFileSync6(path, "utf8").replace(/^/, "");
25769
+ } catch {
25770
+ return "";
25771
+ }
25772
+ }
25773
+ function hashOf(text) {
25774
+ return createHash2("sha256").update(text, "utf8").digest("hex");
25775
+ }
25776
+ function length(text) {
25777
+ let count = 0;
25778
+ for (const _ of text) count += 1;
25779
+ return count;
25780
+ }
25781
+ var said = (count) => count.toLocaleString("en-US");
25782
+ function entriesOf(text) {
25783
+ return [...new Set(text.split(ENTRY_DELIMITER).map((each) => each.trim()).filter((each) => each !== ""))];
25784
+ }
25785
+ function memoryProblem(text, limit, what) {
25786
+ const total = length(entriesOf(text).join(ENTRY_DELIMITER));
25787
+ if (total > limit) {
25788
+ return `That is ${said(total)} characters, and Hermes keeps ${what} to ${said(limit)}. Take out ${said(total - limit)}.`;
25789
+ }
25790
+ return void 0;
25791
+ }
25792
+ function memorySettings(home2) {
25793
+ const text = configText(home2);
25794
+ const value = (key) => yamlValue(text, "memory", key);
25795
+ return {
25796
+ ...yamlBool(value("memory_enabled")) === false ? {} : { memory: yamlNumber(value("memory_char_limit")) ?? MEMORY_LIMIT },
25797
+ ...yamlBool(value("user_profile_enabled")) === false ? {} : { user: yamlNumber(value("user_char_limit")) ?? USER_LIMIT }
25798
+ };
25799
+ }
25800
+ function sourcesOf(profile, env = process.env) {
25801
+ const { name: name2, home: home2 } = profile;
25802
+ const found = [];
25803
+ const soul = join6(home2, "SOUL.md");
25804
+ if (existsSync6(soul)) found.push({ path: soul, kind: "soul", file: "SOUL.md", profile: name2 });
25805
+ const folder2 = workFolder(home2, env);
25806
+ const project = folder2 === void 0 ? void 0 : PROJECT_FILES.find((file3) => readText(join6(folder2, file3)).trim() !== "");
25807
+ if (folder2 !== void 0 && project !== void 0) {
25808
+ found.push({ path: join6(folder2, project), kind: "project", file: project, profile: name2 });
25809
+ }
25810
+ const memory = memorySettings(home2);
25811
+ if (memory.user !== void 0) {
25812
+ found.push({ path: join6(home2, "memories", "USER.md"), kind: "user", file: "USER.md", profile: name2, limit: memory.user });
25813
+ }
25814
+ if (memory.memory !== void 0) {
25815
+ found.push({ path: join6(home2, "memories", "MEMORY.md"), kind: "memory", file: "MEMORY.md", profile: name2, limit: memory.memory });
25816
+ }
25817
+ return found;
25818
+ }
25819
+ function aboutOf(source2) {
25820
+ if (source2.limit === void 0) return void 0;
25821
+ const what = source2.kind === "user" ? "What it knows of you" : "Its notes";
25822
+ return `${what}, up to ${said(source2.limit)} characters. A line with only \xA7 starts the next entry.`;
25823
+ }
25824
+ function fileOf(source2, name2) {
25825
+ const text = readText(source2.path);
25826
+ const about = aboutOf(source2);
25827
+ return {
25828
+ tooLarge: Buffer.byteLength(text, "utf8") > FILE_BYTES,
25829
+ file: {
25830
+ name: name2,
25831
+ title: ROWS[source2.kind].title,
25832
+ role: ROWS[source2.kind].role,
25833
+ ...about === void 0 ? {} : { about },
25834
+ text,
25835
+ hash: hashOf(text),
25836
+ editable: true
25837
+ }
25838
+ };
25839
+ }
25840
+ var byOrder = (a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) || a.profile.localeCompare(b.profile);
25841
+ var Picture = class {
25842
+ /** Each file by its path, and the agents that read it. */
25843
+ readers = /* @__PURE__ */ new Map();
25844
+ /** The shared files, each with its id and the name the set shows. */
25845
+ shared = /* @__PURE__ */ new Map();
25846
+ /** The agents and the files each reads, in page order. */
25847
+ agents = /* @__PURE__ */ new Map();
25848
+ constructor(readers, first, env = process.env) {
25849
+ const byProfile = /* @__PURE__ */ new Map();
25850
+ const sourcesFor = (profile) => {
25851
+ let sources = byProfile.get(profile.home);
25852
+ if (sources === void 0) {
25853
+ sources = sourcesOf(profile, env);
25854
+ byProfile.set(profile.home, sources);
25855
+ }
25856
+ return sources;
25857
+ };
25858
+ if (first !== void 0) for (const source2 of sourcesFor(first)) this.readers.set(source2.path, { source: source2, agents: [] });
25859
+ for (const reader of readers) {
25860
+ const sources = sourcesFor(reader.profile);
25861
+ this.agents.set(reader.agentId, [...sources].sort(byOrder));
25862
+ for (const source2 of sources) {
25863
+ const entry = this.readers.get(source2.path) ?? { source: source2, agents: [] };
25864
+ if (!entry.agents.includes(reader.agentId)) entry.agents.push(reader.agentId);
25865
+ this.readers.set(source2.path, entry);
25866
+ }
25867
+ }
25868
+ const taken = /* @__PURE__ */ new Set();
25869
+ const ids2 = /* @__PURE__ */ new Set();
25870
+ const sharing = [...this.readers.values()].filter(({ source: source2, agents }) => source2.kind === "project" || source2.profile === first?.name || agents.length > 1).sort((a, b) => (a.source.profile === first?.name ? 0 : 1) - (b.source.profile === first?.name ? 0 : 1) || byOrder(a.source, b.source));
25871
+ for (const { source: source2 } of sharing) {
25872
+ const name2 = taken.has(source2.file) ? `${source2.file} (${source2.profile})` : source2.file;
25873
+ taken.add(name2);
25874
+ let id2 = `${source2.profile}-${source2.kind}`.slice(0, 60);
25875
+ for (let count = 2; ids2.has(id2); count += 1) id2 = `${source2.profile}-${source2.kind}-${count}`.slice(0, 64);
25876
+ ids2.add(id2);
25877
+ this.shared.set(source2.path, { id: id2, name: name2 });
25878
+ }
25879
+ }
25880
+ /** The harness's own set, as `PUT /v1/harnesses/:id/context` takes it. */
25881
+ harnessCopy(first) {
25882
+ const shown2 = [];
25883
+ const notShown = [];
25884
+ for (const [path, { id: id2, name: name2 }] of this.shared) {
25885
+ const entry = this.readers.get(path);
25886
+ if (entry === void 0) continue;
25887
+ const { source: source2, agents } = entry;
25888
+ const { file: file3, tooLarge } = fileOf(source2, name2);
25889
+ if (tooLarge || shown2.length >= FILES_SHOWN) {
25890
+ notShown.push({ name: name2, title: ROWS[source2.kind].title, reason: tooLarge ? "too_large" : "count" });
25891
+ continue;
25892
+ }
25893
+ shown2.push({
25894
+ ...file3,
25895
+ id: id2,
25896
+ used_by: agents.length,
25897
+ // A new agent runs on the first profile: it reads that profile's files and its project file.
25898
+ ...source2.profile === first?.name ? { for_new_agents: true } : {}
25899
+ });
25900
+ }
25901
+ return {
25902
+ shared: shown2,
25903
+ ...notShown.length === 0 ? {} : { not_shown: notShown }
25904
+ };
25905
+ }
25906
+ /** One agent's files, as `PUT /v1/agents/:id/context` takes them: its own, and the shared it reads. */
25907
+ agentCopy(agentId2) {
25908
+ const sources = this.agents.get(agentId2);
25909
+ if (sources === void 0) return void 0;
25910
+ const files = [];
25911
+ const notShown = [];
25912
+ for (const source2 of sources) {
25913
+ const shared = this.shared.get(source2.path);
25914
+ if (files.length >= FILES_SHOWN) {
25915
+ notShown.push({ name: shared?.name ?? source2.file, title: ROWS[source2.kind].title, reason: "count" });
25916
+ continue;
25917
+ }
25918
+ if (shared !== void 0) {
25919
+ files.push({
25920
+ name: shared.name,
25921
+ title: ROWS[source2.kind].title,
25922
+ role: ROWS[source2.kind].role,
25923
+ editable: true,
25924
+ shared: shared.id,
25925
+ replaceable: false
25926
+ });
25927
+ continue;
25928
+ }
25929
+ const { file: file3, tooLarge } = fileOf(source2, source2.file);
25930
+ if (tooLarge) notShown.push({ name: source2.file, title: ROWS[source2.kind].title, reason: "too_large" });
25931
+ else files.push(file3);
25932
+ }
25933
+ return {
25934
+ files,
25935
+ ...notShown.length === 0 ? {} : { not_shown: notShown },
25936
+ read_note: READ_NOTE
25937
+ };
25938
+ }
25939
+ /** The file an edit names: an agent's own, or one of the harness's set; with whether it is shared. */
25940
+ find(agentId2, name2) {
25941
+ if (agentId2 === void 0) {
25942
+ for (const [path, { name: shown2 }] of this.shared) {
25943
+ const source2 = this.readers.get(path)?.source;
25944
+ if (shown2 === name2 && source2 !== void 0) return { source: source2, shared: true };
25945
+ }
25946
+ return void 0;
25947
+ }
25948
+ for (const source2 of this.agents.get(agentId2) ?? []) {
25949
+ const shared = this.shared.get(source2.path);
25950
+ if ((shared?.name ?? source2.file) === name2) return { source: source2, shared: shared !== void 0 };
25951
+ }
25952
+ return void 0;
25953
+ }
25954
+ };
25955
+ var LOCK_WAIT_MS = 1e4;
25956
+ var LOCKER = [
25957
+ "use Fcntl qw(:flock O_RDWR O_CREAT O_NOFOLLOW);",
25958
+ 'sysopen(my $fh, $ARGV[0], O_RDWR|O_CREAT|O_NOFOLLOW, 0600) or die "open: $!\\n";',
25959
+ "chmod 0600, $fh;",
25960
+ 'flock($fh, LOCK_EX) or die "flock: $!\\n";',
25961
+ '$| = 1; print "locked\\n";',
25962
+ "while (<STDIN>) {}",
25963
+ "flock($fh, LOCK_UN);"
25964
+ ].join(" ");
25965
+ async function withLock(lockPath, work, waitMs = LOCK_WAIT_MS) {
25966
+ const child = spawn("perl", ["-e", LOCKER, lockPath], { stdio: ["pipe", "pipe", "pipe"] });
25967
+ const exited = new Promise((done) => {
25968
+ child.on("close", () => done());
25969
+ child.on("error", () => done());
25970
+ });
25971
+ try {
25972
+ await new Promise((locked, failed) => {
25973
+ const timer = setTimeout(() => failed(new Error("Hermes is writing its memory right now. Try again in a moment.")), waitMs);
25974
+ let out = "";
25975
+ child.stdout.on("data", (chunk) => {
25976
+ out += chunk.toString("utf8");
25977
+ if (out.includes("locked\n")) {
25978
+ clearTimeout(timer);
25979
+ locked();
25980
+ }
25981
+ });
25982
+ child.on("error", () => {
25983
+ clearTimeout(timer);
25984
+ failed(new Error("The bridge couldn't lock Hermes's memory file, so it saved nothing: this machine has no perl."));
25985
+ });
25986
+ child.on("close", () => {
25987
+ clearTimeout(timer);
25988
+ failed(new Error("The bridge couldn't lock Hermes's memory file, so it saved nothing."));
25989
+ });
25990
+ });
25991
+ return await work();
25992
+ } finally {
25993
+ child.stdin.end();
25994
+ if (child.exitCode === null) {
25995
+ const killer = setTimeout(() => child.kill(), 2e3);
25996
+ await exited;
25997
+ clearTimeout(killer);
25998
+ }
25999
+ }
26000
+ }
26001
+ function writeWhole(path, text) {
26002
+ const target = existsSync6(path) && lstatSync(path).isSymbolicLink() ? realpathSync(path) : path;
26003
+ const mode = existsSync6(target) ? statSync2(target).mode & 511 : 384;
26004
+ const temp = join6(dirname4(target), `.chips_${basename3(target)}.${randomBytes(4).toString("hex")}`);
26005
+ const fd = openSync(temp, "w", mode);
26006
+ try {
26007
+ writeSync2(fd, text, null, "utf8");
26008
+ fsyncSync(fd);
26009
+ } catch (error62) {
26010
+ closeSync(fd);
26011
+ rmSync3(temp, { force: true });
26012
+ throw error62;
26013
+ }
26014
+ closeSync(fd);
26015
+ try {
26016
+ renameSync2(temp, target);
26017
+ } catch (error62) {
26018
+ rmSync3(temp, { force: true });
26019
+ throw error62;
26020
+ }
26021
+ }
26022
+ async function saveEdit(source2, edit) {
26023
+ const memory = source2.limit !== void 0;
26024
+ if (memory) {
26025
+ const problem = memoryProblem(edit.text, source2.limit ?? MEMORY_LIMIT, source2.kind === "user" ? "what it knows of you" : "its memory");
26026
+ if (problem !== void 0) throw new Refuse(problem, void 0, "failed");
26027
+ }
26028
+ const save = () => {
26029
+ const now = readText(source2.path);
26030
+ if (edit.base_hash !== null && hashOf(now) !== edit.base_hash) {
26031
+ throw new Refuse("", void 0, "changed");
26032
+ }
26033
+ writeWhole(source2.path, memory ? entriesOf(edit.text).join(ENTRY_DELIMITER) : edit.text);
26034
+ };
26035
+ if (!memory) {
26036
+ save();
26037
+ return;
26038
+ }
26039
+ const folder2 = dirname4(source2.path);
26040
+ if (!existsSync6(dirname4(folder2))) throw new Refuse(`Hermes has no profile at ${dirname4(folder2)}.`, void 0, "failed");
26041
+ mkdirSync4(folder2, { recursive: true, mode: 448 });
26042
+ await withLock(`${source2.path}.lock`, save);
26043
+ }
26044
+ var Contexts = class {
26045
+ constructor(roster, port, log2, env = process.env) {
26046
+ this.roster = roster;
26047
+ this.port = port;
26048
+ this.log = log2;
26049
+ this.env = env;
26050
+ }
26051
+ roster;
26052
+ port;
26053
+ log;
26054
+ env;
26055
+ chain = Promise.resolve();
26056
+ /** Every copy read again, and sent where it moved. */
26057
+ refresh() {
26058
+ return this.inLine(async () => {
26059
+ const { readers, first } = this.roster();
26060
+ const picture = new Picture(readers, first, this.env);
26061
+ await this.port.harness(picture.harnessCopy(first)).catch((error62) => {
26062
+ this.log(`the harness's context not sent: ${messageOf(error62)}`);
26063
+ });
26064
+ for (const { agentId: agentId2 } of readers) {
26065
+ const copy = picture.agentCopy(agentId2);
26066
+ if (copy === void 0) continue;
26067
+ await this.port.agent(agentId2, copy).catch((error62) => {
26068
+ this.log(`${agentId2}'s context not sent: ${messageOf(error62)}`);
26069
+ });
26070
+ }
26071
+ }).catch((error62) => this.log(`the context wasn't read: ${messageOf(error62)}`));
26072
+ }
26073
+ /**
26074
+ * A person's edit from Chips: saved, and answered with the copy it is in now; or refused, the
26075
+ * copy as it is now beside the reason. The other copies the file is in follow.
26076
+ */
26077
+ async edit(frame) {
26078
+ const answer = await this.inLine(async () => {
26079
+ const { readers, first } = this.roster();
26080
+ const copyNow = () => {
26081
+ const picture = new Picture(readers, first, this.env);
26082
+ return frame.agent_id === void 0 ? picture.harnessCopy(first) : picture.agentCopy(frame.agent_id);
26083
+ };
26084
+ const found = new Picture(readers, first, this.env).find(frame.agent_id, frame.name);
26085
+ if (found === void 0) {
26086
+ throw new Refuse(`Hermes gives this agent no file called ${frame.name}.`, copyNow(), "failed");
26087
+ }
26088
+ const { source: source2, shared } = found;
26089
+ if (frame.base_hash === null) {
26090
+ const why = source2.kind === "project" ? "Every agent on Hermes's gateway reads the same project file, so one can't have its own." : `Every agent on Hermes's ${source2.profile} profile reads the same ${source2.file}, so one can't have its own.`;
26091
+ throw new Refuse(why, copyNow(), "failed");
26092
+ }
26093
+ try {
26094
+ await saveEdit(source2, frame);
26095
+ } catch (error62) {
26096
+ const reason = error62 instanceof Refuse ? error62.reason : "failed";
26097
+ this.log(`${source2.path} not saved from Chips: ${reason === "changed" ? "it changed since" : messageOf(error62)}`);
26098
+ throw new Refuse(error62 instanceof Refuse ? error62.message : messageOf(error62), copyNow(), reason);
26099
+ }
26100
+ this.log(`${source2.path} saved from Chips${shared ? ", for every agent that reads it" : ""}`);
26101
+ const copy = copyNow();
26102
+ if (copy === void 0) throw new Refuse("Saved, but the agent is gone.", void 0, "failed");
26103
+ return copy;
26104
+ });
26105
+ void this.refresh();
26106
+ return answer;
26107
+ }
26108
+ inLine(work) {
26109
+ const done = this.chain.catch(() => void 0).then(work);
26110
+ this.chain = done.catch(() => void 0);
26111
+ return done;
26112
+ }
26113
+ };
26114
+ function messageOf(error62) {
26115
+ return error62 instanceof Error ? error62.message : String(error62);
26116
+ }
26117
+
26118
+ // src/fills.ts
26119
+ import { createHmac, timingSafeEqual } from "node:crypto";
26120
+ import { createServer } from "node:http";
26121
+ var HOOK_PATH = "/chips-hermes/";
26122
+ var MAX_BODY = 4 * 1024 * 1024;
26123
+ var DROP = 0.7;
26124
+ var SEND_EVERY_MS = 2e3;
26125
+ var THRESHOLD = 0.5;
26126
+ var SMALL_WINDOW = 512e3;
26127
+ var SMALL_THRESHOLD = 0.75;
26128
+ var WHY_NOT = "Hermes can't be asked to compact from Chips.";
26129
+ var whyReadOnly = (profile) => `Hermes sets this for every agent on its ${profile} profile.`;
26130
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
26131
+ function limitOf(home2, window) {
26132
+ const text = configText(home2);
26133
+ const enabled = yamlBool(yamlValue(text, "compression", "enabled")) ?? true;
26134
+ const threshold = yamlNumber(yamlValue(text, "compression", "threshold")) ?? THRESHOLD;
26135
+ let ratio = window < SMALL_WINDOW ? Math.max(threshold, SMALL_THRESHOLD) : threshold;
26136
+ const tokens2 = yamlNumber(yamlValue(text, "compression", "threshold_tokens"));
26137
+ if (tokens2 !== void 0 && tokens2 > 0 && window > 0) ratio = Math.min(ratio, tokens2 / window);
26138
+ const at = Math.min(99, Math.max(1, Math.round(ratio * 100)));
26139
+ return enabled ? { at, choices: [at], default: at, may_turn_off: false } : { at: null, choices: [at], default: null, may_turn_off: true };
26140
+ }
26141
+ function signedBy(body, header, secret) {
26142
+ if (header === void 0 || !header.startsWith("sha256=")) return false;
26143
+ const given = Buffer.from(header.slice("sha256=".length), "hex");
26144
+ const made = createHmac("sha256", secret).update(body).digest();
26145
+ return given.length === made.length && timingSafeEqual(given, made);
26146
+ }
26147
+ function numbersOf(profile, body) {
26148
+ if (typeof body !== "object" || body === null) return void 0;
26149
+ const top = body;
26150
+ if (top["hook_event_name"] !== "post_api_request") return void 0;
26151
+ const extra = typeof top["extra"] === "object" && top["extra"] !== null ? top["extra"] : {};
26152
+ const named = typeof top["session_id"] === "string" ? top["session_id"] : "";
26153
+ const task2 = typeof extra["task_id"] === "string" && extra["task_id"] !== "" ? extra["task_id"] : void 0;
26154
+ const session = task2 ?? named;
26155
+ if (session === "") return void 0;
26156
+ const usage2 = typeof extra["usage"] === "object" && extra["usage"] !== null ? extra["usage"] : {};
26157
+ const prompt = num(usage2["prompt_tokens"]) ?? (num(usage2["input_tokens"]) === void 0 ? void 0 : (num(usage2["input_tokens"]) ?? 0) + (num(usage2["cache_read_tokens"]) ?? 0) + (num(usage2["cache_write_tokens"]) ?? 0));
26158
+ const window = num(extra["context_length"]);
26159
+ if (prompt === void 0 || prompt <= 0 || window === void 0 || window <= 0) return void 0;
26160
+ const ended = num(extra["ended_at"]);
26161
+ const stamped = typeof top["timestamp"] === "string" && !Number.isNaN(Date.parse(top["timestamp"])) ? top["timestamp"] : void 0;
26162
+ const at = ended !== void 0 ? new Date(ended * 1e3).toISOString() : stamped ?? (/* @__PURE__ */ new Date()).toISOString();
26163
+ const model = typeof extra["model"] === "string" && extra["model"] !== "" ? extra["model"].slice(0, 120) : void 0;
26164
+ const turn = typeof extra["turn_id"] === "string" && extra["turn_id"] !== "" ? extra["turn_id"] : void 0;
26165
+ return {
26166
+ profile,
26167
+ session,
26168
+ turn,
26169
+ first: num(extra["api_call_count"]) === 1,
26170
+ used: Math.round(prompt),
26171
+ window: Math.round(window),
26172
+ model,
26173
+ at: new Date(at).toISOString()
26174
+ };
26175
+ }
26176
+ var Fills = class {
26177
+ constructor(options) {
26178
+ this.options = options;
26179
+ const kept = options.kept;
26180
+ kept.readings ??= {};
26181
+ kept.lasts ??= {};
26182
+ kept.longTurns ??= {};
26183
+ for (const reading of Object.values(kept.readings)) this.lastOf.set(reading.session, reading);
26184
+ }
26185
+ options;
26186
+ server;
26187
+ /** The last reading of each session, for a compaction's drop. */
26188
+ lastOf = /* @__PURE__ */ new Map();
26189
+ turns = /* @__PURE__ */ new Map();
26190
+ timers = /* @__PURE__ */ new Map();
26191
+ sentAt = /* @__PURE__ */ new Map();
26192
+ /** The profiles whose unsigned or wrongly signed deliveries have been said once. */
26193
+ badSaid = /* @__PURE__ */ new Set();
26194
+ heard = false;
26195
+ /** Whether any reading has come since the bridge started. */
26196
+ get anyHeard() {
26197
+ return this.heard;
26198
+ }
26199
+ /** Hear Hermes on `127.0.0.1:port`: the port it took (one of its own for 0). */
26200
+ listen(port, host = "127.0.0.1") {
26201
+ const server = createServer((request, response) => this.handle(request, response));
26202
+ this.server = server;
26203
+ return new Promise((done, failed) => {
26204
+ server.once("error", failed);
26205
+ server.listen(port, host, () => {
26206
+ server.off("error", failed);
26207
+ done(server.address().port);
26208
+ });
26209
+ });
26210
+ }
26211
+ async close() {
26212
+ for (const timer of this.timers.values()) clearTimeout(timer);
26213
+ if (this.timers.size > 0) this.options.save();
26214
+ this.timers.clear();
26215
+ const server = this.server;
26216
+ this.server = void 0;
26217
+ if (server === void 0) return;
26218
+ server.closeAllConnections();
26219
+ await new Promise((done) => server.close(() => done()));
26220
+ }
26221
+ /** An agent's copy, from what is known of it, or nothing when no reading has come. */
26222
+ copyOf(agentId2) {
26223
+ const { kept } = this.options;
26224
+ const reading = kept.readings?.[agentId2];
26225
+ const profile = this.options.profileOf(agentId2);
26226
+ if (reading === void 0 || profile === void 0) return void 0;
26227
+ const last = kept.lasts?.[agentId2];
26228
+ const long = kept.longTurns?.[agentId2];
26229
+ return {
26230
+ used: reading.used,
26231
+ window: reading.window,
26232
+ ...reading.model === void 0 ? {} : { model: reading.model },
26233
+ as_of: reading.at,
26234
+ ...last === void 0 ? {} : { last },
26235
+ ...long === void 0 ? {} : { long_turn: long },
26236
+ compacts: false,
26237
+ why_not: WHY_NOT,
26238
+ limit: limitOf(profile.home, reading.window),
26239
+ editable: false,
26240
+ why_read_only: whyReadOnly(profile.profile)
26241
+ };
26242
+ }
26243
+ /** Each agent's kept copy, sent: at start, so a restart shows what was known. */
26244
+ async sendAll() {
26245
+ for (const agentId2 of Object.keys(this.options.kept.readings ?? {})) await this.send(agentId2);
26246
+ }
26247
+ /** An agent gone: nothing more is sent for it, and nothing kept. */
26248
+ forget(agentId2) {
26249
+ const { kept } = this.options;
26250
+ clearTimeout(this.timers.get(agentId2));
26251
+ this.timers.delete(agentId2);
26252
+ if (kept.readings?.[agentId2] === void 0 && kept.lasts?.[agentId2] === void 0) return;
26253
+ delete kept.readings?.[agentId2];
26254
+ delete kept.lasts?.[agentId2];
26255
+ delete kept.longTurns?.[agentId2];
26256
+ this.options.save();
26257
+ }
26258
+ /** One delivery: read whole up to its limit, checked, answered at once, then taken in. */
26259
+ handle(request, response) {
26260
+ const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
26261
+ const profile = url2.pathname.startsWith(HOOK_PATH) ? decodeURIComponent(url2.pathname.slice(HOOK_PATH.length)) : "";
26262
+ if (request.method !== "POST" || profile === "" || profile.includes("/")) {
26263
+ response.writeHead(404).end();
26264
+ return;
26265
+ }
26266
+ const chunks = [];
26267
+ let size = 0;
26268
+ let over = false;
26269
+ request.on("data", (chunk) => {
26270
+ if (over) return;
26271
+ size += chunk.length;
26272
+ if (size > MAX_BODY) {
26273
+ over = true;
26274
+ response.writeHead(413, { connection: "close" }).end();
26275
+ request.destroy();
26276
+ return;
26277
+ }
26278
+ chunks.push(chunk);
26279
+ });
26280
+ request.on("end", () => {
26281
+ if (over) return;
26282
+ const body = Buffer.concat(chunks);
26283
+ const secret = this.options.secretFor(profile);
26284
+ const header = request.headers["x-hermes-signature-256"];
26285
+ if (secret === void 0 || !signedBy(body, typeof header === "string" ? header : void 0, secret)) {
26286
+ response.writeHead(401).end();
26287
+ if (!this.badSaid.has(profile)) {
26288
+ this.badSaid.add(profile);
26289
+ this.options.log(
26290
+ `a reading from Hermes's ${profile} profile wasn't signed with Chips's secret, and was dropped: start hermes gateway again if its .env changed`
26291
+ );
26292
+ }
26293
+ return;
26294
+ }
26295
+ response.writeHead(204).end();
26296
+ let parsed;
26297
+ try {
26298
+ parsed = JSON.parse(body.toString("utf8"));
26299
+ } catch {
26300
+ return;
26301
+ }
26302
+ const numbers = numbersOf(profile, parsed);
26303
+ if (numbers !== void 0) this.take(numbers);
26304
+ });
26305
+ }
26306
+ /** A reading taken in: the agent's latest, a compaction when it fell, the turn's peak. */
26307
+ take(numbers) {
26308
+ const whose = this.options.whose(numbers.session);
26309
+ if (whose === void 0) return;
26310
+ this.heard = true;
26311
+ const { kept } = this.options;
26312
+ const { agentId: agentId2 } = whose;
26313
+ const reading = {
26314
+ used: numbers.used,
26315
+ window: numbers.window,
26316
+ ...numbers.model === void 0 ? {} : { model: numbers.model },
26317
+ at: numbers.at,
26318
+ session: numbers.session
26319
+ };
26320
+ const before = this.lastOf.get(numbers.session);
26321
+ const fell = before !== void 0 && before.model === reading.model && reading.used < before.used * DROP;
26322
+ if (fell && before !== void 0) {
26323
+ (kept.lasts ??= {})[agentId2] = { at: reading.at, from: before.used, to: reading.used, by: "itself" };
26324
+ this.options.log(`${agentId2} compacted in Hermes, ${before.used} to ${reading.used} tokens`);
26325
+ }
26326
+ let turn = this.turns.get(numbers.session);
26327
+ const fresh = numbers.turn === void 0 ? numbers.first || turn === void 0 : turn?.id !== numbers.turn;
26328
+ if (turn === void 0 || fresh) {
26329
+ turn = { id: numbers.turn, peak: 0, folds: 0 };
26330
+ this.turns.set(numbers.session, turn);
26331
+ }
26332
+ turn.peak = Math.max(turn.peak, reading.used);
26333
+ if (fell) turn.folds += 1;
26334
+ if (turn.peak > reading.window / 2) {
26335
+ (kept.longTurns ??= {})[agentId2] = { at: reading.at, peak: turn.peak, folds: Math.min(1e3, turn.folds) };
26336
+ }
26337
+ this.lastOf.set(numbers.session, reading);
26338
+ (kept.readings ??= {})[agentId2] = reading;
26339
+ this.schedule(agentId2);
26340
+ }
26341
+ /** The agent's copy sent now, or once readings slow down, and what is kept saved with it. */
26342
+ schedule(agentId2) {
26343
+ if (this.timers.has(agentId2)) return;
26344
+ const every = this.options.sendEveryMs ?? SEND_EVERY_MS;
26345
+ const wait2 = Math.max(0, (this.sentAt.get(agentId2) ?? 0) + every - Date.now());
26346
+ const timer = setTimeout(() => {
26347
+ this.timers.delete(agentId2);
26348
+ this.sentAt.set(agentId2, Date.now());
26349
+ this.options.save();
26350
+ void this.send(agentId2);
26351
+ }, wait2);
26352
+ timer.unref?.();
26353
+ this.timers.set(agentId2, timer);
26354
+ }
26355
+ async send(agentId2) {
26356
+ const copy = this.copyOf(agentId2);
26357
+ if (copy === void 0) return;
26358
+ await this.options.send(agentId2, copy).catch((error62) => {
26359
+ this.options.log(`${agentId2}'s context window not sent: ${error62 instanceof Error ? error62.message : String(error62)}`);
26360
+ });
26361
+ }
26362
+ };
25471
26363
 
25472
26364
  // src/hermes.ts
25473
26365
  var TERMINAL = /* @__PURE__ */ new Set([
@@ -25490,9 +26382,9 @@ var HermesClient = class {
25490
26382
  constructor(address, fetcher = fetch) {
25491
26383
  this.address = address;
25492
26384
  this.fetcher = fetcher;
25493
- const root = address.url.replace(/\/+$/, "");
26385
+ const root2 = address.url.replace(/\/+$/, "");
25494
26386
  const profile = address.profile;
25495
- this.base = profile === void 0 || profile === "default" ? root : `${root}/p/${encodeURIComponent(profile)}`;
26387
+ this.base = profile === void 0 || profile === "default" ? root2 : `${root2}/p/${encodeURIComponent(profile)}`;
25496
26388
  }
25497
26389
  address;
25498
26390
  fetcher;
@@ -25639,7 +26531,7 @@ function sessionIdOf(made) {
25639
26531
  return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
25640
26532
  }
25641
26533
  function eventOf(block) {
25642
- const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
26534
+ const data = block.split("\n").filter((line2) => line2.startsWith("data:")).map((line2) => line2.slice(5).replace(/^ /, "")).join("\n");
25643
26535
  if (data === "") return void 0;
25644
26536
  try {
25645
26537
  const parsed = JSON.parse(data);
@@ -25665,8 +26557,126 @@ async function failureOf(answer) {
25665
26557
  return new HermesError(message, answer.status, code);
25666
26558
  }
25667
26559
 
26560
+ // src/hooks.ts
26561
+ import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
26562
+ import { join as join7 } from "node:path";
26563
+ var HOOK_NAME = "chips";
26564
+ var SECRET_ENV = "CHIPS_HOOK_SECRET";
26565
+ function hookUrl(port, profile) {
26566
+ return `http://127.0.0.1:${port}${HOOK_PATH}${profile}`;
26567
+ }
26568
+ function linesOf2(text) {
26569
+ return text.split("\n").map((line2) => {
26570
+ const indent = line2.length - line2.trimStart().length;
26571
+ const words4 = line2.trimStart().startsWith("#") ? "" : line2.trim().replace(/\s+#.*$/, "");
26572
+ return { indent, words: words4 };
26573
+ });
26574
+ }
26575
+ function hookPortIn(home2) {
26576
+ const path = join7(home2, "config.yaml");
26577
+ if (!existsSync7(path)) return void 0;
26578
+ const escaped = HOOK_PATH.replace(/[/]/g, "\\/");
26579
+ const match = new RegExp(`^\\s*(?:-\\s*)?url:\\s*["']?http:\\/\\/127\\.0\\.0\\.1:(\\d+)${escaped}`, "m").exec(
26580
+ readFileSync7(path, "utf8")
26581
+ );
26582
+ const port = match === null ? void 0 : Number(match[1]);
26583
+ return port !== void 0 && Number.isInteger(port) && port > 0 && port < 65536 ? port : void 0;
26584
+ }
26585
+ function entryLines(indent, url2, secret) {
26586
+ return [
26587
+ `${indent}outbound:`,
26588
+ `${indent} - name: ${HOOK_NAME}`,
26589
+ `${indent} url: ${url2}`,
26590
+ `${indent} events: [post_api_request]`,
26591
+ "env" in secret ? `${indent} secret_env: ${secret.env}` : `${indent} secret: "${secret.inline}"`,
26592
+ `${indent} timeout: 5`
26593
+ ];
26594
+ }
26595
+ function secretInEnv(home2, secret) {
26596
+ const path = join7(home2, ".env");
26597
+ if (!existsSync7(path)) return false;
26598
+ if (envValue(home2, SECRET_ENV) !== void 0) return true;
26599
+ const text = readFileSync7(path, "utf8");
26600
+ const mode = statSync3(path).mode & 511;
26601
+ appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${SECRET_ENV}=${secret}
26602
+ `);
26603
+ chmodSync3(path, mode);
26604
+ return true;
26605
+ }
26606
+ function addHook(home2, port, profile, secret) {
26607
+ const path = join7(home2, "config.yaml");
26608
+ if (!existsSync7(home2)) return { state: "no_home", path };
26609
+ const url2 = hookUrl(port, profile);
26610
+ const text = existsSync7(path) ? readFileSync7(path, "utf8") : "";
26611
+ const lines2 = linesOf2(text);
26612
+ const ours2 = new RegExp(`^(?:-\\s*)?url:\\s*["']?${url2.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}["']?$`);
26613
+ if (lines2.some((line2) => ours2.test(line2.words))) return { state: "there", path };
26614
+ const where = lines2.findIndex((line2) => line2.indent === 0 && /^["']?hooks["']?\s*:/.test(line2.words));
26615
+ const byHand = (why) => {
26616
+ const inEnv2 = secretInEnv(home2, secret);
26617
+ const shown2 = entryLines(" ", url2, inEnv2 ? { env: SECRET_ENV } : { inline: secret });
26618
+ return { state: "by_hand", path, why, lines: ["hooks:", ...shown2].join("\n") };
26619
+ };
26620
+ const plain = /^(?:"[^"]+"|'[^']+'|[A-Za-z_][\w.-]*)\s*:(?:\s|$)/;
26621
+ if (lines2.some((line2) => line2.indent === 0 && line2.words !== "" && !plain.test(line2.words))) {
26622
+ return byHand("its top level isn't laid out as this bridge expects");
26623
+ }
26624
+ if (where === -1) {
26625
+ const inEnv2 = secretInEnv(home2, secret);
26626
+ const block = [
26627
+ "# Chips: how full each agent's context is, sent to the chips-hermes bridge after each model call.",
26628
+ "hooks:",
26629
+ ...entryLines(" ", url2, inEnv2 ? { env: SECRET_ENV } : { inline: secret })
26630
+ ];
26631
+ const before = text === "" ? "" : text.endsWith("\n") ? "\n" : "\n\n";
26632
+ if (text === "") writeFileSync4(path, `${block.join("\n")}
26633
+ `, { mode: 384 });
26634
+ else writeWhole(path, `${text}${before}${block.join("\n")}
26635
+ `);
26636
+ return { state: "added", path };
26637
+ }
26638
+ const head = lines2[where]?.words ?? "";
26639
+ if (head.replace(/^["']?hooks["']?\s*:/, "").trim() !== "") return byHand("its hooks: is written on one line");
26640
+ const children = [];
26641
+ for (const line2 of lines2.slice(where + 1)) {
26642
+ if (line2.words === "") continue;
26643
+ if (line2.indent === 0) break;
26644
+ children.push(line2);
26645
+ }
26646
+ const indent = children[0]?.indent ?? 2;
26647
+ const named = new RegExp(`^(?:-\\s*)?name:\\s*["']?${HOOK_NAME}["']?$`);
26648
+ if (children.some((line2) => named.test(line2.words))) {
26649
+ return byHand(`its hooks have an entry called ${HOOK_NAME} with another address`);
26650
+ }
26651
+ if (children.some((line2) => line2.indent === indent && /^["']?outbound["']?\s*:/.test(line2.words))) {
26652
+ return byHand("its hooks already have an outbound list");
26653
+ }
26654
+ if (children.some((line2) => line2.indent !== indent && line2.indent < indent) || children[0]?.words.startsWith("-")) {
26655
+ return byHand("its hooks: isn't laid out as this bridge expects");
26656
+ }
26657
+ const inEnv = secretInEnv(home2, secret);
26658
+ const raw = text.split("\n");
26659
+ raw.splice(where + 1, 0, ...entryLines(" ".repeat(indent), url2, inEnv ? { env: SECRET_ENV } : { inline: secret }));
26660
+ writeWhole(path, raw.join("\n"));
26661
+ return { state: "added", path };
26662
+ }
26663
+
25668
26664
  // src/bridge.ts
25669
- var WANTS = ["agents", "new_agents", "forks"];
26665
+ var WANTS = ["agents", "new_agents", "forks", "context_files", "context_windows", "restarts"];
26666
+ function settingsFor(active) {
26667
+ const bridge2 = () => {
26668
+ const there = active();
26669
+ if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
26670
+ return there;
26671
+ };
26672
+ return {
26673
+ context: (edit) => bridge2().onContextEdit(edit),
26674
+ contextWindows: {
26675
+ compact: async (ask2) => bridge2().onWindowCompact(ask2),
26676
+ change: async (ask2) => bridge2().onWindowChange(ask2)
26677
+ }
26678
+ };
26679
+ }
25670
26680
  function filesHere(project = false) {
25671
26681
  return `${sendingFiles({ project })} Hermes's note that this API server sends only pictures, and shows a MEDIA: tag on the runs endpoint as text, does not hold for these runs: they come from Chips, whose bridge takes each MEDIA: line out of your answer and attaches that file to your message, whatever its type, up to 100 MB.`;
25672
26682
  }
@@ -25675,10 +26685,10 @@ function instructionsFor(folder2, project = false) {
25675
26685
  const files = filesHere(project);
25676
26686
  return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
25677
26687
  }
25678
- function freeFolder(root, name2, taken) {
26688
+ function freeFolder(root2, name2, taken) {
25679
26689
  const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
25680
26690
  for (let count = 1; ; count += 1) {
25681
- const folder2 = join5(root, count === 1 ? slug : `${slug}-${count}`);
26691
+ const folder2 = join8(root2, count === 1 ? slug : `${slug}-${count}`);
25682
26692
  if (!taken.includes(folder2)) return folder2;
25683
26693
  }
25684
26694
  }
@@ -25709,6 +26719,32 @@ var Bridge = class {
25709
26719
  constructor(options) {
25710
26720
  this.options = options;
25711
26721
  this.log = options.log ?? (() => void 0);
26722
+ const { chips: chips2, hermesRoot: hermesRoot2 } = options;
26723
+ if (hermesRoot2 === void 0) return;
26724
+ this.contexts = new Contexts(
26725
+ () => this.roster(),
26726
+ {
26727
+ agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
26728
+ harness: (copy) => chips2.settings.harnessContext(copy)
26729
+ },
26730
+ (line2) => this.log(`context: ${line2}`),
26731
+ options.env ?? process.env
26732
+ );
26733
+ if (options.fills === void 0) return;
26734
+ const kept = options.state.fills ??= {};
26735
+ this.fills = new Fills({
26736
+ whose: (sessionId) => this.whoseSession(sessionId),
26737
+ profileOf: (agentId2) => {
26738
+ const profile = this.profileFor(agentId2);
26739
+ return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
26740
+ },
26741
+ secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
26742
+ send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
26743
+ kept,
26744
+ save: () => options.saveState(options.state),
26745
+ log: (line2) => this.log(`context window: ${line2}`),
26746
+ sendEveryMs: options.fillsEveryMs
26747
+ });
25712
26748
  }
25713
26749
  options;
25714
26750
  agents = [];
@@ -25723,6 +26759,20 @@ var Bridge = class {
25723
26759
  copying = Promise.resolve();
25724
26760
  log;
25725
26761
  health;
26762
+ /** The harness's agents in Chips as last listed: whose context and window copies are sent. */
26763
+ listed = [];
26764
+ /** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
26765
+ contexts;
26766
+ contextTimer;
26767
+ /** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
26768
+ fills;
26769
+ /** The port Hermes's webhook reaches the bridge on, once it listens. */
26770
+ fillsPort;
26771
+ toldNoReadings = false;
26772
+ /** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
26773
+ held = false;
26774
+ /** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
26775
+ why;
25726
26776
  /** Register the agents, start hearing what is said to them, and say how their gateways are. */
25727
26777
  async start() {
25728
26778
  const { chips: chips2 } = this.options;
@@ -25736,20 +26786,138 @@ var Bridge = class {
25736
26786
  this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
25737
26787
  chips2.on("message.new", (message) => void this.hear(message));
25738
26788
  chips2.on("agent.asked", (asked) => {
25739
- this.make(asked).catch((error62) => this.log(`a new agent went wrong: ${messageOf(error62)}`));
26789
+ this.make(asked).catch((error62) => this.log(`a new agent went wrong: ${messageOf2(error62)}`));
25740
26790
  });
25741
26791
  chips2.on("agent.fork", (fork) => {
25742
- this.copying = this.copying.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf(error62)}`));
26792
+ this.copying = this.copying.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf2(error62)}`));
25743
26793
  });
25744
26794
  chips2.on("feedback", (feedback2) => {
25745
- this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf(error62)}`));
26795
+ this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf2(error62)}`));
25746
26796
  });
26797
+ chips2.on("agent.attached", () => void this.agentsMoved());
26798
+ chips2.on("agent.released", () => void this.agentsMoved());
25747
26799
  await this.sayHowItIs();
25748
26800
  this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
25749
26801
  this.health.unref();
26802
+ await this.startSettings();
26803
+ }
26804
+ /** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
26805
+ hold(held) {
26806
+ this.held = held;
26807
+ }
26808
+ /**
26809
+ * How a restart went (Chips ADR 0094): fine as the gateways now are, or its words, which stand
26810
+ * while Hermes doesn't answer. The health watch speaks for itself again after.
26811
+ */
26812
+ async restarted(trouble) {
26813
+ this.held = false;
26814
+ this.why = trouble;
26815
+ if (trouble === void 0) return this.sayHowItIs();
26816
+ await this.options.chips.reportTrouble(trouble).catch(() => void 0);
25750
26817
  }
25751
26818
  stop() {
25752
26819
  if (this.health !== void 0) clearInterval(this.health);
26820
+ if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
26821
+ void this.fills?.close();
26822
+ }
26823
+ // ------------------------------------------------------------------ an agent's page
26824
+ /**
26825
+ * The agents' context and context windows, sent at start and kept current: the files read
26826
+ * again every so often and after each turn, a window copy on each reading Hermes sends.
26827
+ */
26828
+ async startSettings() {
26829
+ if (this.contexts === void 0) return;
26830
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26831
+ await this.contexts.refresh();
26832
+ this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
26833
+ this.contextTimer.unref();
26834
+ const fills2 = this.fills;
26835
+ const asked = this.options.fills;
26836
+ if (fills2 === void 0 || asked === void 0) return;
26837
+ const kept = this.options.state.fills ??= {};
26838
+ try {
26839
+ this.fillsPort = await fills2.listen(asked.port);
26840
+ if (kept.port !== this.fillsPort) {
26841
+ kept.port = this.fillsPort;
26842
+ this.options.saveState(this.options.state);
26843
+ }
26844
+ } catch (error62) {
26845
+ this.log(`context window: nothing can listen on port ${asked.port} (${messageOf2(error62)}), so Chips won't show how full each agent's context is`);
26846
+ }
26847
+ await fills2.sendAll();
26848
+ }
26849
+ /** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
26850
+ hookUrl(profile = "default") {
26851
+ return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
26852
+ }
26853
+ /** The harness's agents listed again, the copies of any gone forgotten, and the context sent. */
26854
+ async agentsMoved() {
26855
+ if (this.contexts === void 0) return;
26856
+ const before = this.listed;
26857
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26858
+ const now = new Set(this.active().map((agent) => agent.agentId));
26859
+ for (const gone of before) {
26860
+ if (now.has(gone.agentId)) continue;
26861
+ this.options.chips.settings.forget(gone.agentId);
26862
+ this.fills?.forget(gone.agentId);
26863
+ }
26864
+ await this.contexts.refresh();
26865
+ }
26866
+ /** The harness's agents that read anything now: the active ones listed. */
26867
+ active() {
26868
+ return this.listed.filter((agent) => agent.state === "active");
26869
+ }
26870
+ /** A profile as the context needs it: its name and its home under Hermes's root. */
26871
+ profileOfRunning(agent) {
26872
+ const root2 = this.options.hermesRoot;
26873
+ if (root2 === void 0) return void 0;
26874
+ return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
26875
+ }
26876
+ /** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
26877
+ profileFor(agentId2) {
26878
+ const own2 = this.agents.find((agent) => agent.agentId === agentId2);
26879
+ const copied = this.options.state.profiles?.[agentId2];
26880
+ const original = this.agents.find((agent) => copied !== void 0 && agent.key === copied);
26881
+ const running = own2 ?? original ?? this.agents[0];
26882
+ return running === void 0 ? void 0 : this.profileOfRunning(running);
26883
+ }
26884
+ /** Who reads what: each active agent with its profile, and the first profile, which new agents run on. */
26885
+ roster() {
26886
+ const readers = [];
26887
+ const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
26888
+ for (const agentId2 of ids2) {
26889
+ if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
26890
+ const profile = this.profileFor(agentId2);
26891
+ if (profile !== void 0) readers.push({ agentId: agentId2, profile });
26892
+ }
26893
+ const first = this.agents[0];
26894
+ return { readers, first: first === void 0 ? void 0 : this.profileOfRunning(first) };
26895
+ }
26896
+ /** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
26897
+ whoseSession(sessionId) {
26898
+ const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
26899
+ if (entry === void 0) return void 0;
26900
+ const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
26901
+ const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
26902
+ const profile = this.profileFor(agentId2);
26903
+ return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
26904
+ }
26905
+ /** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
26906
+ async onContextEdit(frame) {
26907
+ if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
26908
+ if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
26909
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26910
+ }
26911
+ return this.contexts.edit(frame);
26912
+ }
26913
+ /** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
26914
+ onWindowCompact(frame) {
26915
+ throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
26916
+ }
26917
+ /** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
26918
+ onWindowChange(frame) {
26919
+ const profile = this.profileFor(frame.agent_id);
26920
+ throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
25753
26921
  }
25754
26922
  /** The Main agent, the first registered, once started: who says the harness's first words. */
25755
26923
  get mainAgentId() {
@@ -25801,6 +26969,7 @@ var Bridge = class {
25801
26969
  this.ours.add(agent.agentId);
25802
26970
  this.names.set(agent.agentId, agent.name);
25803
26971
  this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
26972
+ void this.agentsMoved();
25804
26973
  }
25805
26974
  /**
25806
26975
  * A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
@@ -25845,6 +27014,7 @@ var Bridge = class {
25845
27014
  this.names.set(copy.agentId, copy.name);
25846
27015
  const away = copy.attached === false && !copy.spaces.includes(spaceId);
25847
27016
  this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
27017
+ void this.agentsMoved();
25848
27018
  }
25849
27019
  /** An agent's name, from Chips's list when it isn't known yet. */
25850
27020
  async nameOf(agentId2, otherwise) {
@@ -25863,11 +27033,11 @@ var Bridge = class {
25863
27033
  */
25864
27034
  folderOf(agent, agentId2, name2) {
25865
27035
  if (agent.config.folder !== void 0) return agent.config.folder;
25866
- const { folders: root, state: state2 } = this.options;
25867
- if (root === void 0) return void 0;
27036
+ const { folders: root2, state: state2 } = this.options;
27037
+ if (root2 === void 0) return void 0;
25868
27038
  const kept = state2.folders?.[agentId2];
25869
- const folder2 = kept ?? freeFolder(root, name2, Object.values(state2.folders ?? {}));
25870
- mkdirSync4(folder2, { recursive: true });
27039
+ const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
27040
+ mkdirSync5(folder2, { recursive: true });
25871
27041
  if (kept === void 0) {
25872
27042
  state2.folders = { ...state2.folders, [agentId2]: folder2 };
25873
27043
  this.options.saveState(state2);
@@ -25884,12 +27054,12 @@ var Bridge = class {
25884
27054
  const folder2 = turn.folder;
25885
27055
  if (folder2 === void 0) return void 0;
25886
27056
  try {
25887
- const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line) => this.log(line) });
27057
+ const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line2) => this.log(line2) });
25888
27058
  const facts = await project.pull();
25889
27059
  turn.project = { copy: project, name: name2 };
25890
27060
  return facts;
25891
27061
  } catch (error62) {
25892
- this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf(error62)}`);
27062
+ this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf2(error62)}`);
25893
27063
  return void 0;
25894
27064
  }
25895
27065
  }
@@ -25908,18 +27078,21 @@ var Bridge = class {
25908
27078
  const note = heldNote(pushed, name2);
25909
27079
  return note === void 0 ? {} : { note };
25910
27080
  } catch (error62) {
25911
- this.log(`${name2}'s project couldn't be sent: ${messageOf(error62)}`);
25912
- const note = heldNote({ state: "failed", why: messageOf(error62) }, name2);
27081
+ this.log(`${name2}'s project couldn't be sent: ${messageOf2(error62)}`);
27082
+ const note = heldNote({ state: "failed", why: messageOf2(error62) }, name2);
25913
27083
  return note === void 0 ? {} : { note };
25914
27084
  }
25915
27085
  }
25916
- /** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083). */
27086
+ /** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083); nothing while a restart runs. */
25917
27087
  async sayHowItIs() {
27088
+ if (this.held) return;
25918
27089
  for (const agent of this.agents) agent.up = await agent.client.healthy();
27090
+ if (this.held) return;
25919
27091
  const down = this.agents.filter((agent) => !agent.up);
27092
+ if (down.length === 0) this.why = void 0;
25920
27093
  const chips2 = this.options.chips;
25921
- const said = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(`Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
25922
- await said.catch(() => void 0);
27094
+ const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
27095
+ await said2.catch(() => void 0);
25923
27096
  }
25924
27097
  // ------------------------------------------------------------------ what is said to it
25925
27098
  /** A message for some of its agents: each takes a turn in its room, one after another there. */
@@ -25952,15 +27125,15 @@ var Bridge = class {
25952
27125
  this.lookedAt.delete(old);
25953
27126
  }
25954
27127
  const context = await this.options.chips.roomContext(spaceId, { limit: 200 }).catch(() => void 0);
25955
- const line = context?.lines.find((each) => each.message_id === messageId);
27128
+ const line2 = context?.lines.find((each) => each.message_id === messageId);
25956
27129
  const message = {
25957
27130
  type: "message.new",
25958
27131
  space_id: spaceId,
25959
27132
  // An id of its own, so nothing of the turn is taken for the reply's.
25960
27133
  message_id: messageIdOf(feedback2.id),
25961
- text: lookAgain(feedback2.from.display_name, line?.text, note),
27134
+ text: lookAgain(feedback2.from.display_name, line2?.text, note),
25962
27135
  from: feedback2.from,
25963
- ...line === void 0 ? {} : { for: [line.from.id] }
27136
+ ...line2 === void 0 ? {} : { for: [line2.from.id] }
25964
27137
  };
25965
27138
  await this.hear(message, { goal: lookingAgain(note), sayWho: false });
25966
27139
  }
@@ -26014,9 +27187,10 @@ var Bridge = class {
26014
27187
  try {
26015
27188
  await this.turn(room, asked);
26016
27189
  } catch (error62) {
26017
- this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf(error62)}`);
27190
+ this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf2(error62)}`);
26018
27191
  } finally {
26019
27192
  room.busy = false;
27193
+ void this.contexts?.refresh();
26020
27194
  void this.pump(room);
26021
27195
  }
26022
27196
  }
@@ -26051,7 +27225,7 @@ var Bridge = class {
26051
27225
  try {
26052
27226
  session = await this.sessionFor(room);
26053
27227
  } catch (error62) {
26054
- await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf(error62)}`, true);
27228
+ await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf2(error62)}`, true);
26055
27229
  return;
26056
27230
  }
26057
27231
  const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
@@ -26080,7 +27254,7 @@ var Bridge = class {
26080
27254
  try {
26081
27255
  turn.hermesRun = await agent.client.startRun({ text, sessionId: session, model: agent.config.model, key, instructions: instructionsFor(turn.folder, turn.project !== void 0) });
26082
27256
  } catch (error62) {
26083
- await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf(error62)}`, true);
27257
+ await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf2(error62)}`, true);
26084
27258
  return void 0;
26085
27259
  }
26086
27260
  if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
@@ -26088,7 +27262,7 @@ var Bridge = class {
26088
27262
  const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
26089
27263
  return last ?? await this.endOf(turn);
26090
27264
  } catch (error62) {
26091
- this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf(error62)}`);
27265
+ this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf2(error62)}`);
26092
27266
  return await this.endOf(turn);
26093
27267
  }
26094
27268
  }
@@ -26101,7 +27275,7 @@ var Bridge = class {
26101
27275
  if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
26102
27276
  return { ...state2, event: `run.${status}` };
26103
27277
  }
26104
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
27278
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
26105
27279
  }
26106
27280
  await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
26107
27281
  return void 0;
@@ -26133,7 +27307,7 @@ var Bridge = class {
26133
27307
  await turn.agent.client.steer(turn.hermesRun, text);
26134
27308
  return;
26135
27309
  } catch (error62) {
26136
- this.log(`Hermes didn't take a steer mid-run (${messageOf(error62)}); saying it next`);
27310
+ this.log(`Hermes didn't take a steer mid-run (${messageOf2(error62)}); saying it next`);
26137
27311
  }
26138
27312
  }
26139
27313
  turn.next.push(text);
@@ -26169,10 +27343,10 @@ var Bridge = class {
26169
27343
  }
26170
27344
  sayThought(turn, text, phase) {
26171
27345
  const now = Date.now();
26172
- const line = lastLine(text);
26173
- if (line === "" || now - turn.saidAt < QUIET_MS) return;
27346
+ const line2 = lastLine(text);
27347
+ if (line2 === "" || now - turn.saidAt < QUIET_MS) return;
26174
27348
  turn.saidAt = now;
26175
- void turn.run.status({ phase, verbPhrase: line.slice(0, 200) }).catch(() => void 0);
27349
+ void turn.run.status({ phase, verbPhrase: line2.slice(0, 200) }).catch(() => void 0);
26176
27350
  }
26177
27351
  async started(turn, event) {
26178
27352
  const sub = event.event === "subagent.start";
@@ -26182,11 +27356,11 @@ var Bridge = class {
26182
27356
  const stepId = callId ?? `${tool}-${turn.counted}`;
26183
27357
  const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
26184
27358
  turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
26185
- const said = sub ? event["goal"] : event["preview"];
27359
+ const said2 = sub ? event["goal"] : event["preview"];
26186
27360
  await turn.run.step({
26187
27361
  stepId,
26188
27362
  status: "started",
26189
- title: `${tool}${typeof said === "string" && said !== "" ? ` \xB7 ${said}` : ""}`.slice(0, 300),
27363
+ title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
26190
27364
  tool,
26191
27365
  ...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
26192
27366
  }).catch(() => void 0);
@@ -26241,7 +27415,7 @@ var Bridge = class {
26241
27415
  ${command}
26242
27416
  \`\`\`` } }
26243
27417
  });
26244
- const stopped2 = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
27418
+ const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
26245
27419
  const decision = await Promise.race([asked, stopped2]);
26246
27420
  const pending = turn.approvals.get(requestId ?? approvalId);
26247
27421
  if (decision === void 0 || pending === void 0 || pending.answered) return;
@@ -26249,10 +27423,10 @@ ${command}
26249
27423
  if (decision.allowed) turn.allowed.push(approvalId);
26250
27424
  const generation = event["execution_generation"];
26251
27425
  await turn.agent.client.approve(turn.hermesRun, {
26252
- choice: choiceOf(decision, choices),
27426
+ choice: choiceOf(decision, choices, command !== void 0),
26253
27427
  ...requestId === void 0 ? {} : { request_id: requestId },
26254
27428
  ...typeof generation === "number" ? { execution_generation: generation } : {}
26255
- }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf(error62)}`));
27429
+ }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf2(error62)}`));
26256
27430
  }
26257
27431
  /** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
26258
27432
  async settled(turn, event) {
@@ -26297,10 +27471,27 @@ ${command}
26297
27471
  if (reply !== "" || post.uploadIds.length > 0) {
26298
27472
  await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
26299
27473
  }
26300
- const said = post.summary;
26301
- const summary = said === "" ? "Done." : said.length <= 2e3 ? said : `${said.slice(0, 1990)}\u2026`;
27474
+ const said2 = post.summary;
27475
+ const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
26302
27476
  await run2.complete({ summary });
26303
27477
  this.log(`${agent.config.name} answered in ${run2.spaceId}`);
27478
+ this.missingReadings();
27479
+ }
27480
+ /**
27481
+ * A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
27482
+ * likely started before Chips's webhook was in its config. Said once, a little after, since a
27483
+ * reading comes a moment after its model call.
27484
+ */
27485
+ missingReadings() {
27486
+ if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
27487
+ this.toldNoReadings = true;
27488
+ setTimeout(() => {
27489
+ if (this.fills?.anyHeard === false) {
27490
+ this.log(
27491
+ "context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
27492
+ );
27493
+ }
27494
+ }, 1e4).unref();
26304
27495
  }
26305
27496
  /** A run that failed, was stopped or was cut short in Hermes. */
26306
27497
  async ended(turn, last) {
@@ -26349,8 +27540,9 @@ ${command}
26349
27540
  if (retryable) void this.sayHowItIs();
26350
27541
  }
26351
27542
  };
26352
- function choiceOf(decision, choices) {
27543
+ function choiceOf(decision, choices, command = false) {
26353
27544
  if (!decision.allowed) return "deny";
27545
+ if (command) return "once";
26354
27546
  if (decision.scope === "always" && choices.includes("always")) return "always";
26355
27547
  if (decision.scope !== "once" && choices.includes("session")) return "session";
26356
27548
  return "once";
@@ -26359,7 +27551,7 @@ function idOf(event) {
26359
27551
  const id2 = event["request_id"] ?? event["prompt_id"];
26360
27552
  return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
26361
27553
  }
26362
- function messageOf(error62) {
27554
+ function messageOf2(error62) {
26363
27555
  if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
26364
27556
  return error62 instanceof Error ? error62.message : String(error62);
26365
27557
  }
@@ -26367,7 +27559,7 @@ function forkName(original) {
26367
27559
  return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
26368
27560
  }
26369
27561
  function goalOf(message, name2) {
26370
- const first = message.text.split("\n").find((line) => line.trim() !== "")?.trim() ?? "";
27562
+ const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
26371
27563
  const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
26372
27564
  const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
26373
27565
  return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
@@ -26375,17 +27567,17 @@ function goalOf(message, name2) {
26375
27567
  var REPLY_SHOWN = 1500;
26376
27568
  var GOAL_SHOWN = 120;
26377
27569
  function lookAgain(who, reply, note) {
26378
- const said = reply?.trim() ?? "";
27570
+ const said2 = reply?.trim() ?? "";
26379
27571
  return [
26380
27572
  `${who} says your reply in this space wasn't right.`,
26381
- ...said === "" ? [] : [`Your reply:
26382
- \xAB${cut(said, REPLY_SHOWN)}\xBB`],
27573
+ ...said2 === "" ? [] : [`Your reply:
27574
+ \xAB${cut(said2, REPLY_SHOWN)}\xBB`],
26383
27575
  note === void 0 || note === "" ? "They didn't say what. Read your reply again and find what was wrong." : `What wasn't right: \xAB${note}\xBB`,
26384
27576
  "Look at it again and answer properly in its place. If it is not clear what was wrong, ask them in one short line. Say plainly what you changed."
26385
27577
  ].join("\n\n");
26386
27578
  }
26387
27579
  function lookingAgain(note) {
26388
- const first = note?.split("\n").find((line) => line.trim() !== "")?.trim();
27580
+ const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
26389
27581
  return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
26390
27582
  }
26391
27583
  function cut(text, most) {
@@ -26401,31 +27593,492 @@ function resultWords(preview2) {
26401
27593
  }
26402
27594
  if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
26403
27595
  const { output: output2, error: error62 } = result;
26404
- const said = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
26405
- return typeof said === "string" ? said.trim() : "";
27596
+ const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
27597
+ return typeof said2 === "string" ? said2.trim() : "";
26406
27598
  }
26407
27599
  function lastLine(text) {
26408
- const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line) => line.trim()).filter((line) => line !== "");
27600
+ const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line2) => line2.trim()).filter((line2) => line2 !== "");
26409
27601
  return lines2.at(-1) ?? "";
26410
27602
  }
26411
27603
  function lowerFirst(text) {
26412
27604
  return text.length === 0 ? text : `${text[0]?.toLowerCase() ?? ""}${text.slice(1)}`;
26413
27605
  }
26414
27606
 
27607
+ // src/gateway.ts
27608
+ import { spawn as spawn2 } from "node:child_process";
27609
+ import { createHash as createHash3 } from "node:crypto";
27610
+ import { accessSync as accessSync2, appendFileSync as appendFileSync2, closeSync as closeSync2, constants as constants2, existsSync as existsSync8, openSync as openSync2, readFileSync as readFileSync8, readSync, statSync as statSync4 } from "node:fs";
27611
+ import { homedir as homedir5 } from "node:os";
27612
+ import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join9, resolve as resolve3 } from "node:path";
27613
+ var TIMING = {
27614
+ stopMs: 15e3,
27615
+ killMs: 5e3,
27616
+ earlyMs: 1e4,
27617
+ upMs: 9e4,
27618
+ serviceMs: 15e4,
27619
+ pollMs: 500
27620
+ };
27621
+ var EXPAT = "/opt/homebrew/opt/expat/lib/libexpat.1.dylib";
27622
+ var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
27623
+ function gatewaysOf(agents) {
27624
+ return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
27625
+ }
27626
+ function isLocal(url2) {
27627
+ try {
27628
+ return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
27629
+ } catch {
27630
+ return false;
27631
+ }
27632
+ }
27633
+ function portOf(url2) {
27634
+ const parsed = new URL(url2);
27635
+ return Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
27636
+ }
27637
+ function line(words4) {
27638
+ const one = words4.replace(/\s+/g, " ").trim();
27639
+ return one.length <= 200 ? one : `${one.slice(0, 199)}\u2026`;
27640
+ }
27641
+ function whyFrom(output2, code) {
27642
+ const lines2 = output2.split("\n").map((each) => each.trim()).filter((each) => each !== "");
27643
+ const why = lines2.findLast((each) => /^[A-Za-z_][\w.]*(Error|Exception)\b/.test(each)) ?? lines2.at(-1);
27644
+ if (why === void 0) return code === null || code === void 0 ? "it was stopped" : `it exited ${code}`;
27645
+ return why.length <= 200 ? why : why.replace(/\/(?:[^\s/(),:'"]+\/)+([^\s/(),:'"]+)/g, "$1");
27646
+ }
27647
+ function gatewayRunArgs(command) {
27648
+ const words4 = command.trim().split(/\s+/);
27649
+ const at = words4.findIndex((word) => basename4(word) === "hermes");
27650
+ if (at === -1) return void 0;
27651
+ const args = words4.slice(at + 1);
27652
+ const plain = [];
27653
+ for (let next = 0; next < args.length; next += 1) {
27654
+ const word = args[next] ?? "";
27655
+ if (word === "-p" || word === "--profile") next += 1;
27656
+ else if (!word.startsWith("-")) plain.push(word);
27657
+ }
27658
+ return plain[0] === "gateway" && plain[1] === "run" ? args : void 0;
27659
+ }
27660
+ function serviceOf(system, root2, env = {}) {
27661
+ const home2 = resolve3(root2);
27662
+ const native = resolve3(system.home, ".hermes");
27663
+ const suffix = home2 === native ? "" : dirname5(home2) === join9(native, "profiles") ? basename4(home2) : createHash3("sha256").update(home2).digest("hex").slice(0, 8);
27664
+ if (system.platform === "darwin") {
27665
+ const path = join9(system.home, "Library", "LaunchAgents", `ai.hermes.gateway${suffix ? `-${suffix}` : ""}.plist`);
27666
+ return system.exists(path) ? { kind: "launchd", path } : void 0;
27667
+ }
27668
+ if (system.platform === "linux") {
27669
+ const unit = `hermes-gateway${suffix ? `-${suffix}` : ""}.service`;
27670
+ const user = join9(env["XDG_CONFIG_HOME"] || join9(system.home, ".config"), "systemd", "user", unit);
27671
+ if (system.exists(user)) return { kind: "systemd", path: user };
27672
+ const machine = join9("/etc/systemd/system", unit);
27673
+ if (system.exists(machine)) return { kind: "systemd", path: machine, system: true };
27674
+ }
27675
+ return void 0;
27676
+ }
27677
+ function expand(text, env) {
27678
+ let out = "";
27679
+ let at = 0;
27680
+ while (at < text.length) {
27681
+ const char = text[at] ?? "";
27682
+ if (char === "\\" && at + 1 < text.length) {
27683
+ out += text[at + 1];
27684
+ at += 2;
27685
+ } else if (char !== "$") {
27686
+ out += char;
27687
+ at += 1;
27688
+ } else if (text[at + 1] === "{") {
27689
+ let depth = 1;
27690
+ let end2 = at + 2;
27691
+ for (; end2 < text.length; end2 += 1) {
27692
+ if (text[end2] === "{") depth += 1;
27693
+ else if (text[end2] === "}" && --depth === 0) break;
27694
+ }
27695
+ const inner = /^([A-Za-z_][A-Za-z0-9_]*)(?:(:?)-([\s\S]*))?$/.exec(text.slice(at + 2, end2));
27696
+ if (inner !== null) {
27697
+ const value = env[inner[1] ?? ""];
27698
+ const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
27699
+ out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
27700
+ }
27701
+ at = end2 + 1;
27702
+ } else {
27703
+ const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
27704
+ out += name2 === void 0 ? "$" : env[name2] ?? "";
27705
+ at += 1 + (name2?.length ?? 0);
27706
+ }
27707
+ }
27708
+ return out;
27709
+ }
27710
+ function wrapperOf(text, env) {
27711
+ for (const each of text.split("\n")) {
27712
+ const found = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*="(?:[^"\\]|\\.)*"\s+)*)exec\s+"([^"]+)"/.exec(each);
27713
+ if (found === null) continue;
27714
+ const vars = {};
27715
+ for (const set2 of (found[1] ?? "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:[^"\\]|\\.)*)"/g)) {
27716
+ vars[set2[1] ?? ""] = expand(set2[2] ?? "", env);
27717
+ }
27718
+ return { entry: expand(found[2] ?? "", env), vars };
27719
+ }
27720
+ return void 0;
27721
+ }
27722
+ var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
27723
+ var messageOf3 = (error62) => error62 instanceof Error ? error62.message : String(error62);
27724
+ function gatewayEnv(env) {
27725
+ return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
27726
+ }
27727
+ var Gateways = class {
27728
+ constructor(options) {
27729
+ this.options = options;
27730
+ }
27731
+ options;
27732
+ timing = TIMING;
27733
+ /** Each gateway restarted, one after another: what went wrong, in a line, or nothing. */
27734
+ async restart() {
27735
+ const troubles = [];
27736
+ for (const gateway of this.options.gateways) {
27737
+ let trouble;
27738
+ try {
27739
+ trouble = await this.restartOne(gateway);
27740
+ } catch (error62) {
27741
+ trouble = `Restart failed: ${messageOf3(error62)}`;
27742
+ }
27743
+ if (trouble !== void 0) troubles.push(trouble);
27744
+ }
27745
+ return troubles.length === 0 ? void 0 : line(troubles.join(" "));
27746
+ }
27747
+ /** Each local gateway that doesn't answer, started: the bridge is starting. */
27748
+ async startIfDown() {
27749
+ const { system, log: log2 } = this.options;
27750
+ for (const gateway of this.options.gateways) {
27751
+ if (!isLocal(gateway.url)) continue;
27752
+ const port = portOf(gateway.url);
27753
+ try {
27754
+ if (await system.healthy(gateway.url)) continue;
27755
+ let trouble;
27756
+ if (this.serviceFor(gateway) !== void 0) {
27757
+ log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
27758
+ trouble = await this.byService(gateway, "start", void 0);
27759
+ } else {
27760
+ const pid = await system.listener(port);
27761
+ if (pid !== void 0) {
27762
+ log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
27763
+ continue;
27764
+ }
27765
+ log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
27766
+ trouble = await this.start(gateway, ["gateway", "run"], void 0);
27767
+ }
27768
+ if (trouble !== void 0) log2(trouble);
27769
+ } catch (error62) {
27770
+ log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf3(error62)}`);
27771
+ }
27772
+ }
27773
+ }
27774
+ /**
27775
+ * The service of Hermes's root, for the Main agent's gateway: a gateway at another address is
27776
+ * another profile's own, which `hermes gateway status` of the root doesn't speak for.
27777
+ */
27778
+ serviceFor(gateway) {
27779
+ if (gateway !== this.options.gateways[0]) return void 0;
27780
+ return serviceOf(this.options.system, this.options.root, this.options.env);
27781
+ }
27782
+ async restartOne(gateway) {
27783
+ const { system, log: log2 } = this.options;
27784
+ if (!isLocal(gateway.url)) {
27785
+ return `Hermes's gateway at ${new URL(gateway.url).host} isn't on this machine, so it can't be restarted from here.`;
27786
+ }
27787
+ const port = portOf(gateway.url);
27788
+ const pid = await system.listener(port);
27789
+ if (this.serviceFor(gateway) !== void 0) {
27790
+ log2("restarting Hermes's service: hermes gateway restart");
27791
+ return this.byService(gateway, "restart", pid);
27792
+ }
27793
+ if (pid === void 0) {
27794
+ if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
27795
+ log2(`nothing listens on port ${port}: starting hermes gateway run`);
27796
+ return this.start(gateway, ["gateway", "run"], void 0);
27797
+ }
27798
+ const info = await system.describe(pid);
27799
+ if (info === void 0) return this.start(gateway, ["gateway", "run"], void 0);
27800
+ const args = this.ours(info);
27801
+ if (args === void 0) {
27802
+ log2(`pid ${pid} on port ${port} isn't this user's hermes gateway run, so it was left alone: ${info.command.slice(0, 160)}`);
27803
+ return `Port ${port} is held by something other than hermes gateway run (pid ${pid}), so nothing was restarted.`;
27804
+ }
27805
+ if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
27806
+ log2(`stopping Hermes's gateway, pid ${pid}`);
27807
+ if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
27808
+ return this.start(gateway, args, info.cwd);
27809
+ }
27810
+ /** A `hermes gateway run` of this user's: the words to start it again with. */
27811
+ ours(info) {
27812
+ return info.uid === this.options.system.uid ? gatewayRunArgs(info.command) : void 0;
27813
+ }
27814
+ /** SIGTERM, then SIGKILL once it has had a while, and only while it is still that gateway. */
27815
+ async stop(pid) {
27816
+ const { system, log: log2 } = this.options;
27817
+ system.signal(pid, "SIGTERM");
27818
+ if (await this.gone(pid, this.timing.stopMs)) return true;
27819
+ const still = await system.describe(pid);
27820
+ if (still === void 0) return true;
27821
+ if (this.ours(still) === void 0) return false;
27822
+ log2(`Hermes's gateway, pid ${pid}, is still running ${this.timing.stopMs / 1e3} s after SIGTERM: SIGKILL`);
27823
+ system.signal(pid, "SIGKILL");
27824
+ return this.gone(pid, this.timing.killMs);
27825
+ }
27826
+ async gone(pid, withinMs) {
27827
+ const { system } = this.options;
27828
+ const by = system.now() + withinMs;
27829
+ while (system.alive(pid)) {
27830
+ if (system.now() >= by) return false;
27831
+ await system.sleep(this.timing.pollMs);
27832
+ }
27833
+ return true;
27834
+ }
27835
+ /** Hermes's own `hermes gateway restart|start`, then the gateway answering, from a new process. */
27836
+ async byService(gateway, verb, before) {
27837
+ const { system, env } = this.options;
27838
+ const hermes = system.which("hermes", env);
27839
+ if (hermes === void 0) return NO_HERMES;
27840
+ const scope = this.serviceFor(gateway)?.system === true ? ["--system"] : [];
27841
+ const ran = await system.run(hermes, ["gateway", verb, ...scope], gatewayEnv(env), this.timing.serviceMs);
27842
+ if (ran.code !== 0) return line(`hermes gateway ${verb} failed: ${whyFrom(ran.output, ran.code)}`);
27843
+ const by = system.now() + this.timing.serviceMs;
27844
+ for (; ; ) {
27845
+ const old = before !== void 0 && system.alive(before);
27846
+ if (!old && await system.healthy(gateway.url)) return void 0;
27847
+ if (system.now() >= by) {
27848
+ if (await system.healthy(gateway.url)) return void 0;
27849
+ return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
27850
+ }
27851
+ await system.sleep(this.timing.pollMs);
27852
+ }
27853
+ }
27854
+ /**
27855
+ * `hermes <args>` (`gateway run`), on its own, in `cwd` (else the home), then through Homebrew's
27856
+ * expat when it died of macOS's.
27857
+ */
27858
+ async start(gateway, args, cwd) {
27859
+ const { system, env, log: log2 } = this.options;
27860
+ const port = portOf(gateway.url);
27861
+ const hermes = system.which("hermes", env);
27862
+ if (hermes === void 0) return NO_HERMES;
27863
+ const folder2 = cwd ?? system.home;
27864
+ const plain = gatewayEnv(env);
27865
+ let outcome = await this.launch(gateway, hermes, args, plain, folder2, `hermes ${args.join(" ")}`);
27866
+ if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
27867
+ const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
27868
+ if (wrapper !== void 0) {
27869
+ const expat = dirname5(EXPAT);
27870
+ log2(
27871
+ `hermes gateway run stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Starting ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
27872
+ );
27873
+ const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
27874
+ outcome = await this.launch(
27875
+ gateway,
27876
+ wrapper.entry,
27877
+ args,
27878
+ { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
27879
+ folder2,
27880
+ `${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
27881
+ );
27882
+ }
27883
+ }
27884
+ if (outcome.state === "up") {
27885
+ log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
27886
+ return void 0;
27887
+ }
27888
+ if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
27889
+ return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
27890
+ }
27891
+ /** A program started, and watched until it answers, ends, or has had its time. */
27892
+ async launch(gateway, command, args, env, cwd, what) {
27893
+ const { system, logFile } = this.options;
27894
+ system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
27895
+ const from = system.size(logFile);
27896
+ const startedAt = system.now();
27897
+ const child = system.launch({ command, args, env, cwd, log: logFile });
27898
+ let ended;
27899
+ void child.exited.then(({ code }) => {
27900
+ ended = { code, at: system.now() };
27901
+ });
27902
+ for (; ; ) {
27903
+ await Promise.resolve();
27904
+ if (ended !== void 0) {
27905
+ return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
27906
+ }
27907
+ if (await system.healthy(gateway.url)) return { state: "up", pid: child.pid };
27908
+ if (system.now() - startedAt >= this.timing.upMs) return { state: "silent" };
27909
+ await system.sleep(this.timing.pollMs);
27910
+ }
27911
+ }
27912
+ };
27913
+ function restarter(deps) {
27914
+ let running;
27915
+ return (ask2) => {
27916
+ if (running !== void 0) {
27917
+ deps.log(`restart asked by ${ask2.askedBy} while one runs: it is the same restart`);
27918
+ return running;
27919
+ }
27920
+ running = (async () => {
27921
+ deps.log(`restart asked by ${ask2.askedBy}: restarting Hermes's gateway`);
27922
+ deps.hold(true);
27923
+ let trouble;
27924
+ try {
27925
+ trouble = await deps.restart();
27926
+ } catch (error62) {
27927
+ trouble = line(`Restart failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
27928
+ }
27929
+ deps.log(trouble === void 0 ? "restarted, and Hermes answers" : `restart: ${trouble}`);
27930
+ try {
27931
+ await deps.say(trouble);
27932
+ } catch (error62) {
27933
+ deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
27934
+ } finally {
27935
+ deps.hold(false);
27936
+ }
27937
+ })().finally(() => {
27938
+ running = void 0;
27939
+ });
27940
+ return running;
27941
+ };
27942
+ }
27943
+ function gatewayLog(configFolder, platform = process.platform) {
27944
+ return platform === "darwin" ? join9(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join9(configFolder, "gateway.log");
27945
+ }
27946
+ function runToEnd(command, args, options) {
27947
+ return new Promise((done) => {
27948
+ const child = spawn2(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
27949
+ let output2 = "";
27950
+ const keep = (data) => {
27951
+ output2 = (output2 + String(data)).slice(-8e3);
27952
+ };
27953
+ child.stdout.on("data", keep);
27954
+ child.stderr.on("data", keep);
27955
+ const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
27956
+ child.on("error", (error62) => {
27957
+ clearTimeout(limit);
27958
+ done({ code: null, output: error62.message });
27959
+ });
27960
+ child.on("close", (code) => {
27961
+ clearTimeout(limit);
27962
+ done({ code, output: output2 });
27963
+ });
27964
+ });
27965
+ }
27966
+ function thisMachine() {
27967
+ return {
27968
+ platform: process.platform,
27969
+ uid: process.getuid?.() ?? -1,
27970
+ home: homedir5(),
27971
+ exists: existsSync8,
27972
+ read(path) {
27973
+ try {
27974
+ return readFileSync8(path, "utf8");
27975
+ } catch {
27976
+ return void 0;
27977
+ }
27978
+ },
27979
+ which(command, env) {
27980
+ for (const folder2 of (env["PATH"] ?? "").split(delimiter2)) {
27981
+ if (folder2 === "") continue;
27982
+ const path = join9(folder2, command);
27983
+ try {
27984
+ accessSync2(path, constants2.X_OK);
27985
+ return path;
27986
+ } catch {
27987
+ }
27988
+ }
27989
+ return void 0;
27990
+ },
27991
+ async listener(port) {
27992
+ const { output: output2 } = await runToEnd("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], { timeoutMs: 5e3 });
27993
+ const pid = Number(/^p(\d+)$/m.exec(output2)?.[1]);
27994
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
27995
+ },
27996
+ async describe(pid) {
27997
+ const { code, output: output2 } = await runToEnd("ps", ["-ww", "-o", "uid=", "-o", "command=", "-p", String(pid)], { timeoutMs: 5e3 });
27998
+ const found = /^\s*(\d+)\s+(.*)$/m.exec(output2);
27999
+ if (code !== 0 || found === null) return void 0;
28000
+ return { uid: Number(found[1]), command: found[2] ?? "", cwd: processFolder(pid) };
28001
+ },
28002
+ alive(pid) {
28003
+ try {
28004
+ process.kill(pid, 0);
28005
+ return true;
28006
+ } catch (error62) {
28007
+ return error62.code === "EPERM";
28008
+ }
28009
+ },
28010
+ signal(pid, signal) {
28011
+ try {
28012
+ process.kill(pid, signal);
28013
+ } catch {
28014
+ }
28015
+ },
28016
+ run: (command, args, env, timeoutMs) => runToEnd(command, args, { env, timeoutMs }),
28017
+ launch({ command, args, env, cwd, log: log2 }) {
28018
+ const out = openSync2(log2, "a");
28019
+ try {
28020
+ const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
28021
+ const exited = new Promise((done) => {
28022
+ child.once("exit", (code, signal) => done({ code, signal }));
28023
+ child.once("error", (error62) => {
28024
+ appendFileSync2(log2, `${error62.message}
28025
+ `);
28026
+ done({ code: null, signal: null });
28027
+ });
28028
+ });
28029
+ child.unref();
28030
+ return { pid: child.pid, exited };
28031
+ } finally {
28032
+ closeSync2(out);
28033
+ }
28034
+ },
28035
+ note(log2, words4) {
28036
+ appendFileSync2(log2, `${words4}
28037
+ `);
28038
+ },
28039
+ size(log2) {
28040
+ try {
28041
+ return statSync4(log2).size;
28042
+ } catch {
28043
+ return 0;
28044
+ }
28045
+ },
28046
+ since(log2, from) {
28047
+ try {
28048
+ const to = statSync4(log2).size;
28049
+ const start = Math.max(from, to - 64e3);
28050
+ const buffer = Buffer.alloc(Math.max(0, to - start));
28051
+ const fd = openSync2(log2, "r");
28052
+ try {
28053
+ readSync(fd, buffer, 0, buffer.length, start);
28054
+ } finally {
28055
+ closeSync2(fd);
28056
+ }
28057
+ return buffer.toString("utf8");
28058
+ } catch {
28059
+ return "";
28060
+ }
28061
+ },
28062
+ healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
28063
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
28064
+ now: () => Date.now()
28065
+ };
28066
+ }
28067
+
26415
28068
  // src/keep.ts
26416
- import { homedir as homedir4 } from "node:os";
26417
- import { basename as basename3, join as join6, resolve as resolve2 } from "node:path";
28069
+ import { homedir as homedir6 } from "node:os";
28070
+ import { basename as basename5, join as join10, resolve as resolve4 } from "node:path";
26418
28071
  import { fileURLToPath as fileURLToPath2 } from "node:url";
26419
28072
  var flags = process.argv.slice(2);
26420
28073
  var words3 = flags.filter((word) => !word.startsWith("--"));
26421
- var whole = (word) => resolve2(word.startsWith("~/") ? join6(homedir4(), word.slice(2)) : word);
28074
+ var whole = (word) => resolve4(word.startsWith("~/") ? join10(homedir6(), word.slice(2)) : word);
26422
28075
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
26423
28076
  var self = fileURLToPath2(import.meta.url);
26424
28077
  var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
26425
- var again = process.env["npm_lifecycle_event"] === "keep" ? "npm run keep --" : basename3(process.argv[1] ?? "chips");
28078
+ var again = process.env["npm_lifecycle_event"] === "keep" ? "npm run keep --" : basename5(process.argv[1] ?? "chips");
26426
28079
  var stopLine = process.env["CHIPS_STOP_COMMAND"] || [again, ...words3, "--stop"].join(" ");
26427
- var end = (line, code) => {
26428
- (code === 0 ? process.stdout : process.stderr).write(`${line}
28080
+ var end = (line2, code) => {
28081
+ (code === 0 ? process.stdout : process.stderr).write(`${line2}
26429
28082
  `);
26430
28083
  process.exit(code);
26431
28084
  };
@@ -26464,9 +28117,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
26464
28117
  }
26465
28118
 
26466
28119
  // src/setup.ts
26467
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
26468
- import { homedir as homedir5 } from "node:os";
26469
- import { resolve as resolve3 } from "node:path";
28120
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
28121
+ import { homedir as homedir7 } from "node:os";
28122
+ import { join as join11, resolve as resolve5 } from "node:path";
26470
28123
  var SetupError = class extends Error {
26471
28124
  constructor(message, words4 = {}) {
26472
28125
  super(message);
@@ -26475,37 +28128,70 @@ var SetupError = class extends Error {
26475
28128
  words;
26476
28129
  name = "SetupError";
26477
28130
  };
26478
- var shown = (path) => path.startsWith(`${homedir5()}/`) ? `~${path.slice(homedir5().length)}` : path;
26479
- var portOf = (url2) => {
28131
+ var shown = (path) => path.startsWith(`${homedir7()}/`) ? `~${path.slice(homedir7().length)}` : path;
28132
+ var portOf2 = (url2) => {
26480
28133
  const parsed = new URL(url2);
26481
28134
  return parsed.port || (parsed.protocol === "https:" ? "443" : "80");
26482
28135
  };
26483
- function checkedWords(config3) {
28136
+ function checkedWords(config3, restart2 = false) {
26484
28137
  const urls = new Set(config3.agents.map((agent) => agent.address.url));
26485
28138
  const [url2] = urls;
26486
- return urls.size === 1 && url2 !== void 0 ? `Its API server answers on port ${portOf(url2)}` : `Its API server answers for each of ${config3.agents.length} agents`;
28139
+ const found = urls.size === 1 && url2 !== void 0 ? `Its API server answers on port ${portOf2(url2)}` : `Its API server answers for each of ${config3.agents.length} agents`;
28140
+ return restart2 ? `${found}. ${RESTART}` : found;
28141
+ }
28142
+ var RESTART = "Start hermes gateway again once, so it tells Chips how full each agent's context is.";
28143
+ function readyHooks(config3, options) {
28144
+ const lines2 = [];
28145
+ let restart2 = false;
28146
+ const done = /* @__PURE__ */ new Set();
28147
+ for (const agent of config3.agents) {
28148
+ const profile = profileName(agent.address.profile);
28149
+ if (done.has(profile)) continue;
28150
+ done.add(profile);
28151
+ const home2 = profileHome(options.root, agent.address.profile);
28152
+ let outcome;
28153
+ try {
28154
+ outcome = addHook(home2, options.port, profile, options.secret);
28155
+ } catch (error62) {
28156
+ lines2.push(`Chips's webhook wasn't added to ${shown(join11(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
28157
+ continue;
28158
+ }
28159
+ if (outcome.state === "added") {
28160
+ lines2.push(`Added Chips's webhook to ${shown(outcome.path)}, so Hermes tells Chips how full each agent's context is.`);
28161
+ restart2 ||= options.gatewayUp;
28162
+ } else if (outcome.state === "by_hand") {
28163
+ lines2.push(
28164
+ `${shown(outcome.path)} was left as it is: ${outcome.why}. For Chips to show how full each agent's context is, add these lines to it, then start hermes gateway again:
28165
+ ${outcome.lines}`
28166
+ );
28167
+ } else if (outcome.state === "no_home") {
28168
+ lines2.push(`Hermes has no ${profile} profile at ${shown(home2)}, so no webhook was added for it.`);
28169
+ }
28170
+ }
28171
+ if (restart2) lines2.push(RESTART);
28172
+ return { lines: lines2, restart: restart2 };
26487
28173
  }
26488
28174
  var STILL_GOOD = "The link is still good: run the same command again once that's done.";
26489
28175
  var EXAMPLE = '{ "agents": [{ "name": "Hermes", "profile": "default", "keyEnv": "API_SERVER_KEY" }] }';
26490
28176
  var PLAIN = { agents: [{ name: "Hermes", profile: "default", keyEnv: "API_SERVER_KEY" }] };
26491
28177
  function hermesEnvPath(env = process.env) {
26492
- return resolve3(env["HERMES_HOME"] ?? resolve3(homedir5(), ".hermes"), ".env");
28178
+ return resolve5(env["HERMES_HOME"] ?? resolve5(homedir7(), ".hermes"), ".env");
26493
28179
  }
26494
28180
  function fromHermes(name2, env) {
26495
28181
  const path = hermesEnvPath(env);
26496
- if (!existsSync5(path)) return void 0;
26497
- for (const line of readFileSync5(path, "utf8").split("\n")) {
26498
- const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
28182
+ if (!existsSync9(path)) return void 0;
28183
+ for (const line2 of readFileSync9(path, "utf8").split("\n")) {
28184
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line2);
26499
28185
  if (match?.[1] === name2) return match[2]?.replace(/^(["'])(.*)\1$/, "$2");
26500
28186
  }
26501
28187
  return void 0;
26502
28188
  }
26503
- function folderOf(said) {
26504
- if (typeof said !== "string" || said === "") return void 0;
26505
- return resolve3(said === "~" || said.startsWith("~/") ? homedir5() + said.slice(1) : said);
28189
+ function folderOf(said2) {
28190
+ if (typeof said2 !== "string" || said2 === "") return void 0;
28191
+ return resolve5(said2 === "~" || said2.startsWith("~/") ? homedir7() + said2.slice(1) : said2);
26506
28192
  }
26507
28193
  function readConfig(path, env = process.env) {
26508
- const raw = existsSync5(path) ? JSON.parse(readFileSync5(path, "utf8")) : PLAIN;
28194
+ const raw = existsSync9(path) ? JSON.parse(readFileSync9(path, "utf8")) : PLAIN;
26509
28195
  const url2 = typeof raw.url === "string" ? raw.url : "http://127.0.0.1:8642";
26510
28196
  const folder2 = folderOf(raw.folder);
26511
28197
  if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
@@ -26540,10 +28226,12 @@ function readConfig(path, env = process.env) {
26540
28226
  ...own2 === void 0 ? {} : { folder: own2 }
26541
28227
  };
26542
28228
  });
28229
+ const hookPort = raw.hookPort;
26543
28230
  return {
26544
28231
  ...typeof raw.name === "string" ? { name: raw.name } : {},
26545
28232
  agents,
26546
- folders: folderOf(raw.folders) ?? resolve3(homedir5(), "chips-hermes", "agents")
28233
+ folders: folderOf(raw.folders) ?? resolve5(homedir7(), "chips-hermes", "agents"),
28234
+ ...typeof hookPort === "number" && Number.isInteger(hookPort) && hookPort > 0 && hookPort < 65536 ? { hookPort } : {}
26547
28235
  };
26548
28236
  }
26549
28237
  async function checkHermes(config3, clientFor = (agent) => new HermesClient(agent.address)) {
@@ -26553,7 +28241,7 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
26553
28241
  throw new SetupError(
26554
28242
  `Hermes isn't answering for ${agent.name} at ${agent.address.url}: start its API server (API_SERVER_ENABLED=true, then hermes gateway). ${STILL_GOOD}`,
26555
28243
  {
26556
- detail: `Nothing answers on port ${portOf(agent.address.url)}`,
28244
+ detail: `Nothing answers on port ${portOf2(agent.address.url)}`,
26557
28245
  says: "Hermes's API server is off.",
26558
28246
  doing: `Add this line to ${shown(hermesEnvPath())}, then start Hermes with hermes gateway.`,
26559
28247
  copy: "API_SERVER_ENABLED=true"
@@ -26574,19 +28262,46 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
26574
28262
  }
26575
28263
 
26576
28264
  // src/main.ts
26577
- var home = (path) => path.startsWith("~/") ? join7(homedir6(), path.slice(2)) : path;
28265
+ var home = (path) => path.startsWith("~/") ? join12(homedir8(), path.slice(2)) : path;
26578
28266
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
26579
28267
  var LABEL = "chat.chips.hermes";
26580
28268
  stopIfAsked(LABEL);
26581
- var configPath = resolve4(home(words3[0] ?? "~/.chips-hermes/config.json"));
28269
+ var configPath = resolve6(home(words3[0] ?? "~/.chips-hermes/config.json"));
28270
+ var folder = dirname6(configPath);
28271
+ var statePath = join12(folder, "sessions.json");
28272
+ var state = existsSync10(statePath) ? JSON.parse(readFileSync10(statePath, "utf8")) : { sessions: {} };
28273
+ var saveState = (next) => {
28274
+ mkdirSync6(folder, { recursive: true, mode: 448 });
28275
+ writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
28276
+ `, { mode: 384 });
28277
+ };
28278
+ var log = (line2) => process.stdout.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line2}
28279
+ `);
28280
+ var freePort = () => new Promise((done, failed) => {
28281
+ const server = createServer2();
28282
+ server.once("error", failed);
28283
+ server.listen(0, "127.0.0.1", () => {
28284
+ const address = server.address();
28285
+ server.close(() => done(typeof address === "object" && address !== null ? address.port : 0));
28286
+ });
28287
+ });
26582
28288
  var config2;
28289
+ var root = hermesRoot();
28290
+ var fills = state.fills ??= {};
26583
28291
  try {
26584
28292
  config2 = readConfig(configPath);
26585
28293
  if (link !== void 0) {
26586
28294
  reportStep("checking", { detail: "Looking for its API server" });
26587
28295
  await checkHermes(config2);
26588
- reportStep("checked", { detail: checkedWords(config2) });
26589
28296
  }
28297
+ fills.secret ??= randomBytes2(32).toString("hex");
28298
+ fills.port = config2.hookPort ?? fills.port ?? hookPortIn(profileHome(root, config2.agents[0]?.address.profile)) ?? await freePort();
28299
+ saveState(state);
28300
+ const first = config2.agents[0];
28301
+ const gatewayUp = link !== void 0 || first !== void 0 && await new HermesClient(first.address).healthy();
28302
+ const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp });
28303
+ for (const line2 of hooks.lines) log(line2);
28304
+ if (link !== void 0) reportStep("checked", { detail: checkedWords(config2, hooks.restart) });
26590
28305
  } catch (error62) {
26591
28306
  if (!(error62 instanceof SetupError)) throw error62;
26592
28307
  reportStep("failed", error62.words);
@@ -26595,10 +28310,15 @@ try {
26595
28310
  process.exit(1);
26596
28311
  }
26597
28312
  var name = config2.name ?? "Hermes";
26598
- var folder = dirname4(configPath);
26599
- var statePath = join7(folder, "sessions.json");
26600
- var log = (line) => process.stdout.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
26601
- `);
28313
+ var gateways2 = new Gateways({
28314
+ gateways: gatewaysOf(config2.agents),
28315
+ root,
28316
+ env: process.env,
28317
+ system: thisMachine(),
28318
+ log: (line2) => log(`gateway: ${line2}`),
28319
+ logFile: gatewayLog(folder)
28320
+ });
28321
+ if (link === void 0) await gateways2.startIfDown();
26602
28322
  var refused2 = (error62) => {
26603
28323
  if (!(error62 instanceof JoinError)) throw error62;
26604
28324
  process.stderr.write(`${error62.message}
@@ -26614,25 +28334,37 @@ var joined = await join3({
26614
28334
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
26615
28335
  }).catch(refused2);
26616
28336
  keepIfAsked(LABEL, joined.url, ["HERMES_HOME", ...config2.agents.map((agent) => agent.keyEnv ?? "API_SERVER_KEY")], log);
26617
- var state = existsSync6(statePath) ? JSON.parse(readFileSync6(statePath, "utf8")) : { sessions: {} };
28337
+ var bridge;
28338
+ var restart = restarter({
28339
+ restart: () => gateways2.restart(),
28340
+ hold: (held) => bridge?.hold(held),
28341
+ say: async (trouble) => {
28342
+ if (bridge !== void 0) return bridge.restarted(trouble);
28343
+ await (trouble === void 0 ? chips.reportOk() : chips.reportTrouble(trouble));
28344
+ },
28345
+ log: (line2) => log(`gateway: ${line2}`)
28346
+ });
26618
28347
  var chips = await connect({
26619
28348
  url: joined.url,
26620
28349
  token: joined.token,
26621
28350
  client: { name: "chips-hermes", version: "1.0.0" },
26622
- store: fileStore(join7(folder, "chips-state.json"))
28351
+ store: fileStore(join12(folder, "chips-state.json")),
28352
+ onRestart: restart,
28353
+ settings: settingsFor(() => bridge)
26623
28354
  });
26624
28355
  var git = await checkGit();
26625
28356
  if (!git.ok) log(`rooms' projects are off: ${git.why}`);
26626
- var bridge = new Bridge({
28357
+ bridge = new Bridge({
26627
28358
  folders: config2.folders,
26628
28359
  projects: git.ok,
26629
28360
  chips,
26630
28361
  agents: config2.agents,
26631
28362
  home: joined.spaceId,
26632
28363
  state,
26633
- saveState: (next) => writeFileSync4(statePath, `${JSON.stringify(next, null, 2)}
26634
- `, { mode: 384 }),
26635
- log
28364
+ saveState,
28365
+ log,
28366
+ hermesRoot: root,
28367
+ fills: { port: fills.port, secret: fills.secret }
26636
28368
  });
26637
28369
  await bridge.start();
26638
28370
  log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through Hermes`);
@@ -26646,7 +28378,7 @@ await helloOnce(joined, {
26646
28378
  }).catch((error62) => log(`hello not said: ${String(error62)}`));
26647
28379
  for (const signal of ["SIGINT", "SIGTERM"]) {
26648
28380
  process.on(signal, () => {
26649
- bridge.stop();
28381
+ bridge?.stop();
26650
28382
  chips.close();
26651
28383
  process.exit(0);
26652
28384
  });