harnery 0.12.0 → 0.12.1

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.
@@ -396,9 +396,7 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
396
396
  body = await browser.htmlContent(opts.selector);
397
397
  }
398
398
  else if (opts.snapshot) {
399
- body = opts.selector
400
- ? await browser.evaluate(`(() => { const el = document.querySelector(${JSON.stringify(opts.selector)}); return el ? el.innerText : ''; })()`)
401
- : await browser.textSnapshot();
399
+ body = await browser.textSnapshot(opts.selector);
402
400
  }
403
401
  if (opts.json) {
404
402
  const result = {
@@ -1 +1 @@
1
- {"version":3,"file":"eml.d.ts","sourceRoot":"","sources":["../../src/commands/eml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;;;;;;GASG;AAEH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAkB5E;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAuCD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CAYjE;AA4VD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,GACZ,MAAM,CAyDR"}
1
+ {"version":3,"file":"eml.d.ts","sourceRoot":"","sources":["../../src/commands/eml.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;;;;;;GASG;AAEH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAkB5E;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAuCD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,EAAE,CAYjE;AAwSD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,GACZ,MAAM,CAyDR"}
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
+ import { parseHTML } from "linkedom";
2
3
  import { simpleParser } from "mailparser";
3
4
  export function registerEmlCommand(program, emit) {
4
5
  program
@@ -261,13 +262,13 @@ function parseAttribution(text) {
261
262
  const nameMatch = cleaned.match(/(?:AM|PM)\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
262
263
  let name = "Unknown";
263
264
  if (nameMatch) {
264
- name = nameMatch[1].replace(/<[^>]+>/g, "").trim();
265
+ name = nameMatch[1].trim();
265
266
  }
266
267
  else {
267
268
  // Try simpler pattern without AM/PM
268
269
  const simpleMatch = cleaned.match(/at\s+[\d:]+\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
269
270
  if (simpleMatch) {
270
- name = simpleMatch[1].replace(/<[^>]+>/g, "").trim();
271
+ name = simpleMatch[1].trim();
271
272
  }
272
273
  }
273
274
  return { name, date };
@@ -276,18 +277,11 @@ function parseAttribution(text) {
276
277
  * Strip HTML tags, decode entities, and normalize whitespace.
277
278
  */
278
279
  function stripHtml(html) {
279
- let text = html;
280
- // Replace <br>, <br/>, <br /> with newlines
281
- text = text.replace(/<br\s*\/?>/gi, "\n");
282
- // Replace block-level elements with newlines
283
- text = text.replace(/<\/(p|div|tr|li|h[1-6])>/gi, "\n");
284
- text = text.replace(/<(p|div|tr|li|h[1-6])\b[^>]*>/gi, "\n");
285
- // Replace <td> with tab
286
- text = text.replace(/<\/td>/gi, "\t");
287
- // Remove all remaining HTML tags
288
- text = text.replace(/<[^>]+>/g, "");
289
- // Decode HTML entities
290
- text = decodeEntities(text);
280
+ const { document } = parseHTML(`<html><body>${html}</body></html>`);
281
+ for (const unsafe of document.querySelectorAll("script, style, template, noscript")) {
282
+ unsafe.remove();
283
+ }
284
+ let text = document.body?.innerText ?? "";
291
285
  // Normalize whitespace (but preserve newlines)
292
286
  text = text
293
287
  .split("\n")
@@ -295,38 +289,6 @@ function stripHtml(html) {
295
289
  .join("\n");
296
290
  return text;
297
291
  }
298
- /**
299
- * Decode common HTML entities.
300
- */
301
- function decodeEntities(text) {
302
- const entities = {
303
- "&amp;": "&",
304
- "&lt;": "<",
305
- "&gt;": ">",
306
- "&quot;": '"',
307
- "&#39;": "'",
308
- "&apos;": "'",
309
- "&nbsp;": " ",
310
- "&#160;": " ",
311
- "&ndash;": "-",
312
- "&mdash;": "-",
313
- "&laquo;": "<<",
314
- "&raquo;": ">>",
315
- "&bull;": "*",
316
- "&hellip;": "...",
317
- "&copy;": "(c)",
318
- "&reg;": "(R)",
319
- "&trade;": "(TM)",
320
- };
321
- let result = text;
322
- for (const [entity, char] of Object.entries(entities)) {
323
- result = result.replaceAll(entity, char);
324
- }
325
- // Decode numeric entities (&#NNN; and &#xHHH;)
326
- result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 10)));
327
- result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 16)));
328
- return result;
329
- }
330
292
  /**
331
293
  * Clean up message body text.
332
294
  */
@@ -347,10 +309,9 @@ function cleanBody(text) {
347
309
  }
348
310
  }
349
311
  }
350
- // Remove stray partial HTML tags at the end of the body
351
- body = body.replace(/<\w+\s*$/, "");
352
- // Remove any remaining HTML tags that slipped through
353
- body = body.replace(/<[^>]+>/g, "");
312
+ // The renderer emits Markdown. Neutralize raw HTML in both DOM-derived and
313
+ // plain-text messages so decoded entities cannot become executable markup.
314
+ body = body.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
354
315
  // Collapse 3+ consecutive newlines into 2
355
316
  body = body.replace(/\n{3,}/g, "\n\n");
356
317
  return body.trim();
@@ -6,6 +6,13 @@
6
6
  * ppid map, so a single coord state can be observed and mutated from
7
7
  * either side without divergence.
8
8
  */
9
+ /**
10
+ * Instance IDs become coordination filenames, so only one portable basename
11
+ * alphabet is accepted at every filesystem boundary. UUIDs, test IDs, and
12
+ * legacy hex IDs all fit this contract; separators and dot segments do not.
13
+ */
14
+ export declare function isSafeInstanceId(value: unknown): value is string;
15
+ export declare function assertSafeInstanceId(value: unknown): asserts value is string;
9
16
  export interface Heartbeat {
10
17
  instance_id: string;
11
18
  name?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0IAA0I;IAC1I,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CA0C5C;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1D;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAkEA;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2BpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKlE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC"}
1
+ {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0IAA0I;IAC1I,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CA0C5C;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI1D;AAaD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAkEA;AA4CD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2BpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAKlE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC"}
@@ -9,6 +9,32 @@
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { existsSync, readdirSync, readFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
+ // NOTE: kept dependency-free (node builtins only); this file is vendored verbatim into
13
+ // a downstream consumer, so it cannot import the coordEnv helper.
14
+ const MAX_INSTANCE_ID_LENGTH = 128;
15
+ /**
16
+ * Instance IDs become coordination filenames, so only one portable basename
17
+ * alphabet is accepted at every filesystem boundary. UUIDs, test IDs, and
18
+ * legacy hex IDs all fit this contract; separators and dot segments do not.
19
+ */
20
+ export function isSafeInstanceId(value) {
21
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_INSTANCE_ID_LENGTH) {
22
+ return false;
23
+ }
24
+ for (let index = 0; index < value.length; index++) {
25
+ const code = value.charCodeAt(index);
26
+ const alpha = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
27
+ const digit = code >= 48 && code <= 57;
28
+ if (!alpha && !digit && code !== 45 && code !== 95)
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ export function assertSafeInstanceId(value) {
34
+ if (!isSafeInstanceId(value)) {
35
+ throw new Error("instance_id must be 1-128 ASCII letters, digits, hyphens, or underscores");
36
+ }
37
+ }
12
38
  /**
13
39
  * Resolve the monorepo root for coord-state purposes.
14
40
  *
@@ -303,7 +329,7 @@ export function resolveSingleActiveOwner(root) {
303
329
  * `.harnery/active/<id>.json` (the canonical location every reader expects).
304
330
  */
305
331
  export function readHeartbeat(instanceId) {
306
- if (!instanceId)
332
+ if (!isSafeInstanceId(instanceId))
307
333
  return null;
308
334
  const root = monorepoRoot();
309
335
  if (!root)
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/scratch/index.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,kBAAkB,iFAQrB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAC5C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,6DAA6D;AAC7D,eAAO,MAAM,eAAe,QACyF,CAAC;AAEtH,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAID,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAInC;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtD;AAeD,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,CAAC,GAAE,IAAiB,GAAG,MAAM,CAW1D;AAID,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CA+DtE;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAkBxD;AAmBD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,SAAoB,GAAG,SAAS,EAAE,CA2DrF;AAID,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,GACX,UAAU,CAgEZ;AAID,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhE;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,SAAyB,GAAG,MAAM,CAkBnE;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAajD;AAED,qDAAqD;AACrD,wBAAgB,YAAY,IAAI;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,EAAE,CAWF;AAID,wBAAgB,mBAAmB,IAAI,MAAM,CAM5C;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAIjE;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoB9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/scratch/index.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,kBAAkB,iFAQrB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAC3C,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAC5C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,6DAA6D;AAC7D,eAAO,MAAM,eAAe,QACyF,CAAC;AAEtH,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAID,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAInC;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGtD;AAeD,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,CAAC,GAAE,IAAiB,GAAG,MAAM,CAW1D;AAID,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAwEtE;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAkBxD;AAmBD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,SAAoB,GAAG,SAAS,EAAE,CA2DrF;AAID,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,GACX,UAAU,CAgEZ;AAID,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQhE;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,IAAI,SAAyB,GAAG,MAAM,CAkBnE;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAajD;AAED,qDAAqD;AACrD,wBAAgB,YAAY,IAAI;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,EAAE,CAWF;AAID,wBAAgB,mBAAmB,IAAI,MAAM,CAM5C;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAIjE;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoB9D"}
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { resolveMachineLabel } from "../../lib/machine.js";
4
- import { monorepoRoot, readHeartbeat, resolveOwner } from "../agents/index.js";
4
+ import { assertSafeInstanceId, monorepoRoot, readHeartbeat, resolveOwner, } from "../agents/index.js";
5
5
  /**
6
6
  * Agent scratchpad: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
7
7
  *
@@ -46,6 +46,7 @@ export function archiveDir() {
46
46
  return dir;
47
47
  }
48
48
  export function scratchPath(instanceId) {
49
+ assertSafeInstanceId(instanceId);
49
50
  return resolve(scratchDir(), `${instanceId}.md`);
50
51
  }
51
52
  // ─── Chicago time formatting ──────────────────────────────────────────────
@@ -96,9 +97,17 @@ export function parseScratch(path, content) {
96
97
  i++;
97
98
  break;
98
99
  }
99
- const m = line.match(/^(session_id|machine|started|last_updated):\s*(.+?)\s*$/);
100
- if (m) {
101
- header[m[1]] = m[2].trim();
100
+ const separator = line.indexOf(":");
101
+ if (separator > 0) {
102
+ const key = line.slice(0, separator);
103
+ if (key === "session_id" ||
104
+ key === "machine" ||
105
+ key === "started" ||
106
+ key === "last_updated") {
107
+ const value = line.slice(separator + 1).trim();
108
+ if (value)
109
+ header[key] = value;
110
+ }
102
111
  }
103
112
  }
104
113
  // ── entries ──
@@ -293,6 +302,7 @@ export function appendEntry(instanceId, category, body) {
293
302
  // ─── Archive lifecycle ────────────────────────────────────────────────────
294
303
  /** Move `<owner>.md` → `archived/<owner>-<ts>.md`. No-op if missing. Returns archive path (or null). */
295
304
  export function archiveScratch(instanceId) {
305
+ assertSafeInstanceId(instanceId);
296
306
  const src = scratchPath(instanceId);
297
307
  if (!existsSync(src))
298
308
  return null;
@@ -137,7 +137,7 @@ export declare class Browser {
137
137
  * on screen" signal for LLM iteration loops. For richer extraction, use
138
138
  * `htmlContent()` and pipe through a readability filter.
139
139
  */
140
- textSnapshot(): Promise<string>;
140
+ textSnapshot(selector?: string): Promise<string>;
141
141
  /** Raw outer HTML of the page (or a selector if provided). */
142
142
  htmlContent(selector?: string): Promise<string>;
143
143
  click(selector: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/client.ts"],"names":[],"mappings":"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"}
@@ -157,8 +157,14 @@ export class Browser {
157
157
  * on screen" signal for LLM iteration loops. For richer extraction, use
158
158
  * `htmlContent()` and pipe through a readability filter.
159
159
  */
160
- async textSnapshot() {
160
+ async textSnapshot(selector) {
161
161
  const page = this.currentPage;
162
+ if (selector) {
163
+ const el = await page.$(selector);
164
+ if (!el)
165
+ throw new Error(`Selector matched nothing: ${selector}`);
166
+ return await el.evaluate((node) => node.innerText ?? "");
167
+ }
162
168
  return await page.evaluate(() => document.body?.innerText ?? "");
163
169
  }
164
170
  /** Raw outer HTML of the page (or a selector if provided). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -157,15 +157,15 @@
157
157
  "test:web": "cd web && bun install && bun test",
158
158
  "test:integration": "bash tests/integration/run.sh",
159
159
  "smoke": "node scripts/smoke-test.mjs",
160
- "docs:dev": "bun run --bun --cwd docs dev",
161
- "docs:build": "bun run --bun --cwd docs build",
160
+ "docs:dev": "bun run --cwd docs dev",
161
+ "docs:build": "bun run --cwd docs build",
162
162
  "brand:showcase": "node scripts/build-brand-showcase.mjs"
163
163
  },
164
164
  "dependencies": {
165
165
  "@mozilla/readability": "^0.6.0",
166
166
  "commander": "^14.0.3",
167
167
  "gpt-tokenizer": "^3.4.0",
168
- "js-yaml": "^4.1.1",
168
+ "js-yaml": "^4.3.0",
169
169
  "linkedom": "^0.18.0",
170
170
  "mailparser": "^3.9.14",
171
171
  "pixelmatch": "^7.2.0",
@@ -650,11 +650,7 @@ async function runPrintMode(
650
650
  if (opts.html) {
651
651
  body = await browser.htmlContent(opts.selector);
652
652
  } else if (opts.snapshot) {
653
- body = opts.selector
654
- ? await browser.evaluate<string>(
655
- `(() => { const el = document.querySelector(${JSON.stringify(opts.selector)}); return el ? el.innerText : ''; })()`,
656
- )
657
- : await browser.textSnapshot();
653
+ body = await browser.textSnapshot(opts.selector);
658
654
  }
659
655
 
660
656
  if (opts.json) {
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import type { Command } from "commander";
3
+ import { parseHTML } from "linkedom";
3
4
  import { type ParsedMail, simpleParser } from "mailparser";
4
5
  import type { EmitContext } from "../commander.ts";
5
6
 
@@ -322,12 +323,12 @@ function parseAttribution(text: string): { name: string; date: Date | null } {
322
323
  const nameMatch = cleaned.match(/(?:AM|PM)\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
323
324
  let name = "Unknown";
324
325
  if (nameMatch) {
325
- name = nameMatch[1].replace(/<[^>]+>/g, "").trim();
326
+ name = nameMatch[1].trim();
326
327
  } else {
327
328
  // Try simpler pattern without AM/PM
328
329
  const simpleMatch = cleaned.match(/at\s+[\d:]+\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
329
330
  if (simpleMatch) {
330
- name = simpleMatch[1].replace(/<[^>]+>/g, "").trim();
331
+ name = simpleMatch[1].trim();
331
332
  }
332
333
  }
333
334
 
@@ -338,23 +339,11 @@ function parseAttribution(text: string): { name: string; date: Date | null } {
338
339
  * Strip HTML tags, decode entities, and normalize whitespace.
339
340
  */
340
341
  function stripHtml(html: string): string {
341
- let text = html;
342
-
343
- // Replace <br>, <br/>, <br /> with newlines
344
- text = text.replace(/<br\s*\/?>/gi, "\n");
345
-
346
- // Replace block-level elements with newlines
347
- text = text.replace(/<\/(p|div|tr|li|h[1-6])>/gi, "\n");
348
- text = text.replace(/<(p|div|tr|li|h[1-6])\b[^>]*>/gi, "\n");
349
-
350
- // Replace <td> with tab
351
- text = text.replace(/<\/td>/gi, "\t");
352
-
353
- // Remove all remaining HTML tags
354
- text = text.replace(/<[^>]+>/g, "");
355
-
356
- // Decode HTML entities
357
- text = decodeEntities(text);
342
+ const { document } = parseHTML(`<html><body>${html}</body></html>`);
343
+ for (const unsafe of document.querySelectorAll("script, style, template, noscript")) {
344
+ unsafe.remove();
345
+ }
346
+ let text = document.body?.innerText ?? "";
358
347
 
359
348
  // Normalize whitespace (but preserve newlines)
360
349
  text = text
@@ -365,44 +354,6 @@ function stripHtml(html: string): string {
365
354
  return text;
366
355
  }
367
356
 
368
- /**
369
- * Decode common HTML entities.
370
- */
371
- function decodeEntities(text: string): string {
372
- const entities: Record<string, string> = {
373
- "&amp;": "&",
374
- "&lt;": "<",
375
- "&gt;": ">",
376
- "&quot;": '"',
377
- "&#39;": "'",
378
- "&apos;": "'",
379
- "&nbsp;": " ",
380
- "&#160;": " ",
381
- "&ndash;": "-",
382
- "&mdash;": "-",
383
- "&laquo;": "<<",
384
- "&raquo;": ">>",
385
- "&bull;": "*",
386
- "&hellip;": "...",
387
- "&copy;": "(c)",
388
- "&reg;": "(R)",
389
- "&trade;": "(TM)",
390
- };
391
-
392
- let result = text;
393
- for (const [entity, char] of Object.entries(entities)) {
394
- result = result.replaceAll(entity, char);
395
- }
396
-
397
- // Decode numeric entities (&#NNN; and &#xHHH;)
398
- result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 10)));
399
- result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) =>
400
- String.fromCharCode(Number.parseInt(code, 16)),
401
- );
402
-
403
- return result;
404
- }
405
-
406
357
  /**
407
358
  * Clean up message body text.
408
359
  */
@@ -426,11 +377,9 @@ function cleanBody(text: string): string {
426
377
  }
427
378
  }
428
379
 
429
- // Remove stray partial HTML tags at the end of the body
430
- body = body.replace(/<\w+\s*$/, "");
431
-
432
- // Remove any remaining HTML tags that slipped through
433
- body = body.replace(/<[^>]+>/g, "");
380
+ // The renderer emits Markdown. Neutralize raw HTML in both DOM-derived and
381
+ // plain-text messages so decoded entities cannot become executable markup.
382
+ body = body.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
434
383
 
435
384
  // Collapse 3+ consecutive newlines into 2
436
385
  body = body.replace(/\n{3,}/g, "\n\n");
@@ -10,9 +10,36 @@
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { existsSync, readdirSync, readFileSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
+
13
14
  // NOTE: kept dependency-free (node builtins only); this file is vendored verbatim into
14
15
  // a downstream consumer, so it cannot import the coordEnv helper.
15
16
 
17
+ const MAX_INSTANCE_ID_LENGTH = 128;
18
+
19
+ /**
20
+ * Instance IDs become coordination filenames, so only one portable basename
21
+ * alphabet is accepted at every filesystem boundary. UUIDs, test IDs, and
22
+ * legacy hex IDs all fit this contract; separators and dot segments do not.
23
+ */
24
+ export function isSafeInstanceId(value: unknown): value is string {
25
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_INSTANCE_ID_LENGTH) {
26
+ return false;
27
+ }
28
+ for (let index = 0; index < value.length; index++) {
29
+ const code = value.charCodeAt(index);
30
+ const alpha = (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
31
+ const digit = code >= 48 && code <= 57;
32
+ if (!alpha && !digit && code !== 45 && code !== 95) return false;
33
+ }
34
+ return true;
35
+ }
36
+
37
+ export function assertSafeInstanceId(value: unknown): asserts value is string {
38
+ if (!isSafeInstanceId(value)) {
39
+ throw new Error("instance_id must be 1-128 ASCII letters, digits, hyphens, or underscores");
40
+ }
41
+ }
42
+
16
43
  export interface Heartbeat {
17
44
  instance_id: string;
18
45
  name?: string;
@@ -339,7 +366,7 @@ export function resolveSingleActiveOwner(root: string): string | null {
339
366
  * `.harnery/active/<id>.json` (the canonical location every reader expects).
340
367
  */
341
368
  export function readHeartbeat(instanceId: string): Heartbeat | null {
342
- if (!instanceId) return null;
369
+ if (!isSafeInstanceId(instanceId)) return null;
343
370
  const root = monorepoRoot();
344
371
  if (!root) return null;
345
372
  return readJsonHeartbeatFile(resolve(root, ".harnery", "active", `${instanceId}.json`));
@@ -10,7 +10,12 @@ import {
10
10
  } from "node:fs";
11
11
  import { dirname, resolve } from "node:path";
12
12
  import { resolveMachineLabel } from "../../lib/machine.ts";
13
- import { monorepoRoot, readHeartbeat, resolveOwner } from "../agents/index.ts";
13
+ import {
14
+ assertSafeInstanceId,
15
+ monorepoRoot,
16
+ readHeartbeat,
17
+ resolveOwner,
18
+ } from "../agents/index.ts";
14
19
 
15
20
  /**
16
21
  * Agent scratchpad: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
@@ -88,6 +93,7 @@ export function archiveDir(): string {
88
93
  }
89
94
 
90
95
  export function scratchPath(instanceId: string): string {
96
+ assertSafeInstanceId(instanceId);
91
97
  return resolve(scratchDir(), `${instanceId}.md`);
92
98
  }
93
99
 
@@ -144,9 +150,18 @@ export function parseScratch(path: string, content: string): ScratchDoc {
144
150
  i++;
145
151
  break;
146
152
  }
147
- const m = line.match(/^(session_id|machine|started|last_updated):\s*(.+?)\s*$/);
148
- if (m) {
149
- (header as unknown as Record<string, string>)[m[1]] = m[2].trim();
153
+ const separator = line.indexOf(":");
154
+ if (separator > 0) {
155
+ const key = line.slice(0, separator);
156
+ if (
157
+ key === "session_id" ||
158
+ key === "machine" ||
159
+ key === "started" ||
160
+ key === "last_updated"
161
+ ) {
162
+ const value = line.slice(separator + 1).trim();
163
+ if (value) (header as unknown as Record<string, string>)[key] = value;
164
+ }
150
165
  }
151
166
  }
152
167
 
@@ -366,6 +381,7 @@ export function appendEntry(
366
381
 
367
382
  /** Move `<owner>.md` → `archived/<owner>-<ts>.md`. No-op if missing. Returns archive path (or null). */
368
383
  export function archiveScratch(instanceId: string): string | null {
384
+ assertSafeInstanceId(instanceId);
369
385
  const src = scratchPath(instanceId);
370
386
  if (!existsSync(src)) return null;
371
387
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
@@ -285,8 +285,13 @@ export class Browser {
285
285
  * on screen" signal for LLM iteration loops. For richer extraction, use
286
286
  * `htmlContent()` and pipe through a readability filter.
287
287
  */
288
- async textSnapshot(): Promise<string> {
288
+ async textSnapshot(selector?: string): Promise<string> {
289
289
  const page = this.currentPage;
290
+ if (selector) {
291
+ const el = await page.$(selector);
292
+ if (!el) throw new Error(`Selector matched nothing: ${selector}`);
293
+ return await el.evaluate((node) => (node as HTMLElement).innerText ?? "");
294
+ }
290
295
  return await page.evaluate(() => document.body?.innerText ?? "");
291
296
  }
292
297