harnery 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,21 @@
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;
16
+ /**
17
+ * Resolve one direct-child filename beneath a trusted coordination directory.
18
+ *
19
+ * This is a second boundary behind the instance-ID allowlist. Normalizing the
20
+ * candidate and proving its directory prefix prevents traversal even if a new
21
+ * caller constructs a filename from a different untrusted source.
22
+ */
23
+ export declare function resolveContainedFile(directory: string, fileName: string): string;
9
24
  export interface Heartbeat {
10
25
  instance_id: string;
11
26
  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;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhF;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,CAMlE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC"}
@@ -8,7 +8,48 @@
8
8
  */
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { existsSync, readdirSync, readFileSync } from "node:fs";
11
- import { resolve } from "node:path";
11
+ import { dirname, resolve, sep } 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
+ }
38
+ /**
39
+ * Resolve one direct-child filename beneath a trusted coordination directory.
40
+ *
41
+ * This is a second boundary behind the instance-ID allowlist. Normalizing the
42
+ * candidate and proving its directory prefix prevents traversal even if a new
43
+ * caller constructs a filename from a different untrusted source.
44
+ */
45
+ export function resolveContainedFile(directory, fileName) {
46
+ const root = resolve(directory);
47
+ const candidate = resolve(root, fileName);
48
+ if (!candidate.startsWith(`${root}${sep}`) || dirname(candidate) !== root) {
49
+ throw new Error("coordination filename must resolve directly beneath its root");
50
+ }
51
+ return candidate;
52
+ }
12
53
  /**
13
54
  * Resolve the monorepo root for coord-state purposes.
14
55
  *
@@ -303,12 +344,13 @@ export function resolveSingleActiveOwner(root) {
303
344
  * `.harnery/active/<id>.json` (the canonical location every reader expects).
304
345
  */
305
346
  export function readHeartbeat(instanceId) {
306
- if (!instanceId)
347
+ if (!isSafeInstanceId(instanceId))
307
348
  return null;
308
349
  const root = monorepoRoot();
309
350
  if (!root)
310
351
  return null;
311
- return readJsonHeartbeatFile(resolve(root, ".harnery", "active", `${instanceId}.json`));
352
+ const path = resolveContainedFile(resolve(root, ".harnery", "active"), `${instanceId}.json`);
353
+ return readJsonHeartbeatFile(path);
312
354
  }
313
355
  function readJsonHeartbeatFile(path) {
314
356
  if (!existsSync(path))
@@ -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":"AAoBA;;;;;;;;;;;;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, resolveContainedFile, resolveOwner, } from "../agents/index.js";
5
5
  /**
6
6
  * Agent scratchpad: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
7
7
  *
@@ -46,7 +46,8 @@ export function archiveDir() {
46
46
  return dir;
47
47
  }
48
48
  export function scratchPath(instanceId) {
49
- return resolve(scratchDir(), `${instanceId}.md`);
49
+ assertSafeInstanceId(instanceId);
50
+ return resolveContainedFile(scratchDir(), `${instanceId}.md`);
50
51
  }
51
52
  // ─── Chicago time formatting ──────────────────────────────────────────────
52
53
  const CHICAGO_FORMATTER = new Intl.DateTimeFormat("en-US", {
@@ -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,11 +302,12 @@ 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;
299
309
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
300
- const dest = resolve(archiveDir(), `${instanceId}-${ts}.md`);
310
+ const dest = resolveContainedFile(archiveDir(), `${instanceId}-${ts}.md`);
301
311
  renameSync(src, dest);
302
312
  return dest;
303
313
  }
@@ -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.2",
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");
@@ -9,10 +9,53 @@
9
9
 
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { existsSync, readdirSync, readFileSync } from "node:fs";
12
- import { resolve } from "node:path";
12
+ import { dirname, resolve, sep } 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
+
43
+ /**
44
+ * Resolve one direct-child filename beneath a trusted coordination directory.
45
+ *
46
+ * This is a second boundary behind the instance-ID allowlist. Normalizing the
47
+ * candidate and proving its directory prefix prevents traversal even if a new
48
+ * caller constructs a filename from a different untrusted source.
49
+ */
50
+ export function resolveContainedFile(directory: string, fileName: string): string {
51
+ const root = resolve(directory);
52
+ const candidate = resolve(root, fileName);
53
+ if (!candidate.startsWith(`${root}${sep}`) || dirname(candidate) !== root) {
54
+ throw new Error("coordination filename must resolve directly beneath its root");
55
+ }
56
+ return candidate;
57
+ }
58
+
16
59
  export interface Heartbeat {
17
60
  instance_id: string;
18
61
  name?: string;
@@ -339,10 +382,11 @@ export function resolveSingleActiveOwner(root: string): string | null {
339
382
  * `.harnery/active/<id>.json` (the canonical location every reader expects).
340
383
  */
341
384
  export function readHeartbeat(instanceId: string): Heartbeat | null {
342
- if (!instanceId) return null;
385
+ if (!isSafeInstanceId(instanceId)) return null;
343
386
  const root = monorepoRoot();
344
387
  if (!root) return null;
345
- return readJsonHeartbeatFile(resolve(root, ".harnery", "active", `${instanceId}.json`));
388
+ const path = resolveContainedFile(resolve(root, ".harnery", "active"), `${instanceId}.json`);
389
+ return readJsonHeartbeatFile(path);
346
390
  }
347
391
 
348
392
  function readJsonHeartbeatFile(path: string): Heartbeat | null {
@@ -10,7 +10,13 @@ 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
+ resolveContainedFile,
18
+ resolveOwner,
19
+ } from "../agents/index.ts";
14
20
 
15
21
  /**
16
22
  * Agent scratchpad: per-agent markdown journal at `.harnery/scratch/<instance_id>.md`.
@@ -88,7 +94,8 @@ export function archiveDir(): string {
88
94
  }
89
95
 
90
96
  export function scratchPath(instanceId: string): string {
91
- return resolve(scratchDir(), `${instanceId}.md`);
97
+ assertSafeInstanceId(instanceId);
98
+ return resolveContainedFile(scratchDir(), `${instanceId}.md`);
92
99
  }
93
100
 
94
101
  // ─── Chicago time formatting ──────────────────────────────────────────────
@@ -144,9 +151,18 @@ export function parseScratch(path: string, content: string): ScratchDoc {
144
151
  i++;
145
152
  break;
146
153
  }
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();
154
+ const separator = line.indexOf(":");
155
+ if (separator > 0) {
156
+ const key = line.slice(0, separator);
157
+ if (
158
+ key === "session_id" ||
159
+ key === "machine" ||
160
+ key === "started" ||
161
+ key === "last_updated"
162
+ ) {
163
+ const value = line.slice(separator + 1).trim();
164
+ if (value) (header as unknown as Record<string, string>)[key] = value;
165
+ }
150
166
  }
151
167
  }
152
168
 
@@ -366,10 +382,11 @@ export function appendEntry(
366
382
 
367
383
  /** Move `<owner>.md` → `archived/<owner>-<ts>.md`. No-op if missing. Returns archive path (or null). */
368
384
  export function archiveScratch(instanceId: string): string | null {
385
+ assertSafeInstanceId(instanceId);
369
386
  const src = scratchPath(instanceId);
370
387
  if (!existsSync(src)) return null;
371
388
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
372
- const dest = resolve(archiveDir(), `${instanceId}-${ts}.md`);
389
+ const dest = resolveContainedFile(archiveDir(), `${instanceId}-${ts}.md`);
373
390
  renameSync(src, dest);
374
391
  return dest;
375
392
  }
@@ -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