@chipschat/hermes 1.0.3 → 1.0.5

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 existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
13
+ import { createServer as createServer2 } from "node:net";
14
+ import { homedir as homedir7 } from "node:os";
15
+ import { dirname as dirname5, join as join11, resolve as resolve5 } 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)?)??$/;
@@ -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 },
@@ -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, resolve6) {
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, resolve6);
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, resolve6) : 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 ?? (resolve6 ? 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;
@@ -20392,7 +20394,7 @@ var serverFacts = external_exports.object({
20392
20394
  // node_modules/@chipschat/protocol/dist/src/room-files.js
20393
20395
  var ROOM_FILES_IN_CONTEXT = 20;
20394
20396
  var ROOM_FILES_CEILING = 200;
20395
- var ROOM_FILES_HEADING = "Files in this room:";
20397
+ var ROOM_FILES_HEADING = "Files in this space:";
20396
20398
  var roomFile = external_exports.object({
20397
20399
  name: external_exports.string().min(1).max(255).describe("The file's name, which its versions share."),
20398
20400
  version: external_exports.int().positive().describe("Which version this is: 1 for the first, counting each change of its bytes."),
@@ -20483,7 +20485,7 @@ var MONTHS = [
20483
20485
 
20484
20486
  // node_modules/@chipschat/protocol/dist/src/context.js
20485
20487
  var CONTEXT_CEILING = { lines: 200, chars: 32e3 };
20486
- var CONTEXT_HEADING = "Said in this room since you last spoke:";
20488
+ var CONTEXT_HEADING = "Said in this space since you last spoke:";
20487
20489
  var contextBudget = external_exports.object({
20488
20490
  lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 20 when absent. Chips hands over 200 at most, and none when this is 0."),
20489
20491
  chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 4,000 when absent. Chips hands over 32,000 at most.")
@@ -20551,24 +20553,24 @@ function renderLine(line) {
20551
20553
  case "said":
20552
20554
  return `${withRole(name2, line.from)}: ${wordsOf(line)}`;
20553
20555
  case "joined":
20554
- return `(${name2} joined the room)`;
20556
+ return `(${name2} joined the space)`;
20555
20557
  case "left":
20556
- return `(${name2} left the room)`;
20558
+ return `(${name2} left the space)`;
20557
20559
  case "renamed":
20558
20560
  if (line.title !== void 0)
20559
- return `(${name2} renamed the room "${line.title}")`;
20561
+ return `(${name2} renamed the space "${line.title}")`;
20560
20562
  return `(${line.previous ?? name2} is now called ${line.name ?? name2})`;
20561
20563
  case "archived":
20562
- return `(${name2} archived the room)`;
20564
+ return `(${name2} archived the space)`;
20563
20565
  default:
20564
- return `(${name2} unarchived the room)`;
20566
+ return `(${name2} unarchived the space)`;
20565
20567
  }
20566
20568
  }
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 ?? "";
@@ -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.");
@@ -21179,7 +21184,7 @@ function namesInWords(names) {
21179
21184
  function renderProjectNote(facts, options) {
21180
21185
  const files = `${facts.files} file${facts.files === 1 ? "" : "s"}`;
21181
21186
  const lines2 = [
21182
- `The room's project is a git repository. Your copy is ${facts.folder} (${files}), ${facts.trouble === void 0 ? "brought up to date just now" : "not brought up to date"}.`
21187
+ `The space's project is a git repository. Your copy is ${facts.folder} (${files}), ${facts.trouble === void 0 ? "brought up to date just now" : "not brought up to date"}.`
21183
21188
  ];
21184
21189
  if (facts.changed.length > 0) {
21185
21190
  const by = facts.by.length > 0 ? `, by ${namesInWords(facts.by)}` : "";
@@ -21189,7 +21194,7 @@ function renderProjectNote(facts, options) {
21189
21194
  lines2.push(`It couldn't be: ${facts.trouble}`);
21190
21195
  if (facts.clashes.length > 0) {
21191
21196
  const whose = facts.clashedWith.length > 0 ? `${namesInWords(facts.clashedWith)}'s` : "another change";
21192
- lines2.push(`Your last change to ${namesInWords(facts.clashes)} clashed with ${whose}, which reached the room first. Both sides are in those files now: yours between "<<<<<<< yours" and "=======", theirs from there to the ">>>>>>>" line that names them. Settle each, keeping what both meant, and take the marks out. Nothing of yours is sent until they're gone.`);
21197
+ lines2.push(`Your last change to ${namesInWords(facts.clashes)} clashed with ${whose}, which reached the space first. Both sides are in those files now: yours between "<<<<<<< yours" and "=======", theirs from there to the ">>>>>>>" line that names them. Settle each, keeping what both meant, and take the marks out. Nothing of yours is sent until they're gone.`);
21193
21198
  }
21194
21199
  for (const key of facts.keys) {
21195
21200
  lines2.push(`${key.path} holds what looks like ${key.what}, so your last change wasn't sent: take it out and read it from outside the project.`);
@@ -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",
@@ -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((resolve6, reject) => {
22850
+ waiting.set(key, { event, resolve: resolve6, 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((resolve6) => setTimeout(resolve6, 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,7 +22949,7 @@ 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((line) => line !== "");
22865
22953
  var unique = (names) => [...new Set(names)];
22866
22954
  function subjectOf(words4) {
22867
22955
  const first = words4.split("\n").find((line) => line.trim() !== "")?.trim() ?? "";
@@ -22870,11 +22958,11 @@ function subjectOf(words4) {
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((line) => line.startsWith("remote: ")).map((line) => line.slice("remote: ".length).trim()).filter((line) => line !== "" && !/^error: hook declined/.test(line));
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) {
@@ -22904,28 +22992,28 @@ function heldNote(pushed, name2) {
22904
22992
  return void 0;
22905
22993
  case "clash": {
22906
22994
  const whose = pushed.with.length === 0 ? "another change" : `${namesInWords(pushed.with)}'s change`;
22907
- return `(Not in the room's project yet: ${namesInWords(pushed.files)} clashed with ${whose}. ${name2} is asked to settle it next turn.)`;
22995
+ return `(Not in the space's project yet: ${namesInWords(pushed.files)} clashed with ${whose}. ${name2} is asked to settle it next turn.)`;
22908
22996
  }
22909
22997
  case "keys": {
22910
22998
  const [first] = pushed.keys;
22911
22999
  const held = pushed.keys.length === 1 && first !== void 0 ? `${first.path} looks like it holds ${first.what.replace(/, by its name$/, "")}` : `${namesInWords(pushed.keys.map((key) => key.path))} look like they hold keys`;
22912
- return `(Not in the room's project: ${held}, so nothing was sent.)`;
23000
+ return `(Not in the space's project: ${held}, so nothing was sent.)`;
22913
23001
  }
22914
23002
  case "refused":
22915
- return `(Not in the room's project: it was turned back: ${stopped(pushed.why)})`;
23003
+ return `(Not in the space's project: it was turned back: ${stopped(pushed.why)})`;
22916
23004
  case "failed":
22917
- return `(Not in the room's project: it couldn't be sent: ${stopped(pushed.why)})`;
23005
+ return `(Not in the space's project: it couldn't be sent: ${stopped(pushed.why)})`;
22918
23006
  }
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((resolve6) => {
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 resolve6({ 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
+ resolve6(enough ? { ok: true, version: mine.join(".") } : { ok: false, why: `${stdout.trim()} is older than ${PROJECT_GIT_VERSION}` });
22929
23017
  });
22930
23018
  });
22931
23019
  }
@@ -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}`)]);
@@ -23232,7 +23320,7 @@ ${sent.err}`;
23232
23320
  const clashes = lines((await this.git(["diff", "--name-only", "--diff-filter=U"])).out);
23233
23321
  if (clashes.length === 0)
23234
23322
  return merged.ok ? {} : { trouble: remoteWords(merged.err) };
23235
- const label2 = theirs.length === 1 ? theirs[0] ?? "the room" : "the room";
23323
+ const label2 = theirs.length === 1 ? theirs[0] ?? "the space" : "the space";
23236
23324
  for (const path of clashes)
23237
23325
  await this.label(path, onto, label2);
23238
23326
  return { clashes, clashedWith: 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((resolve6) => {
23463
+ execFile(options.git ?? "git", [...args], { cwd: this.copy, env, maxBuffer: 64 * 1024 * 1024 }, (error62, stdout, stderr) => resolve6({ 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((resolve6) => {
23487
+ this.settle = resolve6;
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((resolve6) => setTimeout(resolve6, 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((resolve6) => {
24415
+ this.waiters.add({ match, resolve: resolve6 });
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((resolve6, 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, resolve6, 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, resolve6, 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(resolve6);
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((resolve6) => {
24488
24576
  const timer = setTimeout(() => {
24489
24577
  this.pongs.delete(ts);
24490
- resolve5();
24578
+ resolve6();
24491
24579
  }, 2e3);
24492
24580
  this.pongs.set(ts, () => {
24493
24581
  clearTimeout(timer);
24494
24582
  this.pongs.delete(ts);
24495
- resolve5();
24583
+ resolve6();
24496
24584
  });
24497
24585
  socket.send(JSON.stringify({ frame: "ping", ts }));
24498
24586
  });
@@ -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((resolve6, reject) => {
25048
+ const timer = setTimeout(resolve6, ms);
24961
25049
  signal?.addEventListener("abort", () => {
24962
25050
  clearTimeout(timer);
24963
25051
  reject(new JoinError("stopped", "Stopped before a person decided."));
@@ -25073,8 +25161,8 @@ import { homedir as homedir2 } from "node:os";
25073
25161
  import { basename as basename2, isAbsolute, resolve, sep } from "node:path";
25074
25162
  import { fileURLToPath } from "node:url";
25075
25163
  function sendingFiles(options = {}) {
25076
- const which = options.project === true ? "A file that isn't in the room's project" : "A file";
25077
- return `${which} reaches this room only when you send it: put MEDIA:<path> on a line of its own in your answer, one line for each file, from your own folder. A path, or a link to this machine, reaches nobody: the people and agents here cannot open your folder.`;
25164
+ const which = options.project === true ? "A file that isn't in the space's project" : "A file";
25165
+ return `${which} reaches this space only when you send it: put MEDIA:<path> on a line of its own in your answer, one line for each file, from your own folder. A path, or a link to this machine, reaches nobody: the people and agents here cannot open your folder.`;
25078
25166
  }
25079
25167
  var SENDING_FILES = sendingFiles();
25080
25168
  var MOST_FILES = 20;
@@ -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,810 @@ 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 line of readFileSync5(path, "utf8").split("\n")) {
25600
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
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(line) {
25617
+ const indent = line.length - line.trimStart().length;
25618
+ let quote;
25619
+ for (let at = indent; at < line.length; at += 1) {
25620
+ const char = line[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(line[at - 1] ?? ""))) {
25626
+ return { indent, text: line.slice(indent, at).trimEnd() };
25627
+ }
25628
+ }
25629
+ return { indent, text: line.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: line } = uncommented(lines2[at] ?? "");
25652
+ if (indent !== 0 || !line.startsWith(`${section}:`)) continue;
25653
+ const rest = line.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
+ if (pid === void 0) return void 0;
25726
+ try {
25727
+ if (process.platform === "linux") return readlinkSync(`/proc/${pid}/cwd`);
25728
+ const out = execFileSync3("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
25729
+ encoding: "utf8",
25730
+ timeout: 3e3,
25731
+ stdio: ["ignore", "pipe", "ignore"]
25732
+ });
25733
+ const line = out.split("\n").find((each) => each.startsWith("n"));
25734
+ return line === void 0 ? void 0 : line.slice(1);
25735
+ } catch {
25736
+ return void 0;
25737
+ }
25738
+ }
25739
+
25740
+ // src/context.ts
25741
+ var FILES_SHOWN = 8;
25742
+ var FILE_BYTES = 65536;
25743
+ var READ_NOTE = "Hermes reads these when a conversation starts. A space already talking picks up a change after it next compacts.";
25744
+ var ENTRY_DELIMITER = "\n\xA7\n";
25745
+ var MEMORY_LIMIT = 2200;
25746
+ var USER_LIMIT = 1375;
25747
+ var PROJECT_FILES = [
25748
+ ".hermes.md",
25749
+ "HERMES.md",
25750
+ "AGENTS.override.md",
25751
+ "AGENTS.md",
25752
+ "agents.md",
25753
+ "CLAUDE.md",
25754
+ "claude.md",
25755
+ ".cursorrules"
25756
+ ];
25757
+ var ROWS = {
25758
+ soul: { title: "Who it is", role: "identity" },
25759
+ project: { title: "Project instructions", role: "instructions" },
25760
+ user: { title: "About you", role: "about_you" },
25761
+ memory: { title: "Memory", role: "memory" }
25762
+ };
25763
+ var ORDER = ["soul", "project", "user", "memory"];
25764
+ function readText(path) {
25765
+ try {
25766
+ return readFileSync6(path, "utf8").replace(/^/, "");
25767
+ } catch {
25768
+ return "";
25769
+ }
25770
+ }
25771
+ function hashOf(text) {
25772
+ return createHash2("sha256").update(text, "utf8").digest("hex");
25773
+ }
25774
+ function length(text) {
25775
+ let count = 0;
25776
+ for (const _ of text) count += 1;
25777
+ return count;
25778
+ }
25779
+ var said = (count) => count.toLocaleString("en-US");
25780
+ function entriesOf(text) {
25781
+ return [...new Set(text.split(ENTRY_DELIMITER).map((each) => each.trim()).filter((each) => each !== ""))];
25782
+ }
25783
+ function memoryProblem(text, limit, what) {
25784
+ const total = length(entriesOf(text).join(ENTRY_DELIMITER));
25785
+ if (total > limit) {
25786
+ return `That is ${said(total)} characters, and Hermes keeps ${what} to ${said(limit)}. Take out ${said(total - limit)}.`;
25787
+ }
25788
+ return void 0;
25789
+ }
25790
+ function memorySettings(home2) {
25791
+ const text = configText(home2);
25792
+ const value = (key) => yamlValue(text, "memory", key);
25793
+ return {
25794
+ ...yamlBool(value("memory_enabled")) === false ? {} : { memory: yamlNumber(value("memory_char_limit")) ?? MEMORY_LIMIT },
25795
+ ...yamlBool(value("user_profile_enabled")) === false ? {} : { user: yamlNumber(value("user_char_limit")) ?? USER_LIMIT }
25796
+ };
25797
+ }
25798
+ function sourcesOf(profile, env = process.env) {
25799
+ const { name: name2, home: home2 } = profile;
25800
+ const found = [];
25801
+ const soul = join6(home2, "SOUL.md");
25802
+ if (existsSync6(soul)) found.push({ path: soul, kind: "soul", file: "SOUL.md", profile: name2 });
25803
+ const folder2 = workFolder(home2, env);
25804
+ const project = folder2 === void 0 ? void 0 : PROJECT_FILES.find((file3) => readText(join6(folder2, file3)).trim() !== "");
25805
+ if (folder2 !== void 0 && project !== void 0) {
25806
+ found.push({ path: join6(folder2, project), kind: "project", file: project, profile: name2 });
25807
+ }
25808
+ const memory = memorySettings(home2);
25809
+ if (memory.user !== void 0) {
25810
+ found.push({ path: join6(home2, "memories", "USER.md"), kind: "user", file: "USER.md", profile: name2, limit: memory.user });
25811
+ }
25812
+ if (memory.memory !== void 0) {
25813
+ found.push({ path: join6(home2, "memories", "MEMORY.md"), kind: "memory", file: "MEMORY.md", profile: name2, limit: memory.memory });
25814
+ }
25815
+ return found;
25816
+ }
25817
+ function aboutOf(source2) {
25818
+ if (source2.limit === void 0) return void 0;
25819
+ const what = source2.kind === "user" ? "What it knows of you" : "Its notes";
25820
+ return `${what}, up to ${said(source2.limit)} characters. A line with only \xA7 starts the next entry.`;
25821
+ }
25822
+ function fileOf(source2, name2) {
25823
+ const text = readText(source2.path);
25824
+ const about = aboutOf(source2);
25825
+ return {
25826
+ tooLarge: Buffer.byteLength(text, "utf8") > FILE_BYTES,
25827
+ file: {
25828
+ name: name2,
25829
+ title: ROWS[source2.kind].title,
25830
+ role: ROWS[source2.kind].role,
25831
+ ...about === void 0 ? {} : { about },
25832
+ text,
25833
+ hash: hashOf(text),
25834
+ editable: true
25835
+ }
25836
+ };
25837
+ }
25838
+ var byOrder = (a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) || a.profile.localeCompare(b.profile);
25839
+ var Picture = class {
25840
+ /** Each file by its path, and the agents that read it. */
25841
+ readers = /* @__PURE__ */ new Map();
25842
+ /** The shared files, each with its id and the name the set shows. */
25843
+ shared = /* @__PURE__ */ new Map();
25844
+ /** The agents and the files each reads, in page order. */
25845
+ agents = /* @__PURE__ */ new Map();
25846
+ constructor(readers, first, env = process.env) {
25847
+ const byProfile = /* @__PURE__ */ new Map();
25848
+ const sourcesFor = (profile) => {
25849
+ let sources = byProfile.get(profile.home);
25850
+ if (sources === void 0) {
25851
+ sources = sourcesOf(profile, env);
25852
+ byProfile.set(profile.home, sources);
25853
+ }
25854
+ return sources;
25855
+ };
25856
+ if (first !== void 0) for (const source2 of sourcesFor(first)) this.readers.set(source2.path, { source: source2, agents: [] });
25857
+ for (const reader of readers) {
25858
+ const sources = sourcesFor(reader.profile);
25859
+ this.agents.set(reader.agentId, [...sources].sort(byOrder));
25860
+ for (const source2 of sources) {
25861
+ const entry = this.readers.get(source2.path) ?? { source: source2, agents: [] };
25862
+ if (!entry.agents.includes(reader.agentId)) entry.agents.push(reader.agentId);
25863
+ this.readers.set(source2.path, entry);
25864
+ }
25865
+ }
25866
+ const taken = /* @__PURE__ */ new Set();
25867
+ const ids2 = /* @__PURE__ */ new Set();
25868
+ 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));
25869
+ for (const { source: source2 } of sharing) {
25870
+ const name2 = taken.has(source2.file) ? `${source2.file} (${source2.profile})` : source2.file;
25871
+ taken.add(name2);
25872
+ let id2 = `${source2.profile}-${source2.kind}`.slice(0, 60);
25873
+ for (let count = 2; ids2.has(id2); count += 1) id2 = `${source2.profile}-${source2.kind}-${count}`.slice(0, 64);
25874
+ ids2.add(id2);
25875
+ this.shared.set(source2.path, { id: id2, name: name2 });
25876
+ }
25877
+ }
25878
+ /** The harness's own set, as `PUT /v1/harnesses/:id/context` takes it. */
25879
+ harnessCopy(first) {
25880
+ const shown2 = [];
25881
+ const notShown = [];
25882
+ for (const [path, { id: id2, name: name2 }] of this.shared) {
25883
+ const entry = this.readers.get(path);
25884
+ if (entry === void 0) continue;
25885
+ const { source: source2, agents } = entry;
25886
+ const { file: file3, tooLarge } = fileOf(source2, name2);
25887
+ if (tooLarge || shown2.length >= FILES_SHOWN) {
25888
+ notShown.push({ name: name2, title: ROWS[source2.kind].title, reason: tooLarge ? "too_large" : "count" });
25889
+ continue;
25890
+ }
25891
+ shown2.push({
25892
+ ...file3,
25893
+ id: id2,
25894
+ used_by: agents.length,
25895
+ // A new agent runs on the first profile: it reads that profile's files and its project file.
25896
+ ...source2.profile === first?.name ? { for_new_agents: true } : {}
25897
+ });
25898
+ }
25899
+ return {
25900
+ shared: shown2,
25901
+ ...notShown.length === 0 ? {} : { not_shown: notShown }
25902
+ };
25903
+ }
25904
+ /** One agent's files, as `PUT /v1/agents/:id/context` takes them: its own, and the shared it reads. */
25905
+ agentCopy(agentId2) {
25906
+ const sources = this.agents.get(agentId2);
25907
+ if (sources === void 0) return void 0;
25908
+ const files = [];
25909
+ const notShown = [];
25910
+ for (const source2 of sources) {
25911
+ const shared = this.shared.get(source2.path);
25912
+ if (files.length >= FILES_SHOWN) {
25913
+ notShown.push({ name: shared?.name ?? source2.file, title: ROWS[source2.kind].title, reason: "count" });
25914
+ continue;
25915
+ }
25916
+ if (shared !== void 0) {
25917
+ files.push({
25918
+ name: shared.name,
25919
+ title: ROWS[source2.kind].title,
25920
+ role: ROWS[source2.kind].role,
25921
+ editable: true,
25922
+ shared: shared.id,
25923
+ replaceable: false
25924
+ });
25925
+ continue;
25926
+ }
25927
+ const { file: file3, tooLarge } = fileOf(source2, source2.file);
25928
+ if (tooLarge) notShown.push({ name: source2.file, title: ROWS[source2.kind].title, reason: "too_large" });
25929
+ else files.push(file3);
25930
+ }
25931
+ return {
25932
+ files,
25933
+ ...notShown.length === 0 ? {} : { not_shown: notShown },
25934
+ read_note: READ_NOTE
25935
+ };
25936
+ }
25937
+ /** The file an edit names: an agent's own, or one of the harness's set; with whether it is shared. */
25938
+ find(agentId2, name2) {
25939
+ if (agentId2 === void 0) {
25940
+ for (const [path, { name: shown2 }] of this.shared) {
25941
+ const source2 = this.readers.get(path)?.source;
25942
+ if (shown2 === name2 && source2 !== void 0) return { source: source2, shared: true };
25943
+ }
25944
+ return void 0;
25945
+ }
25946
+ for (const source2 of this.agents.get(agentId2) ?? []) {
25947
+ const shared = this.shared.get(source2.path);
25948
+ if ((shared?.name ?? source2.file) === name2) return { source: source2, shared: shared !== void 0 };
25949
+ }
25950
+ return void 0;
25951
+ }
25952
+ };
25953
+ var LOCK_WAIT_MS = 1e4;
25954
+ var LOCKER = [
25955
+ "use Fcntl qw(:flock O_RDWR O_CREAT O_NOFOLLOW);",
25956
+ 'sysopen(my $fh, $ARGV[0], O_RDWR|O_CREAT|O_NOFOLLOW, 0600) or die "open: $!\\n";',
25957
+ "chmod 0600, $fh;",
25958
+ 'flock($fh, LOCK_EX) or die "flock: $!\\n";',
25959
+ '$| = 1; print "locked\\n";',
25960
+ "while (<STDIN>) {}",
25961
+ "flock($fh, LOCK_UN);"
25962
+ ].join(" ");
25963
+ async function withLock(lockPath, work, waitMs = LOCK_WAIT_MS) {
25964
+ const child = spawn("perl", ["-e", LOCKER, lockPath], { stdio: ["pipe", "pipe", "pipe"] });
25965
+ const exited = new Promise((done) => {
25966
+ child.on("close", () => done());
25967
+ child.on("error", () => done());
25968
+ });
25969
+ try {
25970
+ await new Promise((locked, failed) => {
25971
+ const timer = setTimeout(() => failed(new Error("Hermes is writing its memory right now. Try again in a moment.")), waitMs);
25972
+ let out = "";
25973
+ child.stdout.on("data", (chunk) => {
25974
+ out += chunk.toString("utf8");
25975
+ if (out.includes("locked\n")) {
25976
+ clearTimeout(timer);
25977
+ locked();
25978
+ }
25979
+ });
25980
+ child.on("error", () => {
25981
+ clearTimeout(timer);
25982
+ failed(new Error("The bridge couldn't lock Hermes's memory file, so it saved nothing: this machine has no perl."));
25983
+ });
25984
+ child.on("close", () => {
25985
+ clearTimeout(timer);
25986
+ failed(new Error("The bridge couldn't lock Hermes's memory file, so it saved nothing."));
25987
+ });
25988
+ });
25989
+ return await work();
25990
+ } finally {
25991
+ child.stdin.end();
25992
+ if (child.exitCode === null) {
25993
+ const killer = setTimeout(() => child.kill(), 2e3);
25994
+ await exited;
25995
+ clearTimeout(killer);
25996
+ }
25997
+ }
25998
+ }
25999
+ function writeWhole(path, text) {
26000
+ const target = existsSync6(path) && lstatSync(path).isSymbolicLink() ? realpathSync(path) : path;
26001
+ const mode = existsSync6(target) ? statSync2(target).mode & 511 : 384;
26002
+ const temp = join6(dirname4(target), `.chips_${basename3(target)}.${randomBytes(4).toString("hex")}`);
26003
+ const fd = openSync(temp, "w", mode);
26004
+ try {
26005
+ writeSync2(fd, text, null, "utf8");
26006
+ fsyncSync(fd);
26007
+ } catch (error62) {
26008
+ closeSync(fd);
26009
+ rmSync3(temp, { force: true });
26010
+ throw error62;
26011
+ }
26012
+ closeSync(fd);
26013
+ try {
26014
+ renameSync2(temp, target);
26015
+ } catch (error62) {
26016
+ rmSync3(temp, { force: true });
26017
+ throw error62;
26018
+ }
26019
+ }
26020
+ async function saveEdit(source2, edit) {
26021
+ const memory = source2.limit !== void 0;
26022
+ if (memory) {
26023
+ const problem = memoryProblem(edit.text, source2.limit ?? MEMORY_LIMIT, source2.kind === "user" ? "what it knows of you" : "its memory");
26024
+ if (problem !== void 0) throw new Refuse(problem, void 0, "failed");
26025
+ }
26026
+ const save = () => {
26027
+ const now = readText(source2.path);
26028
+ if (edit.base_hash !== null && hashOf(now) !== edit.base_hash) {
26029
+ throw new Refuse("", void 0, "changed");
26030
+ }
26031
+ writeWhole(source2.path, memory ? entriesOf(edit.text).join(ENTRY_DELIMITER) : edit.text);
26032
+ };
26033
+ if (!memory) {
26034
+ save();
26035
+ return;
26036
+ }
26037
+ const folder2 = dirname4(source2.path);
26038
+ if (!existsSync6(dirname4(folder2))) throw new Refuse(`Hermes has no profile at ${dirname4(folder2)}.`, void 0, "failed");
26039
+ mkdirSync4(folder2, { recursive: true, mode: 448 });
26040
+ await withLock(`${source2.path}.lock`, save);
26041
+ }
26042
+ var Contexts = class {
26043
+ constructor(roster, port, log2, env = process.env) {
26044
+ this.roster = roster;
26045
+ this.port = port;
26046
+ this.log = log2;
26047
+ this.env = env;
26048
+ }
26049
+ roster;
26050
+ port;
26051
+ log;
26052
+ env;
26053
+ chain = Promise.resolve();
26054
+ /** Every copy read again, and sent where it moved. */
26055
+ refresh() {
26056
+ return this.inLine(async () => {
26057
+ const { readers, first } = this.roster();
26058
+ const picture = new Picture(readers, first, this.env);
26059
+ await this.port.harness(picture.harnessCopy(first)).catch((error62) => {
26060
+ this.log(`the harness's context not sent: ${messageOf(error62)}`);
26061
+ });
26062
+ for (const { agentId: agentId2 } of readers) {
26063
+ const copy = picture.agentCopy(agentId2);
26064
+ if (copy === void 0) continue;
26065
+ await this.port.agent(agentId2, copy).catch((error62) => {
26066
+ this.log(`${agentId2}'s context not sent: ${messageOf(error62)}`);
26067
+ });
26068
+ }
26069
+ }).catch((error62) => this.log(`the context wasn't read: ${messageOf(error62)}`));
26070
+ }
26071
+ /**
26072
+ * A person's edit from Chips: saved, and answered with the copy it is in now; or refused, the
26073
+ * copy as it is now beside the reason. The other copies the file is in follow.
26074
+ */
26075
+ async edit(frame) {
26076
+ const answer = await this.inLine(async () => {
26077
+ const { readers, first } = this.roster();
26078
+ const copyNow = () => {
26079
+ const picture = new Picture(readers, first, this.env);
26080
+ return frame.agent_id === void 0 ? picture.harnessCopy(first) : picture.agentCopy(frame.agent_id);
26081
+ };
26082
+ const found = new Picture(readers, first, this.env).find(frame.agent_id, frame.name);
26083
+ if (found === void 0) {
26084
+ throw new Refuse(`Hermes gives this agent no file called ${frame.name}.`, copyNow(), "failed");
26085
+ }
26086
+ const { source: source2, shared } = found;
26087
+ if (frame.base_hash === null) {
26088
+ 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.`;
26089
+ throw new Refuse(why, copyNow(), "failed");
26090
+ }
26091
+ try {
26092
+ await saveEdit(source2, frame);
26093
+ } catch (error62) {
26094
+ const reason = error62 instanceof Refuse ? error62.reason : "failed";
26095
+ this.log(`${source2.path} not saved from Chips: ${reason === "changed" ? "it changed since" : messageOf(error62)}`);
26096
+ throw new Refuse(error62 instanceof Refuse ? error62.message : messageOf(error62), copyNow(), reason);
26097
+ }
26098
+ this.log(`${source2.path} saved from Chips${shared ? ", for every agent that reads it" : ""}`);
26099
+ const copy = copyNow();
26100
+ if (copy === void 0) throw new Refuse("Saved, but the agent is gone.", void 0, "failed");
26101
+ return copy;
26102
+ });
26103
+ void this.refresh();
26104
+ return answer;
26105
+ }
26106
+ inLine(work) {
26107
+ const done = this.chain.catch(() => void 0).then(work);
26108
+ this.chain = done.catch(() => void 0);
26109
+ return done;
26110
+ }
26111
+ };
26112
+ function messageOf(error62) {
26113
+ return error62 instanceof Error ? error62.message : String(error62);
26114
+ }
26115
+
26116
+ // src/fills.ts
26117
+ import { createHmac, timingSafeEqual } from "node:crypto";
26118
+ import { createServer } from "node:http";
26119
+ var HOOK_PATH = "/chips-hermes/";
26120
+ var MAX_BODY = 4 * 1024 * 1024;
26121
+ var DROP = 0.7;
26122
+ var SEND_EVERY_MS = 2e3;
26123
+ var THRESHOLD = 0.5;
26124
+ var SMALL_WINDOW = 512e3;
26125
+ var SMALL_THRESHOLD = 0.75;
26126
+ var WHY_NOT = "Hermes can't be asked to compact from Chips.";
26127
+ var whyReadOnly = (profile) => `Hermes sets this for every agent on its ${profile} profile.`;
26128
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
26129
+ function limitOf(home2, window) {
26130
+ const text = configText(home2);
26131
+ const enabled = yamlBool(yamlValue(text, "compression", "enabled")) ?? true;
26132
+ const threshold = yamlNumber(yamlValue(text, "compression", "threshold")) ?? THRESHOLD;
26133
+ let ratio = window < SMALL_WINDOW ? Math.max(threshold, SMALL_THRESHOLD) : threshold;
26134
+ const tokens2 = yamlNumber(yamlValue(text, "compression", "threshold_tokens"));
26135
+ if (tokens2 !== void 0 && tokens2 > 0 && window > 0) ratio = Math.min(ratio, tokens2 / window);
26136
+ const at = Math.min(99, Math.max(1, Math.round(ratio * 100)));
26137
+ return enabled ? { at, choices: [at], default: at, may_turn_off: false } : { at: null, choices: [at], default: null, may_turn_off: true };
26138
+ }
26139
+ function signedBy(body, header, secret) {
26140
+ if (header === void 0 || !header.startsWith("sha256=")) return false;
26141
+ const given = Buffer.from(header.slice("sha256=".length), "hex");
26142
+ const made = createHmac("sha256", secret).update(body).digest();
26143
+ return given.length === made.length && timingSafeEqual(given, made);
26144
+ }
26145
+ function numbersOf(profile, body) {
26146
+ if (typeof body !== "object" || body === null) return void 0;
26147
+ const top = body;
26148
+ if (top["hook_event_name"] !== "post_api_request") return void 0;
26149
+ const extra = typeof top["extra"] === "object" && top["extra"] !== null ? top["extra"] : {};
26150
+ const named = typeof top["session_id"] === "string" ? top["session_id"] : "";
26151
+ const task2 = typeof extra["task_id"] === "string" && extra["task_id"] !== "" ? extra["task_id"] : void 0;
26152
+ const session = task2 ?? named;
26153
+ if (session === "") return void 0;
26154
+ const usage2 = typeof extra["usage"] === "object" && extra["usage"] !== null ? extra["usage"] : {};
26155
+ 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));
26156
+ const window = num(extra["context_length"]);
26157
+ if (prompt === void 0 || prompt <= 0 || window === void 0 || window <= 0) return void 0;
26158
+ const ended = num(extra["ended_at"]);
26159
+ const stamped = typeof top["timestamp"] === "string" && !Number.isNaN(Date.parse(top["timestamp"])) ? top["timestamp"] : void 0;
26160
+ const at = ended !== void 0 ? new Date(ended * 1e3).toISOString() : stamped ?? (/* @__PURE__ */ new Date()).toISOString();
26161
+ const model = typeof extra["model"] === "string" && extra["model"] !== "" ? extra["model"].slice(0, 120) : void 0;
26162
+ const turn = typeof extra["turn_id"] === "string" && extra["turn_id"] !== "" ? extra["turn_id"] : void 0;
26163
+ return {
26164
+ profile,
26165
+ session,
26166
+ turn,
26167
+ first: num(extra["api_call_count"]) === 1,
26168
+ used: Math.round(prompt),
26169
+ window: Math.round(window),
26170
+ model,
26171
+ at: new Date(at).toISOString()
26172
+ };
26173
+ }
26174
+ var Fills = class {
26175
+ constructor(options) {
26176
+ this.options = options;
26177
+ const kept = options.kept;
26178
+ kept.readings ??= {};
26179
+ kept.lasts ??= {};
26180
+ kept.longTurns ??= {};
26181
+ for (const reading of Object.values(kept.readings)) this.lastOf.set(reading.session, reading);
26182
+ }
26183
+ options;
26184
+ server;
26185
+ /** The last reading of each session, for a compaction's drop. */
26186
+ lastOf = /* @__PURE__ */ new Map();
26187
+ turns = /* @__PURE__ */ new Map();
26188
+ timers = /* @__PURE__ */ new Map();
26189
+ sentAt = /* @__PURE__ */ new Map();
26190
+ /** The profiles whose unsigned or wrongly signed deliveries have been said once. */
26191
+ badSaid = /* @__PURE__ */ new Set();
26192
+ heard = false;
26193
+ /** Whether any reading has come since the bridge started. */
26194
+ get anyHeard() {
26195
+ return this.heard;
26196
+ }
26197
+ /** Hear Hermes on `127.0.0.1:port`: the port it took (one of its own for 0). */
26198
+ listen(port, host = "127.0.0.1") {
26199
+ const server = createServer((request, response) => this.handle(request, response));
26200
+ this.server = server;
26201
+ return new Promise((done, failed) => {
26202
+ server.once("error", failed);
26203
+ server.listen(port, host, () => {
26204
+ server.off("error", failed);
26205
+ done(server.address().port);
26206
+ });
26207
+ });
26208
+ }
26209
+ async close() {
26210
+ for (const timer of this.timers.values()) clearTimeout(timer);
26211
+ if (this.timers.size > 0) this.options.save();
26212
+ this.timers.clear();
26213
+ const server = this.server;
26214
+ this.server = void 0;
26215
+ if (server === void 0) return;
26216
+ server.closeAllConnections();
26217
+ await new Promise((done) => server.close(() => done()));
26218
+ }
26219
+ /** An agent's copy, from what is known of it, or nothing when no reading has come. */
26220
+ copyOf(agentId2) {
26221
+ const { kept } = this.options;
26222
+ const reading = kept.readings?.[agentId2];
26223
+ const profile = this.options.profileOf(agentId2);
26224
+ if (reading === void 0 || profile === void 0) return void 0;
26225
+ const last = kept.lasts?.[agentId2];
26226
+ const long = kept.longTurns?.[agentId2];
26227
+ return {
26228
+ used: reading.used,
26229
+ window: reading.window,
26230
+ ...reading.model === void 0 ? {} : { model: reading.model },
26231
+ as_of: reading.at,
26232
+ ...last === void 0 ? {} : { last },
26233
+ ...long === void 0 ? {} : { long_turn: long },
26234
+ compacts: false,
26235
+ why_not: WHY_NOT,
26236
+ limit: limitOf(profile.home, reading.window),
26237
+ editable: false,
26238
+ why_read_only: whyReadOnly(profile.profile)
26239
+ };
26240
+ }
26241
+ /** Each agent's kept copy, sent: at start, so a restart shows what was known. */
26242
+ async sendAll() {
26243
+ for (const agentId2 of Object.keys(this.options.kept.readings ?? {})) await this.send(agentId2);
26244
+ }
26245
+ /** An agent gone: nothing more is sent for it, and nothing kept. */
26246
+ forget(agentId2) {
26247
+ const { kept } = this.options;
26248
+ clearTimeout(this.timers.get(agentId2));
26249
+ this.timers.delete(agentId2);
26250
+ if (kept.readings?.[agentId2] === void 0 && kept.lasts?.[agentId2] === void 0) return;
26251
+ delete kept.readings?.[agentId2];
26252
+ delete kept.lasts?.[agentId2];
26253
+ delete kept.longTurns?.[agentId2];
26254
+ this.options.save();
26255
+ }
26256
+ /** One delivery: read whole up to its limit, checked, answered at once, then taken in. */
26257
+ handle(request, response) {
26258
+ const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
26259
+ const profile = url2.pathname.startsWith(HOOK_PATH) ? decodeURIComponent(url2.pathname.slice(HOOK_PATH.length)) : "";
26260
+ if (request.method !== "POST" || profile === "" || profile.includes("/")) {
26261
+ response.writeHead(404).end();
26262
+ return;
26263
+ }
26264
+ const chunks = [];
26265
+ let size = 0;
26266
+ let over = false;
26267
+ request.on("data", (chunk) => {
26268
+ if (over) return;
26269
+ size += chunk.length;
26270
+ if (size > MAX_BODY) {
26271
+ over = true;
26272
+ response.writeHead(413, { connection: "close" }).end();
26273
+ request.destroy();
26274
+ return;
26275
+ }
26276
+ chunks.push(chunk);
26277
+ });
26278
+ request.on("end", () => {
26279
+ if (over) return;
26280
+ const body = Buffer.concat(chunks);
26281
+ const secret = this.options.secretFor(profile);
26282
+ const header = request.headers["x-hermes-signature-256"];
26283
+ if (secret === void 0 || !signedBy(body, typeof header === "string" ? header : void 0, secret)) {
26284
+ response.writeHead(401).end();
26285
+ if (!this.badSaid.has(profile)) {
26286
+ this.badSaid.add(profile);
26287
+ this.options.log(
26288
+ `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`
26289
+ );
26290
+ }
26291
+ return;
26292
+ }
26293
+ response.writeHead(204).end();
26294
+ let parsed;
26295
+ try {
26296
+ parsed = JSON.parse(body.toString("utf8"));
26297
+ } catch {
26298
+ return;
26299
+ }
26300
+ const numbers = numbersOf(profile, parsed);
26301
+ if (numbers !== void 0) this.take(numbers);
26302
+ });
26303
+ }
26304
+ /** A reading taken in: the agent's latest, a compaction when it fell, the turn's peak. */
26305
+ take(numbers) {
26306
+ const whose = this.options.whose(numbers.session);
26307
+ if (whose === void 0) return;
26308
+ this.heard = true;
26309
+ const { kept } = this.options;
26310
+ const { agentId: agentId2 } = whose;
26311
+ const reading = {
26312
+ used: numbers.used,
26313
+ window: numbers.window,
26314
+ ...numbers.model === void 0 ? {} : { model: numbers.model },
26315
+ at: numbers.at,
26316
+ session: numbers.session
26317
+ };
26318
+ const before = this.lastOf.get(numbers.session);
26319
+ const fell = before !== void 0 && before.model === reading.model && reading.used < before.used * DROP;
26320
+ if (fell && before !== void 0) {
26321
+ (kept.lasts ??= {})[agentId2] = { at: reading.at, from: before.used, to: reading.used, by: "itself" };
26322
+ this.options.log(`${agentId2} compacted in Hermes, ${before.used} to ${reading.used} tokens`);
26323
+ }
26324
+ let turn = this.turns.get(numbers.session);
26325
+ const fresh = numbers.turn === void 0 ? numbers.first || turn === void 0 : turn?.id !== numbers.turn;
26326
+ if (turn === void 0 || fresh) {
26327
+ turn = { id: numbers.turn, peak: 0, folds: 0 };
26328
+ this.turns.set(numbers.session, turn);
26329
+ }
26330
+ turn.peak = Math.max(turn.peak, reading.used);
26331
+ if (fell) turn.folds += 1;
26332
+ if (turn.peak > reading.window / 2) {
26333
+ (kept.longTurns ??= {})[agentId2] = { at: reading.at, peak: turn.peak, folds: Math.min(1e3, turn.folds) };
26334
+ }
26335
+ this.lastOf.set(numbers.session, reading);
26336
+ (kept.readings ??= {})[agentId2] = reading;
26337
+ this.schedule(agentId2);
26338
+ }
26339
+ /** The agent's copy sent now, or once readings slow down, and what is kept saved with it. */
26340
+ schedule(agentId2) {
26341
+ if (this.timers.has(agentId2)) return;
26342
+ const every = this.options.sendEveryMs ?? SEND_EVERY_MS;
26343
+ const wait2 = Math.max(0, (this.sentAt.get(agentId2) ?? 0) + every - Date.now());
26344
+ const timer = setTimeout(() => {
26345
+ this.timers.delete(agentId2);
26346
+ this.sentAt.set(agentId2, Date.now());
26347
+ this.options.save();
26348
+ void this.send(agentId2);
26349
+ }, wait2);
26350
+ timer.unref?.();
26351
+ this.timers.set(agentId2, timer);
26352
+ }
26353
+ async send(agentId2) {
26354
+ const copy = this.copyOf(agentId2);
26355
+ if (copy === void 0) return;
26356
+ await this.options.send(agentId2, copy).catch((error62) => {
26357
+ this.options.log(`${agentId2}'s context window not sent: ${error62 instanceof Error ? error62.message : String(error62)}`);
26358
+ });
26359
+ }
26360
+ };
25471
26361
 
25472
26362
  // src/hermes.ts
25473
26363
  var TERMINAL = /* @__PURE__ */ new Set([
@@ -25490,9 +26380,9 @@ var HermesClient = class {
25490
26380
  constructor(address, fetcher = fetch) {
25491
26381
  this.address = address;
25492
26382
  this.fetcher = fetcher;
25493
- const root = address.url.replace(/\/+$/, "");
26383
+ const root2 = address.url.replace(/\/+$/, "");
25494
26384
  const profile = address.profile;
25495
- this.base = profile === void 0 || profile === "default" ? root : `${root}/p/${encodeURIComponent(profile)}`;
26385
+ this.base = profile === void 0 || profile === "default" ? root2 : `${root2}/p/${encodeURIComponent(profile)}`;
25496
26386
  }
25497
26387
  address;
25498
26388
  fetcher;
@@ -25665,8 +26555,126 @@ async function failureOf(answer) {
25665
26555
  return new HermesError(message, answer.status, code);
25666
26556
  }
25667
26557
 
26558
+ // src/hooks.ts
26559
+ import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
26560
+ import { join as join7 } from "node:path";
26561
+ var HOOK_NAME = "chips";
26562
+ var SECRET_ENV = "CHIPS_HOOK_SECRET";
26563
+ function hookUrl(port, profile) {
26564
+ return `http://127.0.0.1:${port}${HOOK_PATH}${profile}`;
26565
+ }
26566
+ function linesOf2(text) {
26567
+ return text.split("\n").map((line) => {
26568
+ const indent = line.length - line.trimStart().length;
26569
+ const words4 = line.trimStart().startsWith("#") ? "" : line.trim().replace(/\s+#.*$/, "");
26570
+ return { indent, words: words4 };
26571
+ });
26572
+ }
26573
+ function hookPortIn(home2) {
26574
+ const path = join7(home2, "config.yaml");
26575
+ if (!existsSync7(path)) return void 0;
26576
+ const escaped = HOOK_PATH.replace(/[/]/g, "\\/");
26577
+ const match = new RegExp(`^\\s*(?:-\\s*)?url:\\s*["']?http:\\/\\/127\\.0\\.0\\.1:(\\d+)${escaped}`, "m").exec(
26578
+ readFileSync7(path, "utf8")
26579
+ );
26580
+ const port = match === null ? void 0 : Number(match[1]);
26581
+ return port !== void 0 && Number.isInteger(port) && port > 0 && port < 65536 ? port : void 0;
26582
+ }
26583
+ function entryLines(indent, url2, secret) {
26584
+ return [
26585
+ `${indent}outbound:`,
26586
+ `${indent} - name: ${HOOK_NAME}`,
26587
+ `${indent} url: ${url2}`,
26588
+ `${indent} events: [post_api_request]`,
26589
+ "env" in secret ? `${indent} secret_env: ${secret.env}` : `${indent} secret: "${secret.inline}"`,
26590
+ `${indent} timeout: 5`
26591
+ ];
26592
+ }
26593
+ function secretInEnv(home2, secret) {
26594
+ const path = join7(home2, ".env");
26595
+ if (!existsSync7(path)) return false;
26596
+ if (envValue(home2, SECRET_ENV) !== void 0) return true;
26597
+ const text = readFileSync7(path, "utf8");
26598
+ const mode = statSync3(path).mode & 511;
26599
+ appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${SECRET_ENV}=${secret}
26600
+ `);
26601
+ chmodSync3(path, mode);
26602
+ return true;
26603
+ }
26604
+ function addHook(home2, port, profile, secret) {
26605
+ const path = join7(home2, "config.yaml");
26606
+ if (!existsSync7(home2)) return { state: "no_home", path };
26607
+ const url2 = hookUrl(port, profile);
26608
+ const text = existsSync7(path) ? readFileSync7(path, "utf8") : "";
26609
+ const lines2 = linesOf2(text);
26610
+ const ours2 = new RegExp(`^(?:-\\s*)?url:\\s*["']?${url2.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}["']?$`);
26611
+ if (lines2.some((line) => ours2.test(line.words))) return { state: "there", path };
26612
+ const where = lines2.findIndex((line) => line.indent === 0 && /^["']?hooks["']?\s*:/.test(line.words));
26613
+ const byHand = (why) => {
26614
+ const inEnv2 = secretInEnv(home2, secret);
26615
+ const shown2 = entryLines(" ", url2, inEnv2 ? { env: SECRET_ENV } : { inline: secret });
26616
+ return { state: "by_hand", path, why, lines: ["hooks:", ...shown2].join("\n") };
26617
+ };
26618
+ const plain = /^(?:"[^"]+"|'[^']+'|[A-Za-z_][\w.-]*)\s*:(?:\s|$)/;
26619
+ if (lines2.some((line) => line.indent === 0 && line.words !== "" && !plain.test(line.words))) {
26620
+ return byHand("its top level isn't laid out as this bridge expects");
26621
+ }
26622
+ if (where === -1) {
26623
+ const inEnv2 = secretInEnv(home2, secret);
26624
+ const block = [
26625
+ "# Chips: how full each agent's context is, sent to the chips-hermes bridge after each model call.",
26626
+ "hooks:",
26627
+ ...entryLines(" ", url2, inEnv2 ? { env: SECRET_ENV } : { inline: secret })
26628
+ ];
26629
+ const before = text === "" ? "" : text.endsWith("\n") ? "\n" : "\n\n";
26630
+ if (text === "") writeFileSync4(path, `${block.join("\n")}
26631
+ `, { mode: 384 });
26632
+ else writeWhole(path, `${text}${before}${block.join("\n")}
26633
+ `);
26634
+ return { state: "added", path };
26635
+ }
26636
+ const head = lines2[where]?.words ?? "";
26637
+ if (head.replace(/^["']?hooks["']?\s*:/, "").trim() !== "") return byHand("its hooks: is written on one line");
26638
+ const children = [];
26639
+ for (const line of lines2.slice(where + 1)) {
26640
+ if (line.words === "") continue;
26641
+ if (line.indent === 0) break;
26642
+ children.push(line);
26643
+ }
26644
+ const indent = children[0]?.indent ?? 2;
26645
+ const named = new RegExp(`^(?:-\\s*)?name:\\s*["']?${HOOK_NAME}["']?$`);
26646
+ if (children.some((line) => named.test(line.words))) {
26647
+ return byHand(`its hooks have an entry called ${HOOK_NAME} with another address`);
26648
+ }
26649
+ if (children.some((line) => line.indent === indent && /^["']?outbound["']?\s*:/.test(line.words))) {
26650
+ return byHand("its hooks already have an outbound list");
26651
+ }
26652
+ if (children.some((line) => line.indent !== indent && line.indent < indent) || children[0]?.words.startsWith("-")) {
26653
+ return byHand("its hooks: isn't laid out as this bridge expects");
26654
+ }
26655
+ const inEnv = secretInEnv(home2, secret);
26656
+ const raw = text.split("\n");
26657
+ raw.splice(where + 1, 0, ...entryLines(" ".repeat(indent), url2, inEnv ? { env: SECRET_ENV } : { inline: secret }));
26658
+ writeWhole(path, raw.join("\n"));
26659
+ return { state: "added", path };
26660
+ }
26661
+
25668
26662
  // src/bridge.ts
25669
- var WANTS = ["agents", "new_agents", "forks"];
26663
+ var WANTS = ["agents", "new_agents", "forks", "context_files", "context_windows"];
26664
+ function settingsFor(active) {
26665
+ const bridge2 = () => {
26666
+ const there = active();
26667
+ if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
26668
+ return there;
26669
+ };
26670
+ return {
26671
+ context: (edit) => bridge2().onContextEdit(edit),
26672
+ contextWindows: {
26673
+ compact: async (ask2) => bridge2().onWindowCompact(ask2),
26674
+ change: async (ask2) => bridge2().onWindowChange(ask2)
26675
+ }
26676
+ };
26677
+ }
25670
26678
  function filesHere(project = false) {
25671
26679
  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
26680
  }
@@ -25675,10 +26683,10 @@ function instructionsFor(folder2, project = false) {
25675
26683
  const files = filesHere(project);
25676
26684
  return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
25677
26685
  }
25678
- function freeFolder(root, name2, taken) {
26686
+ function freeFolder(root2, name2, taken) {
25679
26687
  const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
25680
26688
  for (let count = 1; ; count += 1) {
25681
- const folder2 = join5(root, count === 1 ? slug : `${slug}-${count}`);
26689
+ const folder2 = join8(root2, count === 1 ? slug : `${slug}-${count}`);
25682
26690
  if (!taken.includes(folder2)) return folder2;
25683
26691
  }
25684
26692
  }
@@ -25709,6 +26717,32 @@ var Bridge = class {
25709
26717
  constructor(options) {
25710
26718
  this.options = options;
25711
26719
  this.log = options.log ?? (() => void 0);
26720
+ const { chips: chips2, hermesRoot: hermesRoot2 } = options;
26721
+ if (hermesRoot2 === void 0) return;
26722
+ this.contexts = new Contexts(
26723
+ () => this.roster(),
26724
+ {
26725
+ agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
26726
+ harness: (copy) => chips2.settings.harnessContext(copy)
26727
+ },
26728
+ (line) => this.log(`context: ${line}`),
26729
+ options.env ?? process.env
26730
+ );
26731
+ if (options.fills === void 0) return;
26732
+ const kept = options.state.fills ??= {};
26733
+ this.fills = new Fills({
26734
+ whose: (sessionId) => this.whoseSession(sessionId),
26735
+ profileOf: (agentId2) => {
26736
+ const profile = this.profileFor(agentId2);
26737
+ return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
26738
+ },
26739
+ secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
26740
+ send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
26741
+ kept,
26742
+ save: () => options.saveState(options.state),
26743
+ log: (line) => this.log(`context window: ${line}`),
26744
+ sendEveryMs: options.fillsEveryMs
26745
+ });
25712
26746
  }
25713
26747
  options;
25714
26748
  agents = [];
@@ -25723,6 +26757,16 @@ var Bridge = class {
25723
26757
  copying = Promise.resolve();
25724
26758
  log;
25725
26759
  health;
26760
+ /** The harness's agents in Chips as last listed: whose context and window copies are sent. */
26761
+ listed = [];
26762
+ /** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
26763
+ contexts;
26764
+ contextTimer;
26765
+ /** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
26766
+ fills;
26767
+ /** The port Hermes's webhook reaches the bridge on, once it listens. */
26768
+ fillsPort;
26769
+ toldNoReadings = false;
25726
26770
  /** Register the agents, start hearing what is said to them, and say how their gateways are. */
25727
26771
  async start() {
25728
26772
  const { chips: chips2 } = this.options;
@@ -25736,20 +26780,124 @@ var Bridge = class {
25736
26780
  this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
25737
26781
  chips2.on("message.new", (message) => void this.hear(message));
25738
26782
  chips2.on("agent.asked", (asked) => {
25739
- this.make(asked).catch((error62) => this.log(`a new agent went wrong: ${messageOf(error62)}`));
26783
+ this.make(asked).catch((error62) => this.log(`a new agent went wrong: ${messageOf2(error62)}`));
25740
26784
  });
25741
26785
  chips2.on("agent.fork", (fork) => {
25742
- this.copying = this.copying.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf(error62)}`));
26786
+ this.copying = this.copying.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf2(error62)}`));
25743
26787
  });
25744
26788
  chips2.on("feedback", (feedback2) => {
25745
- this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf(error62)}`));
26789
+ this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf2(error62)}`));
25746
26790
  });
26791
+ chips2.on("agent.attached", () => void this.agentsMoved());
26792
+ chips2.on("agent.released", () => void this.agentsMoved());
25747
26793
  await this.sayHowItIs();
25748
26794
  this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
25749
26795
  this.health.unref();
26796
+ await this.startSettings();
25750
26797
  }
25751
26798
  stop() {
25752
26799
  if (this.health !== void 0) clearInterval(this.health);
26800
+ if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
26801
+ void this.fills?.close();
26802
+ }
26803
+ // ------------------------------------------------------------------ an agent's page
26804
+ /**
26805
+ * The agents' context and context windows, sent at start and kept current: the files read
26806
+ * again every so often and after each turn, a window copy on each reading Hermes sends.
26807
+ */
26808
+ async startSettings() {
26809
+ if (this.contexts === void 0) return;
26810
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26811
+ await this.contexts.refresh();
26812
+ this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
26813
+ this.contextTimer.unref();
26814
+ const fills2 = this.fills;
26815
+ const asked = this.options.fills;
26816
+ if (fills2 === void 0 || asked === void 0) return;
26817
+ const kept = this.options.state.fills ??= {};
26818
+ try {
26819
+ this.fillsPort = await fills2.listen(asked.port);
26820
+ if (kept.port !== this.fillsPort) {
26821
+ kept.port = this.fillsPort;
26822
+ this.options.saveState(this.options.state);
26823
+ }
26824
+ } catch (error62) {
26825
+ 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`);
26826
+ }
26827
+ await fills2.sendAll();
26828
+ }
26829
+ /** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
26830
+ hookUrl(profile = "default") {
26831
+ return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
26832
+ }
26833
+ /** The harness's agents listed again, the copies of any gone forgotten, and the context sent. */
26834
+ async agentsMoved() {
26835
+ if (this.contexts === void 0) return;
26836
+ const before = this.listed;
26837
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26838
+ const now = new Set(this.active().map((agent) => agent.agentId));
26839
+ for (const gone of before) {
26840
+ if (now.has(gone.agentId)) continue;
26841
+ this.options.chips.settings.forget(gone.agentId);
26842
+ this.fills?.forget(gone.agentId);
26843
+ }
26844
+ await this.contexts.refresh();
26845
+ }
26846
+ /** The harness's agents that read anything now: the active ones listed. */
26847
+ active() {
26848
+ return this.listed.filter((agent) => agent.state === "active");
26849
+ }
26850
+ /** A profile as the context needs it: its name and its home under Hermes's root. */
26851
+ profileOfRunning(agent) {
26852
+ const root2 = this.options.hermesRoot;
26853
+ if (root2 === void 0) return void 0;
26854
+ return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
26855
+ }
26856
+ /** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
26857
+ profileFor(agentId2) {
26858
+ const own2 = this.agents.find((agent) => agent.agentId === agentId2);
26859
+ const copied = this.options.state.profiles?.[agentId2];
26860
+ const original = this.agents.find((agent) => copied !== void 0 && agent.key === copied);
26861
+ const running = own2 ?? original ?? this.agents[0];
26862
+ return running === void 0 ? void 0 : this.profileOfRunning(running);
26863
+ }
26864
+ /** Who reads what: each active agent with its profile, and the first profile, which new agents run on. */
26865
+ roster() {
26866
+ const readers = [];
26867
+ const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
26868
+ for (const agentId2 of ids2) {
26869
+ if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
26870
+ const profile = this.profileFor(agentId2);
26871
+ if (profile !== void 0) readers.push({ agentId: agentId2, profile });
26872
+ }
26873
+ const first = this.agents[0];
26874
+ return { readers, first: first === void 0 ? void 0 : this.profileOfRunning(first) };
26875
+ }
26876
+ /** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
26877
+ whoseSession(sessionId) {
26878
+ const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
26879
+ if (entry === void 0) return void 0;
26880
+ const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
26881
+ const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
26882
+ const profile = this.profileFor(agentId2);
26883
+ return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
26884
+ }
26885
+ /** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
26886
+ async onContextEdit(frame) {
26887
+ if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
26888
+ if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
26889
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26890
+ }
26891
+ return this.contexts.edit(frame);
26892
+ }
26893
+ /** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
26894
+ onWindowCompact(frame) {
26895
+ throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
26896
+ }
26897
+ /** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
26898
+ onWindowChange(frame) {
26899
+ const profile = this.profileFor(frame.agent_id);
26900
+ throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
25753
26901
  }
25754
26902
  /** The Main agent, the first registered, once started: who says the harness's first words. */
25755
26903
  get mainAgentId() {
@@ -25801,6 +26949,7 @@ var Bridge = class {
25801
26949
  this.ours.add(agent.agentId);
25802
26950
  this.names.set(agent.agentId, agent.name);
25803
26951
  this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
26952
+ void this.agentsMoved();
25804
26953
  }
25805
26954
  /**
25806
26955
  * A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
@@ -25834,7 +26983,7 @@ var Bridge = class {
25834
26983
  const room = listed.find((agent) => agent.agentId === fork.agent_id)?.room ?? void 0;
25835
26984
  const from = (room === void 0 ? void 0 : state2.sessions[`${who}|${room}`]) ?? Object.entries(state2.sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
25836
26985
  if (from === void 0) this.log(`${name2} never spoke: its copy remembers nothing`);
25837
- const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips room ${spaceId} \xB7 ${key}` });
26986
+ const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips space ${spaceId} \xB7 ${key}` });
25838
26987
  const folder2 = this.folderOf(original, fork.agent_id, name2);
25839
26988
  const copy = await chips2.agents.register({ key, name: forkName(name2), forkedFrom: fork.agent_id, spaceId });
25840
26989
  if (session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] ??= session;
@@ -25845,6 +26994,7 @@ var Bridge = class {
25845
26994
  this.names.set(copy.agentId, copy.name);
25846
26995
  const away = copy.attached === false && !copy.spaces.includes(spaceId);
25847
26996
  this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
26997
+ void this.agentsMoved();
25848
26998
  }
25849
26999
  /** An agent's name, from Chips's list when it isn't known yet. */
25850
27000
  async nameOf(agentId2, otherwise) {
@@ -25863,11 +27013,11 @@ var Bridge = class {
25863
27013
  */
25864
27014
  folderOf(agent, agentId2, name2) {
25865
27015
  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;
27016
+ const { folders: root2, state: state2 } = this.options;
27017
+ if (root2 === void 0) return void 0;
25868
27018
  const kept = state2.folders?.[agentId2];
25869
- const folder2 = kept ?? freeFolder(root, name2, Object.values(state2.folders ?? {}));
25870
- mkdirSync4(folder2, { recursive: true });
27019
+ const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
27020
+ mkdirSync5(folder2, { recursive: true });
25871
27021
  if (kept === void 0) {
25872
27022
  state2.folders = { ...state2.folders, [agentId2]: folder2 };
25873
27023
  this.options.saveState(state2);
@@ -25889,7 +27039,7 @@ var Bridge = class {
25889
27039
  turn.project = { copy: project, name: name2 };
25890
27040
  return facts;
25891
27041
  } catch (error62) {
25892
- this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf(error62)}`);
27042
+ this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf2(error62)}`);
25893
27043
  return void 0;
25894
27044
  }
25895
27045
  }
@@ -25908,8 +27058,8 @@ var Bridge = class {
25908
27058
  const note = heldNote(pushed, name2);
25909
27059
  return note === void 0 ? {} : { note };
25910
27060
  } catch (error62) {
25911
- this.log(`${name2}'s project couldn't be sent: ${messageOf(error62)}`);
25912
- const note = heldNote({ state: "failed", why: messageOf(error62) }, name2);
27061
+ this.log(`${name2}'s project couldn't be sent: ${messageOf2(error62)}`);
27062
+ const note = heldNote({ state: "failed", why: messageOf2(error62) }, name2);
25913
27063
  return note === void 0 ? {} : { note };
25914
27064
  }
25915
27065
  }
@@ -25918,8 +27068,8 @@ var Bridge = class {
25918
27068
  for (const agent of this.agents) agent.up = await agent.client.healthy();
25919
27069
  const down = this.agents.filter((agent) => !agent.up);
25920
27070
  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);
27071
+ const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(`Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
27072
+ await said2.catch(() => void 0);
25923
27073
  }
25924
27074
  // ------------------------------------------------------------------ what is said to it
25925
27075
  /** A message for some of its agents: each takes a turn in its room, one after another there. */
@@ -26014,9 +27164,10 @@ var Bridge = class {
26014
27164
  try {
26015
27165
  await this.turn(room, asked);
26016
27166
  } catch (error62) {
26017
- this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf(error62)}`);
27167
+ this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf2(error62)}`);
26018
27168
  } finally {
26019
27169
  room.busy = false;
27170
+ void this.contexts?.refresh();
26020
27171
  void this.pump(room);
26021
27172
  }
26022
27173
  }
@@ -26051,7 +27202,7 @@ var Bridge = class {
26051
27202
  try {
26052
27203
  session = await this.sessionFor(room);
26053
27204
  } catch (error62) {
26054
- await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf(error62)}`, true);
27205
+ await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf2(error62)}`, true);
26055
27206
  return;
26056
27207
  }
26057
27208
  const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
@@ -26080,7 +27231,7 @@ var Bridge = class {
26080
27231
  try {
26081
27232
  turn.hermesRun = await agent.client.startRun({ text, sessionId: session, model: agent.config.model, key, instructions: instructionsFor(turn.folder, turn.project !== void 0) });
26082
27233
  } catch (error62) {
26083
- await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf(error62)}`, true);
27234
+ await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf2(error62)}`, true);
26084
27235
  return void 0;
26085
27236
  }
26086
27237
  if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
@@ -26088,7 +27239,7 @@ var Bridge = class {
26088
27239
  const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
26089
27240
  return last ?? await this.endOf(turn);
26090
27241
  } catch (error62) {
26091
- this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf(error62)}`);
27242
+ this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf2(error62)}`);
26092
27243
  return await this.endOf(turn);
26093
27244
  }
26094
27245
  }
@@ -26101,7 +27252,7 @@ var Bridge = class {
26101
27252
  if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
26102
27253
  return { ...state2, event: `run.${status}` };
26103
27254
  }
26104
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
27255
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
26105
27256
  }
26106
27257
  await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
26107
27258
  return void 0;
@@ -26111,7 +27262,7 @@ var Bridge = class {
26111
27262
  const kept = this.options.state.sessions[id2];
26112
27263
  if (kept !== void 0) return kept;
26113
27264
  const who = id2.slice(0, id2.lastIndexOf("|"));
26114
- const made = await agent.client.newSession(`Chips room ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
27265
+ const made = await agent.client.newSession(`Chips space ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
26115
27266
  this.options.state.sessions[id2] = made;
26116
27267
  this.options.saveState(this.options.state);
26117
27268
  return made;
@@ -26133,7 +27284,7 @@ var Bridge = class {
26133
27284
  await turn.agent.client.steer(turn.hermesRun, text);
26134
27285
  return;
26135
27286
  } catch (error62) {
26136
- this.log(`Hermes didn't take a steer mid-run (${messageOf(error62)}); saying it next`);
27287
+ this.log(`Hermes didn't take a steer mid-run (${messageOf2(error62)}); saying it next`);
26137
27288
  }
26138
27289
  }
26139
27290
  turn.next.push(text);
@@ -26182,11 +27333,11 @@ var Bridge = class {
26182
27333
  const stepId = callId ?? `${tool}-${turn.counted}`;
26183
27334
  const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
26184
27335
  turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
26185
- const said = sub ? event["goal"] : event["preview"];
27336
+ const said2 = sub ? event["goal"] : event["preview"];
26186
27337
  await turn.run.step({
26187
27338
  stepId,
26188
27339
  status: "started",
26189
- title: `${tool}${typeof said === "string" && said !== "" ? ` \xB7 ${said}` : ""}`.slice(0, 300),
27340
+ title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
26190
27341
  tool,
26191
27342
  ...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
26192
27343
  }).catch(() => void 0);
@@ -26241,7 +27392,7 @@ var Bridge = class {
26241
27392
  ${command}
26242
27393
  \`\`\`` } }
26243
27394
  });
26244
- const stopped2 = new Promise((resolve5) => turn.run.on("cancel", () => resolve5(void 0)));
27395
+ const stopped2 = new Promise((resolve6) => turn.run.on("cancel", () => resolve6(void 0)));
26245
27396
  const decision = await Promise.race([asked, stopped2]);
26246
27397
  const pending = turn.approvals.get(requestId ?? approvalId);
26247
27398
  if (decision === void 0 || pending === void 0 || pending.answered) return;
@@ -26252,7 +27403,7 @@ ${command}
26252
27403
  choice: choiceOf(decision, choices),
26253
27404
  ...requestId === void 0 ? {} : { request_id: requestId },
26254
27405
  ...typeof generation === "number" ? { execution_generation: generation } : {}
26255
- }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf(error62)}`));
27406
+ }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf2(error62)}`));
26256
27407
  }
26257
27408
  /** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
26258
27409
  async settled(turn, event) {
@@ -26297,10 +27448,27 @@ ${command}
26297
27448
  if (reply !== "" || post.uploadIds.length > 0) {
26298
27449
  await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
26299
27450
  }
26300
- const said = post.summary;
26301
- const summary = said === "" ? "Done." : said.length <= 2e3 ? said : `${said.slice(0, 1990)}\u2026`;
27451
+ const said2 = post.summary;
27452
+ const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
26302
27453
  await run2.complete({ summary });
26303
27454
  this.log(`${agent.config.name} answered in ${run2.spaceId}`);
27455
+ this.missingReadings();
27456
+ }
27457
+ /**
27458
+ * A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
27459
+ * likely started before Chips's webhook was in its config. Said once, a little after, since a
27460
+ * reading comes a moment after its model call.
27461
+ */
27462
+ missingReadings() {
27463
+ if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
27464
+ this.toldNoReadings = true;
27465
+ setTimeout(() => {
27466
+ if (this.fills?.anyHeard === false) {
27467
+ this.log(
27468
+ "context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
27469
+ );
27470
+ }
27471
+ }, 1e4).unref();
26304
27472
  }
26305
27473
  /** A run that failed, was stopped or was cut short in Hermes. */
26306
27474
  async ended(turn, last) {
@@ -26359,7 +27527,7 @@ function idOf(event) {
26359
27527
  const id2 = event["request_id"] ?? event["prompt_id"];
26360
27528
  return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
26361
27529
  }
26362
- function messageOf(error62) {
27530
+ function messageOf2(error62) {
26363
27531
  if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
26364
27532
  return error62 instanceof Error ? error62.message : String(error62);
26365
27533
  }
@@ -26375,11 +27543,11 @@ function goalOf(message, name2) {
26375
27543
  var REPLY_SHOWN = 1500;
26376
27544
  var GOAL_SHOWN = 120;
26377
27545
  function lookAgain(who, reply, note) {
26378
- const said = reply?.trim() ?? "";
27546
+ const said2 = reply?.trim() ?? "";
26379
27547
  return [
26380
- `${who} says your reply in this room wasn't right.`,
26381
- ...said === "" ? [] : [`Your reply:
26382
- \xAB${cut(said, REPLY_SHOWN)}\xBB`],
27548
+ `${who} says your reply in this space wasn't right.`,
27549
+ ...said2 === "" ? [] : [`Your reply:
27550
+ \xAB${cut(said2, REPLY_SHOWN)}\xBB`],
26383
27551
  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
27552
  "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
27553
  ].join("\n\n");
@@ -26401,8 +27569,8 @@ function resultWords(preview2) {
26401
27569
  }
26402
27570
  if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
26403
27571
  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() : "";
27572
+ const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
27573
+ return typeof said2 === "string" ? said2.trim() : "";
26406
27574
  }
26407
27575
  function lastLine(text) {
26408
27576
  const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line) => line.trim()).filter((line) => line !== "");
@@ -26413,16 +27581,16 @@ function lowerFirst(text) {
26413
27581
  }
26414
27582
 
26415
27583
  // 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";
27584
+ import { homedir as homedir5 } from "node:os";
27585
+ import { basename as basename4, join as join9, resolve as resolve3 } from "node:path";
26418
27586
  import { fileURLToPath as fileURLToPath2 } from "node:url";
26419
27587
  var flags = process.argv.slice(2);
26420
27588
  var words3 = flags.filter((word) => !word.startsWith("--"));
26421
- var whole = (word) => resolve2(word.startsWith("~/") ? join6(homedir4(), word.slice(2)) : word);
27589
+ var whole = (word) => resolve3(word.startsWith("~/") ? join9(homedir5(), word.slice(2)) : word);
26422
27590
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
26423
27591
  var self = fileURLToPath2(import.meta.url);
26424
27592
  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");
27593
+ var again = process.env["npm_lifecycle_event"] === "keep" ? "npm run keep --" : basename4(process.argv[1] ?? "chips");
26426
27594
  var stopLine = process.env["CHIPS_STOP_COMMAND"] || [again, ...words3, "--stop"].join(" ");
26427
27595
  var end = (line, code) => {
26428
27596
  (code === 0 ? process.stdout : process.stderr).write(`${line}
@@ -26464,9 +27632,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
26464
27632
  }
26465
27633
 
26466
27634
  // 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";
27635
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
27636
+ import { homedir as homedir6 } from "node:os";
27637
+ import { join as join10, resolve as resolve4 } from "node:path";
26470
27638
  var SetupError = class extends Error {
26471
27639
  constructor(message, words4 = {}) {
26472
27640
  super(message);
@@ -26475,37 +27643,70 @@ var SetupError = class extends Error {
26475
27643
  words;
26476
27644
  name = "SetupError";
26477
27645
  };
26478
- var shown = (path) => path.startsWith(`${homedir5()}/`) ? `~${path.slice(homedir5().length)}` : path;
27646
+ var shown = (path) => path.startsWith(`${homedir6()}/`) ? `~${path.slice(homedir6().length)}` : path;
26479
27647
  var portOf = (url2) => {
26480
27648
  const parsed = new URL(url2);
26481
27649
  return parsed.port || (parsed.protocol === "https:" ? "443" : "80");
26482
27650
  };
26483
- function checkedWords(config3) {
27651
+ function checkedWords(config3, restart = false) {
26484
27652
  const urls = new Set(config3.agents.map((agent) => agent.address.url));
26485
27653
  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`;
27654
+ const found = 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`;
27655
+ return restart ? `${found}. ${RESTART}` : found;
27656
+ }
27657
+ var RESTART = "Start hermes gateway again once, so it tells Chips how full each agent's context is.";
27658
+ function readyHooks(config3, options) {
27659
+ const lines2 = [];
27660
+ let restart = false;
27661
+ const done = /* @__PURE__ */ new Set();
27662
+ for (const agent of config3.agents) {
27663
+ const profile = profileName(agent.address.profile);
27664
+ if (done.has(profile)) continue;
27665
+ done.add(profile);
27666
+ const home2 = profileHome(options.root, agent.address.profile);
27667
+ let outcome;
27668
+ try {
27669
+ outcome = addHook(home2, options.port, profile, options.secret);
27670
+ } catch (error62) {
27671
+ lines2.push(`Chips's webhook wasn't added to ${shown(join10(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
27672
+ continue;
27673
+ }
27674
+ if (outcome.state === "added") {
27675
+ lines2.push(`Added Chips's webhook to ${shown(outcome.path)}, so Hermes tells Chips how full each agent's context is.`);
27676
+ restart ||= options.gatewayUp;
27677
+ } else if (outcome.state === "by_hand") {
27678
+ lines2.push(
27679
+ `${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:
27680
+ ${outcome.lines}`
27681
+ );
27682
+ } else if (outcome.state === "no_home") {
27683
+ lines2.push(`Hermes has no ${profile} profile at ${shown(home2)}, so no webhook was added for it.`);
27684
+ }
27685
+ }
27686
+ if (restart) lines2.push(RESTART);
27687
+ return { lines: lines2, restart };
26487
27688
  }
26488
27689
  var STILL_GOOD = "The link is still good: run the same command again once that's done.";
26489
27690
  var EXAMPLE = '{ "agents": [{ "name": "Hermes", "profile": "default", "keyEnv": "API_SERVER_KEY" }] }';
26490
27691
  var PLAIN = { agents: [{ name: "Hermes", profile: "default", keyEnv: "API_SERVER_KEY" }] };
26491
27692
  function hermesEnvPath(env = process.env) {
26492
- return resolve3(env["HERMES_HOME"] ?? resolve3(homedir5(), ".hermes"), ".env");
27693
+ return resolve4(env["HERMES_HOME"] ?? resolve4(homedir6(), ".hermes"), ".env");
26493
27694
  }
26494
27695
  function fromHermes(name2, env) {
26495
27696
  const path = hermesEnvPath(env);
26496
- if (!existsSync5(path)) return void 0;
26497
- for (const line of readFileSync5(path, "utf8").split("\n")) {
27697
+ if (!existsSync8(path)) return void 0;
27698
+ for (const line of readFileSync8(path, "utf8").split("\n")) {
26498
27699
  const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
26499
27700
  if (match?.[1] === name2) return match[2]?.replace(/^(["'])(.*)\1$/, "$2");
26500
27701
  }
26501
27702
  return void 0;
26502
27703
  }
26503
- function folderOf(said) {
26504
- if (typeof said !== "string" || said === "") return void 0;
26505
- return resolve3(said === "~" || said.startsWith("~/") ? homedir5() + said.slice(1) : said);
27704
+ function folderOf(said2) {
27705
+ if (typeof said2 !== "string" || said2 === "") return void 0;
27706
+ return resolve4(said2 === "~" || said2.startsWith("~/") ? homedir6() + said2.slice(1) : said2);
26506
27707
  }
26507
27708
  function readConfig(path, env = process.env) {
26508
- const raw = existsSync5(path) ? JSON.parse(readFileSync5(path, "utf8")) : PLAIN;
27709
+ const raw = existsSync8(path) ? JSON.parse(readFileSync8(path, "utf8")) : PLAIN;
26509
27710
  const url2 = typeof raw.url === "string" ? raw.url : "http://127.0.0.1:8642";
26510
27711
  const folder2 = folderOf(raw.folder);
26511
27712
  if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
@@ -26540,10 +27741,12 @@ function readConfig(path, env = process.env) {
26540
27741
  ...own2 === void 0 ? {} : { folder: own2 }
26541
27742
  };
26542
27743
  });
27744
+ const hookPort = raw.hookPort;
26543
27745
  return {
26544
27746
  ...typeof raw.name === "string" ? { name: raw.name } : {},
26545
27747
  agents,
26546
- folders: folderOf(raw.folders) ?? resolve3(homedir5(), "chips-hermes", "agents")
27748
+ folders: folderOf(raw.folders) ?? resolve4(homedir6(), "chips-hermes", "agents"),
27749
+ ...typeof hookPort === "number" && Number.isInteger(hookPort) && hookPort > 0 && hookPort < 65536 ? { hookPort } : {}
26547
27750
  };
26548
27751
  }
26549
27752
  async function checkHermes(config3, clientFor = (agent) => new HermesClient(agent.address)) {
@@ -26574,19 +27777,46 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
26574
27777
  }
26575
27778
 
26576
27779
  // src/main.ts
26577
- var home = (path) => path.startsWith("~/") ? join7(homedir6(), path.slice(2)) : path;
27780
+ var home = (path) => path.startsWith("~/") ? join11(homedir7(), path.slice(2)) : path;
26578
27781
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
26579
27782
  var LABEL = "chat.chips.hermes";
26580
27783
  stopIfAsked(LABEL);
26581
- var configPath = resolve4(home(words3[0] ?? "~/.chips-hermes/config.json"));
27784
+ var configPath = resolve5(home(words3[0] ?? "~/.chips-hermes/config.json"));
27785
+ var folder = dirname5(configPath);
27786
+ var statePath = join11(folder, "sessions.json");
27787
+ var state = existsSync9(statePath) ? JSON.parse(readFileSync9(statePath, "utf8")) : { sessions: {} };
27788
+ var saveState = (next) => {
27789
+ mkdirSync6(folder, { recursive: true, mode: 448 });
27790
+ writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
27791
+ `, { mode: 384 });
27792
+ };
27793
+ var log = (line) => process.stdout.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
27794
+ `);
27795
+ var freePort = () => new Promise((done, failed) => {
27796
+ const server = createServer2();
27797
+ server.once("error", failed);
27798
+ server.listen(0, "127.0.0.1", () => {
27799
+ const address = server.address();
27800
+ server.close(() => done(typeof address === "object" && address !== null ? address.port : 0));
27801
+ });
27802
+ });
26582
27803
  var config2;
27804
+ var root = hermesRoot();
27805
+ var fills = state.fills ??= {};
26583
27806
  try {
26584
27807
  config2 = readConfig(configPath);
26585
27808
  if (link !== void 0) {
26586
27809
  reportStep("checking", { detail: "Looking for its API server" });
26587
27810
  await checkHermes(config2);
26588
- reportStep("checked", { detail: checkedWords(config2) });
26589
27811
  }
27812
+ fills.secret ??= randomBytes2(32).toString("hex");
27813
+ fills.port = config2.hookPort ?? fills.port ?? hookPortIn(profileHome(root, config2.agents[0]?.address.profile)) ?? await freePort();
27814
+ saveState(state);
27815
+ const first = config2.agents[0];
27816
+ const gatewayUp = link !== void 0 || first !== void 0 && await new HermesClient(first.address).healthy();
27817
+ const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp });
27818
+ for (const line of hooks.lines) log(line);
27819
+ if (link !== void 0) reportStep("checked", { detail: checkedWords(config2, hooks.restart) });
26590
27820
  } catch (error62) {
26591
27821
  if (!(error62 instanceof SetupError)) throw error62;
26592
27822
  reportStep("failed", error62.words);
@@ -26595,10 +27825,6 @@ try {
26595
27825
  process.exit(1);
26596
27826
  }
26597
27827
  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
- `);
26602
27828
  var refused2 = (error62) => {
26603
27829
  if (!(error62 instanceof JoinError)) throw error62;
26604
27830
  process.stderr.write(`${error62.message}
@@ -26614,25 +27840,27 @@ var joined = await join3({
26614
27840
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
26615
27841
  }).catch(refused2);
26616
27842
  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: {} };
27843
+ var bridge;
26618
27844
  var chips = await connect({
26619
27845
  url: joined.url,
26620
27846
  token: joined.token,
26621
27847
  client: { name: "chips-hermes", version: "1.0.0" },
26622
- store: fileStore(join7(folder, "chips-state.json"))
27848
+ store: fileStore(join11(folder, "chips-state.json")),
27849
+ settings: settingsFor(() => bridge)
26623
27850
  });
26624
27851
  var git = await checkGit();
26625
27852
  if (!git.ok) log(`rooms' projects are off: ${git.why}`);
26626
- var bridge = new Bridge({
27853
+ bridge = new Bridge({
26627
27854
  folders: config2.folders,
26628
27855
  projects: git.ok,
26629
27856
  chips,
26630
27857
  agents: config2.agents,
26631
27858
  home: joined.spaceId,
26632
27859
  state,
26633
- saveState: (next) => writeFileSync4(statePath, `${JSON.stringify(next, null, 2)}
26634
- `, { mode: 384 }),
26635
- log
27860
+ saveState,
27861
+ log,
27862
+ hermesRoot: root,
27863
+ fills: { port: fills.port, secret: fills.secret }
26636
27864
  });
26637
27865
  await bridge.start();
26638
27866
  log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through Hermes`);
@@ -26646,7 +27874,7 @@ await helloOnce(joined, {
26646
27874
  }).catch((error62) => log(`hello not said: ${String(error62)}`));
26647
27875
  for (const signal of ["SIGINT", "SIGTERM"]) {
26648
27876
  process.on(signal, () => {
26649
- bridge.stop();
27877
+ bridge?.stop();
26650
27878
  chips.close();
26651
27879
  process.exit(0);
26652
27880
  });