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.
- package/dist/commands/browse.js +1 -3
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +11 -50
- package/dist/core/agents/coord-client.d.ts +15 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +45 -3
- package/dist/core/scratch/index.d.ts.map +1 -1
- package/dist/core/scratch/index.js +16 -6
- package/dist/lib/browser/client.d.ts +1 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +7 -1
- package/package.json +4 -4
- package/src/commands/browse.ts +1 -5
- package/src/commands/eml.ts +11 -62
- package/src/core/agents/coord-client.ts +47 -3
- package/src/core/scratch/index.ts +23 -6
- package/src/lib/browser/client.ts +6 -1
package/dist/commands/browse.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/commands/eml.js
CHANGED
|
@@ -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].
|
|
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].
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
text =
|
|
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
|
-
"&": "&",
|
|
304
|
-
"<": "<",
|
|
305
|
-
">": ">",
|
|
306
|
-
""": '"',
|
|
307
|
-
"'": "'",
|
|
308
|
-
"'": "'",
|
|
309
|
-
" ": " ",
|
|
310
|
-
" ": " ",
|
|
311
|
-
"–": "-",
|
|
312
|
-
"—": "-",
|
|
313
|
-
"«": "<<",
|
|
314
|
-
"»": ">>",
|
|
315
|
-
"•": "*",
|
|
316
|
-
"…": "...",
|
|
317
|
-
"©": "(c)",
|
|
318
|
-
"®": "(R)",
|
|
319
|
-
"™": "(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
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
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("<", "<").replaceAll(">", ">");
|
|
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;
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
|
100
|
-
if (
|
|
101
|
-
|
|
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 =
|
|
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.
|
|
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 --
|
|
161
|
-
"docs:build": "bun run --
|
|
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.
|
|
168
|
+
"js-yaml": "^4.3.0",
|
|
169
169
|
"linkedom": "^0.18.0",
|
|
170
170
|
"mailparser": "^3.9.14",
|
|
171
171
|
"pixelmatch": "^7.2.0",
|
package/src/commands/browse.ts
CHANGED
|
@@ -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) {
|
package/src/commands/eml.ts
CHANGED
|
@@ -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].
|
|
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].
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
"&": "&",
|
|
374
|
-
"<": "<",
|
|
375
|
-
">": ">",
|
|
376
|
-
""": '"',
|
|
377
|
-
"'": "'",
|
|
378
|
-
"'": "'",
|
|
379
|
-
" ": " ",
|
|
380
|
-
" ": " ",
|
|
381
|
-
"–": "-",
|
|
382
|
-
"—": "-",
|
|
383
|
-
"«": "<<",
|
|
384
|
-
"»": ">>",
|
|
385
|
-
"•": "*",
|
|
386
|
-
"…": "...",
|
|
387
|
-
"©": "(c)",
|
|
388
|
-
"®": "(R)",
|
|
389
|
-
"™": "(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
|
-
//
|
|
430
|
-
|
|
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("<", "<").replaceAll(">", ">");
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
148
|
-
if (
|
|
149
|
-
|
|
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 =
|
|
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
|
|