harlay 0.3.1 → 0.4.0
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/README.md +2 -2
- package/app/dist/main.cjs +1268 -205
- package/app/renderer/overlay.js +4 -3
- package/app/renderer/popover.css +13 -5
- package/app/renderer/popover.js +101 -37
- package/dist/harlay.mjs +409 -81
- package/package.json +1 -1
package/app/dist/main.cjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
var import_electron2 = require("electron");
|
|
5
|
-
var
|
|
5
|
+
var import_promises20 = require("node:fs/promises");
|
|
6
6
|
var import_node_os3 = require("node:os");
|
|
7
|
-
var
|
|
7
|
+
var import_node_path24 = require("node:path");
|
|
8
8
|
|
|
9
9
|
// ../../packages/core/src/types.ts
|
|
10
10
|
var TOOLS = ["claude-code", "codex", "cursor"];
|
|
@@ -12,6 +12,20 @@ var TOOL_LABEL = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor
|
|
|
12
12
|
function isTool(v) {
|
|
13
13
|
return v === "claude-code" || v === "codex" || v === "cursor";
|
|
14
14
|
}
|
|
15
|
+
var WEB_CHATS = ["claude-web", "chatgpt"];
|
|
16
|
+
var WEB_LABEL = { "claude-web": "claude.ai", chatgpt: "ChatGPT" };
|
|
17
|
+
function isWebChat(v) {
|
|
18
|
+
return v === "claude-web" || v === "chatgpt";
|
|
19
|
+
}
|
|
20
|
+
function isTarget(v) {
|
|
21
|
+
return isTool(v) || isWebChat(v);
|
|
22
|
+
}
|
|
23
|
+
function targetLabel(t) {
|
|
24
|
+
return isWebChat(t) ? WEB_LABEL[t] : TOOL_LABEL[t];
|
|
25
|
+
}
|
|
26
|
+
function speakerLabel(t) {
|
|
27
|
+
return t === "claude-web" ? "Claude" : targetLabel(t);
|
|
28
|
+
}
|
|
15
29
|
|
|
16
30
|
// ../../packages/core/src/errors.ts
|
|
17
31
|
var HarlayError = class extends Error {
|
|
@@ -126,7 +140,7 @@ function slugify(s, max = 40) {
|
|
|
126
140
|
|
|
127
141
|
// ../../packages/core/src/sources/claude.ts
|
|
128
142
|
var import_promises5 = require("node:fs/promises");
|
|
129
|
-
var
|
|
143
|
+
var import_node_path6 = require("node:path");
|
|
130
144
|
|
|
131
145
|
// ../../packages/core/src/util/files.ts
|
|
132
146
|
var import_node_fs = require("node:fs");
|
|
@@ -188,18 +202,18 @@ function blockText(content) {
|
|
|
188
202
|
if (typeof content === "string") return content;
|
|
189
203
|
if (!Array.isArray(content)) return "";
|
|
190
204
|
const parts = [];
|
|
191
|
-
for (const b of content) if (isObj(b) && b.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
205
|
+
for (const b of content) if (isObj(b) && (b.type === "text" || b.type === "Text") && typeof b.text === "string") parts.push(b.text);
|
|
192
206
|
return parts.join("\n");
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
// ../../packages/core/src/sources/folder.ts
|
|
196
210
|
var import_promises4 = require("node:fs/promises");
|
|
197
|
-
var
|
|
211
|
+
var import_node_path5 = require("node:path");
|
|
198
212
|
|
|
199
213
|
// ../../packages/core/src/brief/write.ts
|
|
200
214
|
var import_node_crypto = require("node:crypto");
|
|
201
215
|
var import_promises3 = require("node:fs/promises");
|
|
202
|
-
var
|
|
216
|
+
var import_node_path4 = require("node:path");
|
|
203
217
|
|
|
204
218
|
// ../../packages/core/src/launch/launchers.ts
|
|
205
219
|
var import_node_child_process = require("node:child_process");
|
|
@@ -351,6 +365,7 @@ var Redactor = class {
|
|
|
351
365
|
};
|
|
352
366
|
|
|
353
367
|
// ../../packages/core/src/brief/record.ts
|
|
368
|
+
var import_node_path3 = require("node:path");
|
|
354
369
|
var RECORD_SCHEMA = "harlay/handoff@6";
|
|
355
370
|
var MESSAGE_SCHEMAS = /* @__PURE__ */ new Set([RECORD_SCHEMA, "harlay/handoff@5"]);
|
|
356
371
|
var OLD_SCHEMAS = /* @__PURE__ */ new Set(["harlay/handoff@4", "harlay/handoff@3", "harlay/handoff@2"]);
|
|
@@ -365,7 +380,8 @@ var REASON_TEXT = {
|
|
|
365
380
|
manual: "the user moved it here"
|
|
366
381
|
};
|
|
367
382
|
function harlayAbout(from, reason, root) {
|
|
368
|
-
|
|
383
|
+
const where = isWebChat(from) ? `chatting with ${targetLabel(from)} in their browser (this project is ${root})` : `working with ${targetLabel(from)} in ${root}`;
|
|
384
|
+
return `Harlay context: the user was ${where}, and that chat stopped (${REASON_TEXT[reason]}). This file is what happened there: everything the user typed, word for word and oldest first (task.userMessages), where it stopped and their latest request (state), a command that still fails (lastFailure), how each command went (results), what the previous agent read, was given and looked up (references), and the recent activity and diff.`;
|
|
369
385
|
}
|
|
370
386
|
function harlayInstructions(root, createdAt, head) {
|
|
371
387
|
return [
|
|
@@ -377,6 +393,18 @@ function harlayInstructions(root, createdAt, head) {
|
|
|
377
393
|
"Open what references lists only if you need it."
|
|
378
394
|
];
|
|
379
395
|
}
|
|
396
|
+
function webAbout(from, reason, root) {
|
|
397
|
+
if (isWebChat(from))
|
|
398
|
+
return `Harlay context: the user was chatting with ${targetLabel(from)}, and that chat stopped (${REASON_TEXT[reason]}). This file is that conversation: everything the user typed, word for word and oldest first (task.userMessages), the last reply (state.whereItStopped) and the end of the conversation (recentActivity).`;
|
|
399
|
+
return `Harlay context: the user was working with ${targetLabel(from)} on a coding task in ${(0, import_node_path3.basename)(root)}, and that chat stopped (${REASON_TEXT[reason]}). This file is what happened there: everything the user typed, word for word and oldest first (task.userMessages), where it stopped (state), a command that still fails (lastFailure), the files changed (files), and the recent activity and diff.`;
|
|
400
|
+
}
|
|
401
|
+
function webInstructions(from) {
|
|
402
|
+
return [
|
|
403
|
+
"Reply with a short summary of where things stand (two or three lines), then carry on: if the user's last message has no answer yet, answer it; otherwise ask what they want to do next.",
|
|
404
|
+
"What the user asked for and decided is in task.userMessages, in their own words: follow it.",
|
|
405
|
+
...isWebChat(from) ? [] : ["You can't see the user's files: ask them to paste any you need."]
|
|
406
|
+
];
|
|
407
|
+
}
|
|
380
408
|
var LINE_MAX = 500;
|
|
381
409
|
function lines(s) {
|
|
382
410
|
const out = [];
|
|
@@ -396,27 +424,27 @@ function lines(s) {
|
|
|
396
424
|
}
|
|
397
425
|
function tailLines(s, max) {
|
|
398
426
|
if (s.length <= max) return lines(s);
|
|
399
|
-
const
|
|
400
|
-
const nl =
|
|
401
|
-
return ["\u2026(earlier lines cut)", ...lines(nl === -1 ?
|
|
427
|
+
const cut2 = s.slice(s.length - max);
|
|
428
|
+
const nl = cut2.indexOf("\n");
|
|
429
|
+
return ["\u2026(earlier lines cut)", ...lines(nl === -1 ? cut2 : cut2.slice(nl + 1))];
|
|
402
430
|
}
|
|
403
431
|
function headLines(s, max, more) {
|
|
404
432
|
if (s.length <= max) return lines(s.replace(/\n$/, ""));
|
|
405
|
-
const
|
|
406
|
-
const nl =
|
|
407
|
-
return [...lines(nl === -1 ?
|
|
433
|
+
const cut2 = s.slice(0, max);
|
|
434
|
+
const nl = cut2.lastIndexOf("\n");
|
|
435
|
+
return [...lines(nl === -1 ? cut2 : cut2.slice(0, nl)), more];
|
|
408
436
|
}
|
|
409
437
|
function buildRecord(d, id, previousId, delivery, r) {
|
|
410
438
|
const a = (s) => wellFormed(r.apply(stripControl(s)));
|
|
411
439
|
const files = d.git?.files ?? [];
|
|
412
440
|
const failure = d.lastFailure ? a(d.lastFailure.output).split("\n").slice(-FAILURE_OUTPUT_LINES).join("\n") : "";
|
|
413
441
|
return {
|
|
414
|
-
harlay: harlayAbout(d.from.tool, d.from.reason, d.workdir),
|
|
442
|
+
harlay: isWebChat(delivery.target) ? webAbout(d.from.tool, d.from.reason, d.workdir) : harlayAbout(d.from.tool, d.from.reason, d.workdir),
|
|
415
443
|
// The id stays near the start: an agent may read only the file's first lines, and its copy is matched by the id.
|
|
416
444
|
schema: RECORD_SCHEMA,
|
|
417
445
|
id,
|
|
418
446
|
createdAt: d.createdAt.toISOString(),
|
|
419
|
-
instructions: harlayInstructions(d.workdir, d.createdAt.toISOString(), d.git?.head ?? null),
|
|
447
|
+
instructions: isWebChat(delivery.target) ? webInstructions(d.from.tool) : harlayInstructions(d.workdir, d.createdAt.toISOString(), d.git?.head ?? null),
|
|
420
448
|
task: {
|
|
421
449
|
title: a(d.title),
|
|
422
450
|
originalGoal: d.originalGoal === null ? null : clipWords(a(d.originalGoal), 60),
|
|
@@ -471,7 +499,7 @@ function messagesOf(schema, task) {
|
|
|
471
499
|
function validateRecord(raw) {
|
|
472
500
|
if (!isObj2(raw) || !(MESSAGE_SCHEMAS.has(raw.schema) || OLD_SCHEMAS.has(raw.schema)) || !isStr(raw.id) || !isStr(raw.createdAt)) return null;
|
|
473
501
|
const { task, from } = raw;
|
|
474
|
-
if (!isObj2(from) || !
|
|
502
|
+
if (!isObj2(from) || !isTarget(from.tool)) return null;
|
|
475
503
|
if (!isObj2(task) || !isStr(task.title) || !(task.originalGoal === null || isStr(task.originalGoal))) return null;
|
|
476
504
|
const messages = messagesOf(raw.schema, task);
|
|
477
505
|
if (!messages) return null;
|
|
@@ -486,16 +514,16 @@ function validateRecord(raw) {
|
|
|
486
514
|
|
|
487
515
|
// ../../packages/core/src/brief/write.ts
|
|
488
516
|
function handoffStore(home2) {
|
|
489
|
-
return (0,
|
|
517
|
+
return (0, import_node_path4.join)(home2, "Library", "Application Support", "Harlay", "handoffs");
|
|
490
518
|
}
|
|
491
519
|
function repoFolder(store, root) {
|
|
492
|
-
return (0,
|
|
520
|
+
return (0, import_node_path4.join)(store, `${slugify((0, import_node_path4.basename)(root), 30)}-${(0, import_node_crypto.createHash)("sha256").update(root).digest("hex").slice(0, 8)}`);
|
|
493
521
|
}
|
|
494
522
|
var LEGACY_FILE = "handoff.harlay";
|
|
495
523
|
async function readHandoffState(workdir) {
|
|
496
524
|
let raw;
|
|
497
525
|
try {
|
|
498
|
-
raw = JSON.parse(await (0, import_promises3.readFile)((0,
|
|
526
|
+
raw = JSON.parse(await (0, import_promises3.readFile)((0, import_node_path4.join)(workdir, ".harlay", "state.json"), "utf8"));
|
|
499
527
|
} catch {
|
|
500
528
|
return null;
|
|
501
529
|
}
|
|
@@ -540,7 +568,7 @@ async function newestIn(dir) {
|
|
|
540
568
|
let best = null;
|
|
541
569
|
for (const name of names) {
|
|
542
570
|
if (!name.endsWith(".harlay") || name.startsWith(".")) continue;
|
|
543
|
-
const file = (0,
|
|
571
|
+
const file = (0, import_node_path4.join)(dir, name);
|
|
544
572
|
try {
|
|
545
573
|
const at = (await (0, import_promises3.stat)(file)).mtimeMs;
|
|
546
574
|
if (!best || at > best.at) best = { file, at };
|
|
@@ -551,7 +579,7 @@ async function newestIn(dir) {
|
|
|
551
579
|
}
|
|
552
580
|
async function readPreviousHandoff(store, workdir) {
|
|
553
581
|
const newest = await newestIn(repoFolder(store, workdir));
|
|
554
|
-
for (const file of newest ? [newest] : [(0,
|
|
582
|
+
for (const file of newest ? [newest] : [(0, import_node_path4.join)(workdir, LEGACY_FILE), (0, import_node_path4.join)(workdir, ".harlay", "handoff.json")]) {
|
|
555
583
|
const raw = await readIfExists(file);
|
|
556
584
|
if (raw === null) continue;
|
|
557
585
|
const rec = parseRecord(raw);
|
|
@@ -567,7 +595,7 @@ function addCounts(a, b) {
|
|
|
567
595
|
}
|
|
568
596
|
var writing = /* @__PURE__ */ new Map();
|
|
569
597
|
function writeHandoff(d, o) {
|
|
570
|
-
const dir = repoFolder(o.store, d.workdir);
|
|
598
|
+
const dir = o.dir ?? repoFolder(o.store, d.workdir);
|
|
571
599
|
const run = (writing.get(dir) ?? Promise.resolve()).catch(() => void 0).then(() => write(d, o, dir));
|
|
572
600
|
writing.set(dir, run);
|
|
573
601
|
void run.finally(() => {
|
|
@@ -585,14 +613,14 @@ async function exists(file) {
|
|
|
585
613
|
}
|
|
586
614
|
async function write(d, o, dir) {
|
|
587
615
|
await (0, import_promises3.mkdir)(dir, { recursive: true });
|
|
588
|
-
const tmp = (0,
|
|
616
|
+
const tmp = (0, import_node_path4.join)(dir, `.tmp-${process.pid}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}.harlay`);
|
|
589
617
|
try {
|
|
590
618
|
for (let n = 1; ; n++) {
|
|
591
619
|
const id = n === 1 ? d.baseId : `${d.baseId}-${n}`;
|
|
592
|
-
const path = (0,
|
|
620
|
+
const path = (0, import_node_path4.join)(dir, `${id}.harlay`);
|
|
593
621
|
if (await exists(path)) continue;
|
|
594
622
|
const r = new Redactor();
|
|
595
|
-
const prompt = launchPrompt(path);
|
|
623
|
+
const prompt = o.prompt ?? launchPrompt(path);
|
|
596
624
|
const record = buildRecord(d, id, o.previousId, { target: o.target, mode: o.mode, prompt }, r);
|
|
597
625
|
const text = JSON.stringify(record, null, 2) + "\n";
|
|
598
626
|
await (0, import_promises3.writeFile)(tmp, text);
|
|
@@ -625,7 +653,7 @@ async function handoffRepo(file) {
|
|
|
625
653
|
}
|
|
626
654
|
const root = raw === null ? null : parseRecord(raw)?.root;
|
|
627
655
|
if (root && await isDir(root)) return root;
|
|
628
|
-
return (0,
|
|
656
|
+
return (0, import_node_path5.basename)(file) === LEGACY_FILE && await isDir((0, import_node_path5.dirname)(file)) ? (0, import_node_path5.dirname)(file) : null;
|
|
629
657
|
}
|
|
630
658
|
async function sessionFolder(opened, firstPrompt2) {
|
|
631
659
|
const file = firstPrompt2 === null ? null : harlayFileIn(firstPrompt2);
|
|
@@ -813,7 +841,7 @@ function normalizeClaude(records, ref, invalid = 0) {
|
|
|
813
841
|
}
|
|
814
842
|
async function loadDesktopTitles(dir) {
|
|
815
843
|
const out = /* @__PURE__ */ new Map();
|
|
816
|
-
const files = await listSessionFiles(dir, (f) => (0,
|
|
844
|
+
const files = await listSessionFiles(dir, (f) => (0, import_node_path6.basename)(f).startsWith("local_") && f.endsWith(".json"));
|
|
817
845
|
for (const f of files) {
|
|
818
846
|
try {
|
|
819
847
|
const d = JSON.parse(await (0, import_promises5.readFile)(f.path, "utf8"));
|
|
@@ -826,9 +854,9 @@ async function loadDesktopTitles(dir) {
|
|
|
826
854
|
return out;
|
|
827
855
|
}
|
|
828
856
|
function createClaudeReader(o) {
|
|
829
|
-
const root = (0,
|
|
830
|
-
const indexDir = (0,
|
|
831
|
-
const isSessionFile = (file) => file.startsWith(root +
|
|
857
|
+
const root = (0, import_node_path6.join)(o.home, ".claude", "projects");
|
|
858
|
+
const indexDir = (0, import_node_path6.join)(o.home, "Library", "Application Support", "Claude", "claude-code-sessions");
|
|
859
|
+
const isSessionFile = (file) => file.startsWith(root + import_node_path6.sep) && file.endsWith(".jsonl") && (0, import_node_path6.relative)(root, file).split(import_node_path6.sep).length === 2;
|
|
832
860
|
const cache = /* @__PURE__ */ new Map();
|
|
833
861
|
async function refForFile(file, titles) {
|
|
834
862
|
let st;
|
|
@@ -884,7 +912,7 @@ function createClaudeReader(o) {
|
|
|
884
912
|
entry = { size: st.size, mtimeMs: st.mtimeMs, cwd, fileTitle: custom ?? ai, ref: null };
|
|
885
913
|
cache.set(file, entry);
|
|
886
914
|
}
|
|
887
|
-
const id = (0,
|
|
915
|
+
const id = (0, import_node_path6.basename)(file, ".jsonl");
|
|
888
916
|
const title = entry.fileTitle ?? (titles ?? await loadDesktopTitles(indexDir)).get(id) ?? null;
|
|
889
917
|
if (!entry.ref || entry.ref.title !== title) entry.ref = { tool: "claude-code", id, file, cwd: entry.cwd, title, lastActivityAt: st.mtime };
|
|
890
918
|
return entry.ref;
|
|
@@ -893,7 +921,7 @@ function createClaudeReader(o) {
|
|
|
893
921
|
tool: "claude-code",
|
|
894
922
|
root,
|
|
895
923
|
isSessionFile,
|
|
896
|
-
idFromPath: (file) => isSessionFile(file) ? (0,
|
|
924
|
+
idFromPath: (file) => isSessionFile(file) ? (0, import_node_path6.basename)(file, ".jsonl") : null,
|
|
897
925
|
refForFile: (file) => refForFile(file),
|
|
898
926
|
async listSessions(limit = 20) {
|
|
899
927
|
const files = await listSessionFiles(root, isSessionFile);
|
|
@@ -921,7 +949,7 @@ function createClaudeReader(o) {
|
|
|
921
949
|
|
|
922
950
|
// ../../packages/core/src/sources/codex.ts
|
|
923
951
|
var import_promises6 = require("node:fs/promises");
|
|
924
|
-
var
|
|
952
|
+
var import_node_path7 = require("node:path");
|
|
925
953
|
var OUTPUT_TAIL2 = 2e4;
|
|
926
954
|
var ROLLOUT_ID_RE = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
927
955
|
var KIND = { add: "create", update: "modify", delete: "delete" };
|
|
@@ -989,8 +1017,9 @@ function normalizeCodex(records, ref, invalid = 0) {
|
|
|
989
1017
|
sawItemPrompt = true;
|
|
990
1018
|
}
|
|
991
1019
|
} else if (it.type === "AgentMessage") {
|
|
992
|
-
const text = blockText(it.content)
|
|
1020
|
+
const { text, calls } = externalCalls(blockText(it.content));
|
|
993
1021
|
if (text) timeline.push({ kind: "assistant", text });
|
|
1022
|
+
timeline.push(...calls);
|
|
994
1023
|
halt = null;
|
|
995
1024
|
} else if (it.type === "CommandExecution") {
|
|
996
1025
|
const cmd = codexCommand(it.command);
|
|
@@ -1034,6 +1063,25 @@ function parseSessionMeta(line) {
|
|
|
1034
1063
|
const id = str(p.id);
|
|
1035
1064
|
return id ? { id, cwd: str(p.cwd) } : null;
|
|
1036
1065
|
}
|
|
1066
|
+
var EXTERNAL_CALL_RE = /\[external_agent_tool_call: ([\w-]+)\]\n?([\s\S]*?)\[\/external_agent_tool_call\]/g;
|
|
1067
|
+
function externalCalls(raw) {
|
|
1068
|
+
const calls = [];
|
|
1069
|
+
const text = raw.replace(EXTERNAL_CALL_RE, (_m, name, body) => {
|
|
1070
|
+
const command = /^command: (.+)$/m.exec(body)?.[1];
|
|
1071
|
+
let input = {};
|
|
1072
|
+
try {
|
|
1073
|
+
const parsed = JSON.parse(/^input: (\{.*\})$/m.exec(body)?.[1] ?? "{}");
|
|
1074
|
+
if (isObj(parsed)) input = parsed;
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
const path = str(input.path) ?? str(input.file_path) ?? str(input.target_file);
|
|
1078
|
+
if (name === "Shell" && command) calls.push({ kind: "command", run: { cmd: command, exitCode: null, output: "" } });
|
|
1079
|
+
else if (name === "Read" && path) calls.push({ kind: "read", path });
|
|
1080
|
+
else if (/^(Edit|Write|StrReplace|MultiEdit|Delete)$/.test(name) && path) calls.push({ kind: "edit", edit: { path, kind: name === "Delete" ? "delete" : "modify" } });
|
|
1081
|
+
return "";
|
|
1082
|
+
});
|
|
1083
|
+
return { text: text.replace(/\n{3,}/g, "\n\n").trim(), calls };
|
|
1084
|
+
}
|
|
1037
1085
|
function typedPrompt(line) {
|
|
1038
1086
|
if (!line.includes('"user_message"') && !line.includes('"UserMessage"')) return null;
|
|
1039
1087
|
let r;
|
|
@@ -1062,9 +1110,9 @@ async function loadTitles(file) {
|
|
|
1062
1110
|
return out;
|
|
1063
1111
|
}
|
|
1064
1112
|
function createCodexReader(o) {
|
|
1065
|
-
const root = (0,
|
|
1066
|
-
const indexFile = (0,
|
|
1067
|
-
const isSessionFile = (file) => file.startsWith(root +
|
|
1113
|
+
const root = (0, import_node_path7.join)(o.home, ".codex", "sessions");
|
|
1114
|
+
const indexFile = (0, import_node_path7.join)(o.home, ".codex", "session_index.jsonl");
|
|
1115
|
+
const isSessionFile = (file) => file.startsWith(root + import_node_path7.sep) && (0, import_node_path7.basename)(file).startsWith("rollout-") && file.endsWith(".jsonl");
|
|
1068
1116
|
const cache = /* @__PURE__ */ new Map();
|
|
1069
1117
|
async function refForFile(file, titles) {
|
|
1070
1118
|
let st;
|
|
@@ -1100,7 +1148,7 @@ function createCodexReader(o) {
|
|
|
1100
1148
|
tool: "codex",
|
|
1101
1149
|
root,
|
|
1102
1150
|
isSessionFile,
|
|
1103
|
-
idFromPath: (file) => isSessionFile(file) ? ROLLOUT_ID_RE.exec((0,
|
|
1151
|
+
idFromPath: (file) => isSessionFile(file) ? ROLLOUT_ID_RE.exec((0, import_node_path7.basename)(file))?.[1] ?? null : null,
|
|
1104
1152
|
refForFile: (file) => refForFile(file),
|
|
1105
1153
|
async listSessions(limit = 20) {
|
|
1106
1154
|
const files = await listSessionFiles(root, isSessionFile);
|
|
@@ -1140,7 +1188,7 @@ function createCodexReader(o) {
|
|
|
1140
1188
|
|
|
1141
1189
|
// ../../packages/core/src/sources/cursor.ts
|
|
1142
1190
|
var import_promises7 = require("node:fs/promises");
|
|
1143
|
-
var
|
|
1191
|
+
var import_node_path8 = require("node:path");
|
|
1144
1192
|
var norm = (name) => name.replace(/[^A-Za-z0-9]/g, "-");
|
|
1145
1193
|
async function resolveCursorSlug(slug, rootDir = "/") {
|
|
1146
1194
|
if (!slug || /^\d+$/.test(slug)) return null;
|
|
@@ -1151,9 +1199,9 @@ async function resolveCursorSlug(slug, rootDir = "/") {
|
|
|
1151
1199
|
for (const e of entries) {
|
|
1152
1200
|
if (!e.isDirectory()) continue;
|
|
1153
1201
|
const n = norm(e.name);
|
|
1154
|
-
if (rest === n) return (0,
|
|
1202
|
+
if (rest === n) return (0, import_node_path8.join)(dir, e.name);
|
|
1155
1203
|
if (rest.startsWith(n + "-")) {
|
|
1156
|
-
const found = await walk((0,
|
|
1204
|
+
const found = await walk((0, import_node_path8.join)(dir, e.name), rest.slice(n.length + 1), depth + 1);
|
|
1157
1205
|
if (found) return found;
|
|
1158
1206
|
}
|
|
1159
1207
|
}
|
|
@@ -1219,7 +1267,7 @@ function normalizeCursor(records, ref, invalid = 0) {
|
|
|
1219
1267
|
return { ref, model: null, timeline, todos: todos ? [...todos.values()] : null, halt, usage: null, unknownRecords: unknown };
|
|
1220
1268
|
}
|
|
1221
1269
|
function createCursorReader(o) {
|
|
1222
|
-
const root = (0,
|
|
1270
|
+
const root = (0, import_node_path8.join)(o.home, ".cursor", "projects");
|
|
1223
1271
|
const slugCache = /* @__PURE__ */ new Map();
|
|
1224
1272
|
const cwdFor = (slug) => {
|
|
1225
1273
|
let p = slugCache.get(slug);
|
|
@@ -1230,14 +1278,14 @@ function createCursorReader(o) {
|
|
|
1230
1278
|
return p;
|
|
1231
1279
|
};
|
|
1232
1280
|
const isSessionFile = (file) => {
|
|
1233
|
-
if (!file.startsWith(root +
|
|
1234
|
-
const parts = (0,
|
|
1281
|
+
if (!file.startsWith(root + import_node_path8.sep) || !file.endsWith(".jsonl")) return false;
|
|
1282
|
+
const parts = (0, import_node_path8.relative)(root, file).split(import_node_path8.sep);
|
|
1235
1283
|
return parts.length === 4 && parts[1] === "agent-transcripts" && parts[3] === `${parts[2]}.jsonl`;
|
|
1236
1284
|
};
|
|
1237
1285
|
const cache = /* @__PURE__ */ new Map();
|
|
1238
1286
|
async function refForFile(file) {
|
|
1239
1287
|
if (!isSessionFile(file)) return null;
|
|
1240
|
-
const parts = (0,
|
|
1288
|
+
const parts = (0, import_node_path8.relative)(root, file).split(import_node_path8.sep);
|
|
1241
1289
|
let st;
|
|
1242
1290
|
try {
|
|
1243
1291
|
st = await (0, import_promises7.stat)(file);
|
|
@@ -1274,7 +1322,7 @@ function createCursorReader(o) {
|
|
|
1274
1322
|
tool: "cursor",
|
|
1275
1323
|
root,
|
|
1276
1324
|
isSessionFile,
|
|
1277
|
-
idFromPath: (file) => isSessionFile(file) ? (0,
|
|
1325
|
+
idFromPath: (file) => isSessionFile(file) ? (0, import_node_path8.relative)(root, file).split(import_node_path8.sep)[2] : null,
|
|
1278
1326
|
refForFile,
|
|
1279
1327
|
async listSessions(limit = 20) {
|
|
1280
1328
|
const files = await listSessionFiles(root, isSessionFile);
|
|
@@ -1302,10 +1350,22 @@ function createCursorReader(o) {
|
|
|
1302
1350
|
// ../../packages/core/src/git/snapshot.ts
|
|
1303
1351
|
var import_node_child_process2 = require("node:child_process");
|
|
1304
1352
|
var import_node_crypto2 = require("node:crypto");
|
|
1353
|
+
var import_promises8 = require("node:fs/promises");
|
|
1354
|
+
var import_node_path9 = require("node:path");
|
|
1305
1355
|
var import_node_util2 = require("node:util");
|
|
1306
1356
|
var pexec2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
1307
1357
|
var MAX_DIFF_CHARS = 2e6;
|
|
1308
1358
|
var MAX_FILES = 500;
|
|
1359
|
+
async function inGitRepo(dir) {
|
|
1360
|
+
for (let d = dir; ; d = (0, import_node_path9.dirname)(d)) {
|
|
1361
|
+
try {
|
|
1362
|
+
await (0, import_promises8.stat)((0, import_node_path9.join)(d, ".git"));
|
|
1363
|
+
return true;
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
if ((0, import_node_path9.dirname)(d) === d) return false;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1309
1369
|
async function git(cwd, args) {
|
|
1310
1370
|
const { stdout } = await pexec2("git", args, {
|
|
1311
1371
|
cwd,
|
|
@@ -1382,7 +1442,7 @@ async function takeGitSnapshot(cwd) {
|
|
|
1382
1442
|
}
|
|
1383
1443
|
|
|
1384
1444
|
// ../../packages/core/src/extract/extract.ts
|
|
1385
|
-
var
|
|
1445
|
+
var import_node_path10 = require("node:path");
|
|
1386
1446
|
|
|
1387
1447
|
// ../../packages/core/src/extract/recent.ts
|
|
1388
1448
|
var USER_MESSAGES = { budgetChars: 16e3, messageChars: 1500 };
|
|
@@ -1473,7 +1533,7 @@ function splitShell(cmd, pipes = false) {
|
|
|
1473
1533
|
var isCd = (s) => /^cd(\s|$)/.test(s);
|
|
1474
1534
|
function isProbeStage(stage) {
|
|
1475
1535
|
const words = stage.trim().split(/\s+/);
|
|
1476
|
-
const first = (0,
|
|
1536
|
+
const first = (0, import_node_path10.basename)(words[0] ?? "");
|
|
1477
1537
|
if (first !== "git") return PROBES.has(first);
|
|
1478
1538
|
let i = 1;
|
|
1479
1539
|
while (i < words.length && words[i].startsWith("-")) i += words[i] === "-C" || words[i] === "-c" ? 2 : 1;
|
|
@@ -1564,18 +1624,18 @@ function buildTranscript(s, title, rel, redactor) {
|
|
|
1564
1624
|
}
|
|
1565
1625
|
const out = lines2.join("\n");
|
|
1566
1626
|
if (out.length <= TRANSCRIPT_MAX) return out;
|
|
1567
|
-
const
|
|
1568
|
-
const nl =
|
|
1569
|
-
return "\u2026(earlier transcript truncated)\n" + (nl === -1 ?
|
|
1627
|
+
const cut2 = out.slice(out.length - TRANSCRIPT_MAX);
|
|
1628
|
+
const nl = cut2.indexOf("\n");
|
|
1629
|
+
return "\u2026(earlier transcript truncated)\n" + (nl === -1 ? cut2 : cut2.slice(nl + 1));
|
|
1570
1630
|
}
|
|
1571
1631
|
function extractHandoff(s, git2, o) {
|
|
1572
1632
|
const cwd = s.ref.cwd;
|
|
1573
1633
|
if (!cwd) throw new HarlayError("This session has no known working directory, so Harlay cannot hand it off.");
|
|
1574
1634
|
const workdir = git2?.root ?? cwd;
|
|
1575
1635
|
const rel = (p) => {
|
|
1576
|
-
if (!(0,
|
|
1577
|
-
const r = (0,
|
|
1578
|
-
return r.startsWith("..") || (0,
|
|
1636
|
+
if (!(0, import_node_path10.isAbsolute)(p)) return p;
|
|
1637
|
+
const r = (0, import_node_path10.relative)(workdir, p);
|
|
1638
|
+
return r.startsWith("..") || (0, import_node_path10.isAbsolute)(r) ? p : r;
|
|
1579
1639
|
};
|
|
1580
1640
|
const split = s.timeline.flatMap((t) => t.kind === "user" ? [splitHandoffPrompt(t.text)] : []);
|
|
1581
1641
|
const prompts = split.flatMap((p) => p.rest ? [p.rest] : []);
|
|
@@ -1613,13 +1673,13 @@ function extractHandoff(s, git2, o) {
|
|
|
1613
1673
|
const filesTouched = [];
|
|
1614
1674
|
for (const e of [...edits].reverse()) {
|
|
1615
1675
|
const r = rel(e.path);
|
|
1616
|
-
if (!(0,
|
|
1676
|
+
if (!(0, import_node_path10.isAbsolute)(r) && !filesTouched.includes(r)) filesTouched.push(r);
|
|
1617
1677
|
if (filesTouched.length === 12) break;
|
|
1618
1678
|
}
|
|
1619
1679
|
const title = carried ? carried.title : s.ref.title ?? (originalGoal ? oneLine(redactor.apply(originalGoal), 60) : "Untitled task");
|
|
1620
|
-
const
|
|
1680
|
+
const pad2 = (n2) => String(n2).padStart(2, "0");
|
|
1621
1681
|
const n = o.now;
|
|
1622
|
-
const baseId = `${
|
|
1682
|
+
const baseId = `${pad2(n.getMonth() + 1)}${pad2(n.getDate())}-${pad2(n.getHours())}${pad2(n.getMinutes())}-${slugify(redactor.apply(title))}`;
|
|
1623
1683
|
return {
|
|
1624
1684
|
baseId,
|
|
1625
1685
|
title,
|
|
@@ -1653,9 +1713,9 @@ function extractHandoff(s, git2, o) {
|
|
|
1653
1713
|
|
|
1654
1714
|
// ../../packages/core/src/launch/resolve.ts
|
|
1655
1715
|
var import_node_fs2 = require("node:fs");
|
|
1656
|
-
var
|
|
1716
|
+
var import_promises9 = require("node:fs/promises");
|
|
1657
1717
|
var import_node_os = require("node:os");
|
|
1658
|
-
var
|
|
1718
|
+
var import_node_path11 = require("node:path");
|
|
1659
1719
|
function defaultResolveOptions(env = process.env) {
|
|
1660
1720
|
return {
|
|
1661
1721
|
home: env.HARLAY_HOME ?? (0, import_node_os.homedir)(),
|
|
@@ -1669,20 +1729,20 @@ var APP_BUNDLES = { "claude-code": ["Claude.app"], codex: ["ChatGPT.app", "Codex
|
|
|
1669
1729
|
function knownPaths(tool, o) {
|
|
1670
1730
|
switch (tool) {
|
|
1671
1731
|
case "claude-code":
|
|
1672
|
-
return [(0,
|
|
1732
|
+
return [(0, import_node_path11.join)(o.home, ".local", "bin", "claude")];
|
|
1673
1733
|
case "codex":
|
|
1674
1734
|
return [
|
|
1675
|
-
(0,
|
|
1676
|
-
(0,
|
|
1735
|
+
(0, import_node_path11.join)(o.applicationsDir, "ChatGPT.app", "Contents", "Resources", "codex-cli", "bin", "codex"),
|
|
1736
|
+
(0, import_node_path11.join)(o.applicationsDir, "Codex.app", "Contents", "Resources", "codex-cli", "bin", "codex")
|
|
1677
1737
|
];
|
|
1678
1738
|
case "cursor":
|
|
1679
|
-
return [(0,
|
|
1739
|
+
return [(0, import_node_path11.join)(o.home, ".local", "bin", "cursor-agent")];
|
|
1680
1740
|
}
|
|
1681
1741
|
}
|
|
1682
1742
|
async function isExecutable(p) {
|
|
1683
1743
|
try {
|
|
1684
|
-
if (!(await (0,
|
|
1685
|
-
await (0,
|
|
1744
|
+
if (!(await (0, import_promises9.stat)(p)).isFile()) return false;
|
|
1745
|
+
await (0, import_promises9.access)(p, import_node_fs2.constants.X_OK);
|
|
1686
1746
|
return true;
|
|
1687
1747
|
} catch {
|
|
1688
1748
|
return false;
|
|
@@ -1690,7 +1750,7 @@ async function isExecutable(p) {
|
|
|
1690
1750
|
}
|
|
1691
1751
|
async function exists2(p) {
|
|
1692
1752
|
try {
|
|
1693
|
-
await (0,
|
|
1753
|
+
await (0, import_promises9.access)(p);
|
|
1694
1754
|
return true;
|
|
1695
1755
|
} catch {
|
|
1696
1756
|
return false;
|
|
@@ -1699,7 +1759,7 @@ async function exists2(p) {
|
|
|
1699
1759
|
async function resolveBinary(tool, o) {
|
|
1700
1760
|
const dirs = [...(o.pathEnv ?? "").split(":").filter(Boolean), ...o.systemDirs];
|
|
1701
1761
|
for (const d of dirs) {
|
|
1702
|
-
const p = (0,
|
|
1762
|
+
const p = (0, import_node_path11.join)(d, BIN_NAME[tool]);
|
|
1703
1763
|
if (await isExecutable(p)) return p;
|
|
1704
1764
|
}
|
|
1705
1765
|
for (const p of knownPaths(tool, o)) if (await isExecutable(p)) return p;
|
|
@@ -1718,12 +1778,178 @@ async function availableTargets(o) {
|
|
|
1718
1778
|
const out = {};
|
|
1719
1779
|
for (const t of TOOLS) {
|
|
1720
1780
|
let app2 = false;
|
|
1721
|
-
for (const b of APP_BUNDLES[t]) if (await exists2((0,
|
|
1781
|
+
for (const b of APP_BUNDLES[t]) if (await exists2((0, import_node_path11.join)(o.applicationsDir, b))) app2 = true;
|
|
1722
1782
|
out[t] = { app: app2, terminal: bins[t] !== void 0 };
|
|
1723
1783
|
}
|
|
1724
1784
|
return out;
|
|
1725
1785
|
}
|
|
1726
1786
|
|
|
1787
|
+
// ../../packages/core/src/launch/web.ts
|
|
1788
|
+
var import_node_path12 = require("node:path");
|
|
1789
|
+
var NEW_CHAT_URL = { "claude-web": "https://claude.ai/new?q=", chatgpt: "https://chatgpt.com/?q=" };
|
|
1790
|
+
var ATTACHED = {
|
|
1791
|
+
usage_limit: (c) => `My chat with ${c} stopped on a usage limit, and the attached file is that chat.`,
|
|
1792
|
+
crash: (c) => `My chat with ${c} stopped with an error, and the attached file is that chat.`,
|
|
1793
|
+
manual: (c) => `I'm moving my chat with ${c} here, and the attached file is that chat.`
|
|
1794
|
+
};
|
|
1795
|
+
function webAttachPrompt(from, reason) {
|
|
1796
|
+
return `This is the context from Harlay. ${ATTACHED[reason](targetLabel(from))} Read it and pick up where it left off.`;
|
|
1797
|
+
}
|
|
1798
|
+
function webChatUrl(chat, text) {
|
|
1799
|
+
return NEW_CHAT_URL[chat] + encodeURIComponent(text);
|
|
1800
|
+
}
|
|
1801
|
+
var WEB_MAX_BYTES = 10500;
|
|
1802
|
+
var bytes = (s) => encodeURIComponent(s).length;
|
|
1803
|
+
function clipBytes(s, max) {
|
|
1804
|
+
if (bytes(s) <= max) return s;
|
|
1805
|
+
let lo = 0;
|
|
1806
|
+
let hi = s.length;
|
|
1807
|
+
while (lo < hi) {
|
|
1808
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
1809
|
+
if (bytes(cut(s, mid) + " \u2026") <= max) lo = mid;
|
|
1810
|
+
else hi = mid - 1;
|
|
1811
|
+
}
|
|
1812
|
+
const head = cut(s, lo);
|
|
1813
|
+
const space = head.search(/\s\S*$/);
|
|
1814
|
+
return `${(space > head.length * 0.8 ? head.slice(0, space) : head).trimEnd()} \u2026`;
|
|
1815
|
+
}
|
|
1816
|
+
function cut(s, n) {
|
|
1817
|
+
const c = s.charCodeAt(n - 1);
|
|
1818
|
+
return s.slice(0, c >= 55296 && c <= 56319 ? n - 1 : n);
|
|
1819
|
+
}
|
|
1820
|
+
function chatKind(s, inRepo = false) {
|
|
1821
|
+
for (const t of s.timeline) {
|
|
1822
|
+
if (t.kind === "user" && splitHandoffPrompt(t.text).handoff) return "coding";
|
|
1823
|
+
if (t.kind === "edit") return "coding";
|
|
1824
|
+
if (t.kind === "command" && !/\.harlay\b/.test(t.run.cmd)) return "coding";
|
|
1825
|
+
if (t.kind === "read" && t.how !== "fetched" && !t.path.endsWith(".harlay")) return "coding";
|
|
1826
|
+
}
|
|
1827
|
+
return inRepo ? "coding" : "chat";
|
|
1828
|
+
}
|
|
1829
|
+
var WHY = {
|
|
1830
|
+
usage_limit: (a) => `My chat with ${a} stopped on a usage limit.`,
|
|
1831
|
+
crash: (a) => `My chat with ${a} stopped with an error.`,
|
|
1832
|
+
manual: (a) => `I'm moving my chat with ${a} here.`
|
|
1833
|
+
};
|
|
1834
|
+
var USER_MAX = 2e3;
|
|
1835
|
+
var AGENT_MAX = 1600;
|
|
1836
|
+
var LAST_AGENT_MAX = 3200;
|
|
1837
|
+
var SEP = "\n\n";
|
|
1838
|
+
var DIFF_MIN = 2500;
|
|
1839
|
+
function fitList(items, budget, note, sep4 = SEP) {
|
|
1840
|
+
const gap = bytes(sep4);
|
|
1841
|
+
const size = (xs) => xs.reduce((n, x) => n + bytes(x) + gap, 0);
|
|
1842
|
+
if (size(items) <= budget) return items;
|
|
1843
|
+
const first = items[0];
|
|
1844
|
+
const noteSize = bytes(note(items.length)) + gap;
|
|
1845
|
+
const keepFirst = items.length > 1 && bytes(first) + gap + noteSize <= budget / 3;
|
|
1846
|
+
let room = budget - noteSize - (keepFirst ? bytes(first) + gap : 0);
|
|
1847
|
+
const latest = [];
|
|
1848
|
+
for (let i = items.length - 1; i >= (keepFirst ? 1 : 0); i--) {
|
|
1849
|
+
const c = bytes(items[i]) + gap;
|
|
1850
|
+
if (c > room) {
|
|
1851
|
+
if (!latest.length) latest.unshift(clipBytes(items[i], Math.max(0, room - gap)));
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
room -= c;
|
|
1855
|
+
latest.unshift(items[i]);
|
|
1856
|
+
}
|
|
1857
|
+
const left = items.length - latest.length - (keepFirst ? 1 : 0);
|
|
1858
|
+
return [...keepFirst ? [first] : [], ...left ? [note(left)] : [], ...latest];
|
|
1859
|
+
}
|
|
1860
|
+
function webMessage(d, s, kind, max = WEB_MAX_BYTES) {
|
|
1861
|
+
const r = new Redactor();
|
|
1862
|
+
const a = (x) => wellFormed(r.apply(stripControl(x))).trim();
|
|
1863
|
+
const agent = speakerLabel(d.from.tool);
|
|
1864
|
+
const turns = conversation(s, a);
|
|
1865
|
+
const unanswered = turns.at(-1)?.who === "me" ? `(The chat stopped here, before ${agent} answered.)` : null;
|
|
1866
|
+
const parts = [`This is the context from Harlay. ${WHY[d.from.reason](targetLabel(d.from.tool))} Pick up where it left off. Don't start over and don't repeat what was already said.`];
|
|
1867
|
+
const room = (rest) => max - bytes([...parts, ...rest].join(SEP)) - 2 * bytes(SEP);
|
|
1868
|
+
const done = () => ({ text: clipBytes(parts.join(SEP), max), redactions: { ...r.counts } });
|
|
1869
|
+
const left = (n) => `(${n} earlier message${n === 1 ? "" : "s"} left out)`;
|
|
1870
|
+
if (kind === "chat") {
|
|
1871
|
+
const items = turns.map(
|
|
1872
|
+
(t, i) => t.who === "me" ? `Me: ${clipBytes(t.text, USER_MAX)}` : `${agent}: ${clipBytes(t.text, i === turns.length - 1 ? LAST_AGENT_MAX : AGENT_MAX)}`
|
|
1873
|
+
);
|
|
1874
|
+
const tail2 = unanswered ? [unanswered] : [];
|
|
1875
|
+
if (items.length) parts.push("Conversation so far:");
|
|
1876
|
+
parts.push(...fitList(items, room(tail2), left), ...tail2);
|
|
1877
|
+
return done();
|
|
1878
|
+
}
|
|
1879
|
+
const branch = d.git?.branch ? ` on branch ${d.git.branch}` : "";
|
|
1880
|
+
parts.push(`It's a coding task in my project ${(0, import_node_path12.basename)(d.workdir)}${branch}. You can't see my files, so ask me to paste any you need.`);
|
|
1881
|
+
parts.push(`Task: ${a(d.title)}`);
|
|
1882
|
+
const facts = [];
|
|
1883
|
+
if (d.whereItStopped && !unanswered) facts.push(`${agent}'s last message:
|
|
1884
|
+
${clipBytes(a(d.whereItStopped), LAST_AGENT_MAX)}`);
|
|
1885
|
+
if (unanswered) facts.push(workedAfterLastMessage(s) ? `(It stopped while working on my last message, before ${agent} replied.)` : unanswered);
|
|
1886
|
+
const changed = [.../* @__PURE__ */ new Set([...d.git?.files.map((f) => f.path) ?? [], ...d.filesTouched])];
|
|
1887
|
+
if (changed.length) facts.push(`Files changed: ${changed.slice(0, 15).map(a).join(", ")}${changed.length > 15 ? ` and ${changed.length - 15} more` : ""}`);
|
|
1888
|
+
if (d.lastFailure) {
|
|
1889
|
+
const out = d.lastFailure.output.trim().split("\n").slice(-25).join("\n");
|
|
1890
|
+
facts.push(`Still failing: \`${a(d.lastFailure.cmd)}\` (exit ${d.lastFailure.exitCode})${out ? `
|
|
1891
|
+
${clipBytes(a(out), 1500)}` : ""}`);
|
|
1892
|
+
}
|
|
1893
|
+
if (d.todos.length) facts.push(`Still to do:
|
|
1894
|
+
${d.todos.map((t) => `- ${a(t.text)}${t.status === "in_progress" ? " (in progress)" : ""}`).join("\n")}`);
|
|
1895
|
+
const diff = d.git?.diff ? a(d.git.diff) : "";
|
|
1896
|
+
const heading = "What I asked, oldest first:";
|
|
1897
|
+
if (d.userMessages.length) {
|
|
1898
|
+
const asked = d.userMessages.map((m) => `- ${clipBytes(a(m), USER_MAX)}`);
|
|
1899
|
+
const free = room([heading, ...facts]);
|
|
1900
|
+
const budget = diff ? Math.max(free - DIFF_MIN, free * 0.6) : free;
|
|
1901
|
+
const list = fitList(asked, budget, (n) => `- ${left(n + d.omittedMessages)}`, "\n");
|
|
1902
|
+
if (d.omittedMessages && list.length === asked.length) list.splice(1, 0, `- ${left(d.omittedMessages)}`);
|
|
1903
|
+
parts.push(`${heading}
|
|
1904
|
+
${list.join("\n")}`);
|
|
1905
|
+
}
|
|
1906
|
+
parts.push(...facts);
|
|
1907
|
+
const diffHead = "Uncommitted changes (git diff):";
|
|
1908
|
+
const diffRoom = room([diffHead]) - bytes("\n");
|
|
1909
|
+
if (diff && diffRoom >= 600) parts.push(`${diffHead}
|
|
1910
|
+
${bytes(diff) <= diffRoom ? diff : clipLines(diff, diffRoom)}`);
|
|
1911
|
+
return done();
|
|
1912
|
+
}
|
|
1913
|
+
function clipLines(s, max) {
|
|
1914
|
+
const out = [];
|
|
1915
|
+
let n = bytes("\n\u2026(cut)");
|
|
1916
|
+
for (const line of s.split("\n")) {
|
|
1917
|
+
const c = bytes(`${line}
|
|
1918
|
+
`);
|
|
1919
|
+
if (n + c > max) break;
|
|
1920
|
+
n += c;
|
|
1921
|
+
out.push(line);
|
|
1922
|
+
}
|
|
1923
|
+
return [...out, "\u2026(cut)"].join("\n");
|
|
1924
|
+
}
|
|
1925
|
+
function workedAfterLastMessage(s) {
|
|
1926
|
+
for (let i = s.timeline.length - 1; i >= 0; i--) {
|
|
1927
|
+
const t = s.timeline[i];
|
|
1928
|
+
if (t.kind === "user") return false;
|
|
1929
|
+
if (t.kind === "command" || t.kind === "edit") return true;
|
|
1930
|
+
}
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1933
|
+
function conversation(s, a) {
|
|
1934
|
+
const turns = [];
|
|
1935
|
+
for (const t of s.timeline) {
|
|
1936
|
+
if (t.kind === "user") {
|
|
1937
|
+
const text = a(splitHandoffPrompt(t.text).rest);
|
|
1938
|
+
if (!text || /^\/[\w:-]+$/.test(text.split(/\s/)[0]) && text.length < 200) continue;
|
|
1939
|
+
turns.push({ who: "me", text });
|
|
1940
|
+
} else if (t.kind === "assistant") {
|
|
1941
|
+
const text = a(t.text);
|
|
1942
|
+
if (!text) continue;
|
|
1943
|
+
const last = turns.at(-1);
|
|
1944
|
+
if (last?.who === "agent") last.text += `
|
|
1945
|
+
|
|
1946
|
+
${text}`;
|
|
1947
|
+
else turns.push({ who: "agent", text });
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return turns;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1727
1953
|
// ../../packages/core/src/sources/index.ts
|
|
1728
1954
|
function createReaders(o) {
|
|
1729
1955
|
return [createClaudeReader({ home: o.home }), createCodexReader({ home: o.home }), createCursorReader({ home: o.home, slugRoot: o.slugRoot })];
|
|
@@ -1758,17 +1984,22 @@ function parseSessionSpec(spec) {
|
|
|
1758
1984
|
}
|
|
1759
1985
|
|
|
1760
1986
|
// ../../packages/core/src/handoff.ts
|
|
1761
|
-
var
|
|
1987
|
+
var import_promises10 = require("node:fs/promises");
|
|
1762
1988
|
var import_node_os2 = require("node:os");
|
|
1763
|
-
var
|
|
1989
|
+
var import_node_path13 = require("node:path");
|
|
1764
1990
|
async function launchHandoff(o) {
|
|
1991
|
+
if (isWebChat(o.target)) {
|
|
1992
|
+
const plan2 = { target: o.target, mode: "web", steps: [{ kind: "open", args: [webChatUrl(o.target, o.prompt)] }] };
|
|
1993
|
+
await executeLaunch(plan2, o.runner);
|
|
1994
|
+
return plan2;
|
|
1995
|
+
}
|
|
1765
1996
|
const binaries = await resolveBinaries(o.resolve ?? defaultResolveOptions());
|
|
1766
1997
|
const plan = planLaunch({
|
|
1767
1998
|
target: o.target,
|
|
1768
1999
|
mode: o.mode,
|
|
1769
2000
|
cwd: o.cwd,
|
|
1770
2001
|
prompt: o.prompt,
|
|
1771
|
-
scriptDir: (0,
|
|
2002
|
+
scriptDir: (0, import_node_path13.join)(o.scriptDir ?? (0, import_node_path13.join)((0, import_node_os2.tmpdir)(), "harlay"), o.id),
|
|
1772
2003
|
binaries,
|
|
1773
2004
|
terminalApp: o.terminalApp ?? "Terminal"
|
|
1774
2005
|
});
|
|
@@ -1779,7 +2010,7 @@ async function runHandoff(o) {
|
|
|
1779
2010
|
const cwd = o.session.cwd;
|
|
1780
2011
|
if (!cwd) throw new HarlayError("This session has no known working directory, so Harlay cannot hand it off.");
|
|
1781
2012
|
try {
|
|
1782
|
-
if (!(await (0,
|
|
2013
|
+
if (!(await (0, import_promises10.stat)(cwd)).isDirectory()) throw new Error("not a directory");
|
|
1783
2014
|
} catch {
|
|
1784
2015
|
throw new HarlayError(`The session's folder no longer exists: ${cwd}`);
|
|
1785
2016
|
}
|
|
@@ -1793,9 +2024,11 @@ async function runHandoff(o) {
|
|
|
1793
2024
|
filesChanged: git2 ? git2.files.length : data.filesTouched.length,
|
|
1794
2025
|
chars: sessionChars(session)
|
|
1795
2026
|
});
|
|
1796
|
-
const
|
|
1797
|
-
o.
|
|
1798
|
-
|
|
2027
|
+
const kind = chatKind(session, git2 !== null);
|
|
2028
|
+
const web = isWebChat(o.target) ? o.attach ? webAttachPrompt(data.from.tool, data.from.reason) : webMessage(data, session, kind).text : null;
|
|
2029
|
+
const written = await writeHandoff(data, { target: o.target, mode: web === null ? o.mode : "web", previousId: data.carriedFrom, store: o.store, prompt: web ?? void 0 });
|
|
2030
|
+
o.onProgress?.({ stage: "written", path: written.path, chars: web !== null && !o.attach ? web.length : written.chars });
|
|
2031
|
+
if (o.launch === false) return { written, plan: null, data, kind };
|
|
1799
2032
|
let plan;
|
|
1800
2033
|
try {
|
|
1801
2034
|
plan = await launchHandoff({ ...o, cwd: data.workdir, id: written.id, prompt: written.prompt });
|
|
@@ -1803,7 +2036,7 @@ async function runHandoff(o) {
|
|
|
1803
2036
|
const reason = (e instanceof Error ? e.message : String(e)).trim().replace(/\.$/, "");
|
|
1804
2037
|
throw new LaunchError(`${reason}. The handoff is ready at ${written.path}`, written);
|
|
1805
2038
|
}
|
|
1806
|
-
return { written, plan, data };
|
|
2039
|
+
return { written, plan, data, kind };
|
|
1807
2040
|
}
|
|
1808
2041
|
function sessionChars(s) {
|
|
1809
2042
|
let n = 0;
|
|
@@ -1851,8 +2084,8 @@ async function checkDelivery(readers2, h) {
|
|
|
1851
2084
|
// ../../packages/core/src/watch/watcher.ts
|
|
1852
2085
|
var import_node_events = require("node:events");
|
|
1853
2086
|
var import_node_fs3 = require("node:fs");
|
|
1854
|
-
var
|
|
1855
|
-
var
|
|
2087
|
+
var import_promises11 = require("node:fs/promises");
|
|
2088
|
+
var import_node_path14 = require("node:path");
|
|
1856
2089
|
var import_node_string_decoder = require("node:string_decoder");
|
|
1857
2090
|
var Watcher = class extends import_node_events.EventEmitter {
|
|
1858
2091
|
constructor(readers2, opts = {}) {
|
|
@@ -1876,14 +2109,14 @@ var Watcher = class extends import_node_events.EventEmitter {
|
|
|
1876
2109
|
for (const r of this.readers) {
|
|
1877
2110
|
for (const f of await listSessionFiles(r.root, r.isSessionFile)) {
|
|
1878
2111
|
try {
|
|
1879
|
-
this.offsets.set(f.path, (await (0,
|
|
2112
|
+
this.offsets.set(f.path, (await (0, import_promises11.stat)(f.path)).size);
|
|
1880
2113
|
} catch {
|
|
1881
2114
|
}
|
|
1882
2115
|
}
|
|
1883
2116
|
if (this.opts.fsWatch === false) continue;
|
|
1884
2117
|
try {
|
|
1885
2118
|
const w = (0, import_node_fs3.watch)(r.root, { recursive: true }, (_event, name) => {
|
|
1886
|
-
if (name) this.schedule((0,
|
|
2119
|
+
if (name) this.schedule((0, import_node_path14.join)(r.root, name.toString()));
|
|
1887
2120
|
});
|
|
1888
2121
|
w.on("error", (e) => this.emit("error", e));
|
|
1889
2122
|
this.watchers.push(w);
|
|
@@ -1904,7 +2137,7 @@ var Watcher = class extends import_node_events.EventEmitter {
|
|
|
1904
2137
|
for (const f of await listSessionFiles(r.root, r.isSessionFile)) {
|
|
1905
2138
|
let size;
|
|
1906
2139
|
try {
|
|
1907
|
-
size = (await (0,
|
|
2140
|
+
size = (await (0, import_promises11.stat)(f.path)).size;
|
|
1908
2141
|
} catch {
|
|
1909
2142
|
continue;
|
|
1910
2143
|
}
|
|
@@ -1961,7 +2194,7 @@ var Watcher = class extends import_node_events.EventEmitter {
|
|
|
1961
2194
|
if (!reader) return [];
|
|
1962
2195
|
let size;
|
|
1963
2196
|
try {
|
|
1964
|
-
size = (await (0,
|
|
2197
|
+
size = (await (0, import_promises11.stat)(file)).size;
|
|
1965
2198
|
} catch {
|
|
1966
2199
|
return [];
|
|
1967
2200
|
}
|
|
@@ -1973,7 +2206,7 @@ var Watcher = class extends import_node_events.EventEmitter {
|
|
|
1973
2206
|
}
|
|
1974
2207
|
if (size === offset) return [];
|
|
1975
2208
|
const buf = Buffer.alloc(size - offset);
|
|
1976
|
-
const fh = await (0,
|
|
2209
|
+
const fh = await (0, import_promises11.open)(file, "r");
|
|
1977
2210
|
try {
|
|
1978
2211
|
await fh.read(buf, 0, buf.length, offset);
|
|
1979
2212
|
} finally {
|
|
@@ -2050,8 +2283,8 @@ function fullestWindow(s) {
|
|
|
2050
2283
|
|
|
2051
2284
|
// ../../packages/core/src/usage/codex.ts
|
|
2052
2285
|
var import_node_child_process3 = require("node:child_process");
|
|
2053
|
-
var
|
|
2054
|
-
var
|
|
2286
|
+
var import_promises12 = require("node:fs/promises");
|
|
2287
|
+
var import_node_path15 = require("node:path");
|
|
2055
2288
|
var PACE_MS = 60 * 6e4;
|
|
2056
2289
|
var TAIL_BYTES = 1024 * 1024;
|
|
2057
2290
|
function num(x) {
|
|
@@ -2075,11 +2308,11 @@ function planOf(rl) {
|
|
|
2075
2308
|
const p = rl.plan_type ?? rl.planType;
|
|
2076
2309
|
return typeof p === "string" ? p : null;
|
|
2077
2310
|
}
|
|
2078
|
-
async function readTail(file,
|
|
2079
|
-
const fh = await (0,
|
|
2311
|
+
async function readTail(file, bytes2) {
|
|
2312
|
+
const fh = await (0, import_promises12.open)(file, "r");
|
|
2080
2313
|
try {
|
|
2081
2314
|
const { size } = await fh.stat();
|
|
2082
|
-
const start = Math.max(0, size -
|
|
2315
|
+
const start = Math.max(0, size - bytes2);
|
|
2083
2316
|
const buf = Buffer.alloc(size - start);
|
|
2084
2317
|
await fh.read(buf, 0, buf.length, start);
|
|
2085
2318
|
const text = buf.toString("utf8");
|
|
@@ -2089,7 +2322,7 @@ async function readTail(file, bytes) {
|
|
|
2089
2322
|
}
|
|
2090
2323
|
}
|
|
2091
2324
|
async function readCodexReadings(home2, files = 8) {
|
|
2092
|
-
const logs = (await listSessionFiles((0,
|
|
2325
|
+
const logs = (await listSessionFiles((0, import_node_path15.join)(home2, ".codex", "sessions"), (f) => f.endsWith(".jsonl"))).slice(0, files);
|
|
2093
2326
|
const out = [];
|
|
2094
2327
|
for (const { path } of logs) {
|
|
2095
2328
|
let text;
|
|
@@ -2193,8 +2426,8 @@ function queryCodexUsage(codexBin, timeoutMs = 15e3) {
|
|
|
2193
2426
|
}
|
|
2194
2427
|
|
|
2195
2428
|
// ../../packages/core/src/usage/claude.ts
|
|
2196
|
-
var
|
|
2197
|
-
var
|
|
2429
|
+
var import_promises13 = require("node:fs/promises");
|
|
2430
|
+
var import_node_path16 = require("node:path");
|
|
2198
2431
|
var HOUR = 36e5;
|
|
2199
2432
|
var SAMPLE_MAX_AGE_MS = 30 * 24 * HOUR;
|
|
2200
2433
|
var DISPROVE_FACTOR = 1.15;
|
|
@@ -2222,18 +2455,18 @@ var ClaudeUsageLog = class {
|
|
|
2222
2455
|
replies = /* @__PURE__ */ new Map();
|
|
2223
2456
|
hits = /* @__PURE__ */ new Map();
|
|
2224
2457
|
async refresh(now = /* @__PURE__ */ new Date(), maxAgeMs = 8 * 24 * HOUR) {
|
|
2225
|
-
const root = (0,
|
|
2458
|
+
const root = (0, import_node_path16.join)(this.home, ".claude", "projects");
|
|
2226
2459
|
let names;
|
|
2227
2460
|
try {
|
|
2228
|
-
names = await (0,
|
|
2461
|
+
names = await (0, import_promises13.readdir)(root, { recursive: true });
|
|
2229
2462
|
} catch {
|
|
2230
2463
|
return;
|
|
2231
2464
|
}
|
|
2232
2465
|
for (const name of names) {
|
|
2233
2466
|
if (!name.endsWith(".jsonl")) continue;
|
|
2234
|
-
const path = (0,
|
|
2467
|
+
const path = (0, import_node_path16.join)(root, name);
|
|
2235
2468
|
try {
|
|
2236
|
-
const s = await (0,
|
|
2469
|
+
const s = await (0, import_promises13.stat)(path);
|
|
2237
2470
|
if (now.getTime() - s.mtimeMs > maxAgeMs) continue;
|
|
2238
2471
|
await this.readFrom(path, s.size);
|
|
2239
2472
|
} catch {
|
|
@@ -2247,7 +2480,7 @@ var ClaudeUsageLog = class {
|
|
|
2247
2480
|
const st = this.files.get(path) ?? { offset: 0, partial: "" };
|
|
2248
2481
|
if (size < st.offset) Object.assign(st, { offset: 0, partial: "" });
|
|
2249
2482
|
if (size === st.offset) return;
|
|
2250
|
-
const fh = await (0,
|
|
2483
|
+
const fh = await (0, import_promises13.open)(path, "r");
|
|
2251
2484
|
try {
|
|
2252
2485
|
const buf = Buffer.alloc(size - st.offset);
|
|
2253
2486
|
await fh.read(buf, 0, buf.length, st.offset);
|
|
@@ -2452,18 +2685,18 @@ function claudeSnapshot({ entries, hits, exact, samples, now }) {
|
|
|
2452
2685
|
}
|
|
2453
2686
|
|
|
2454
2687
|
// ../../packages/core/src/usage/bridge.ts
|
|
2455
|
-
var
|
|
2456
|
-
var
|
|
2688
|
+
var import_promises14 = require("node:fs/promises");
|
|
2689
|
+
var import_node_path17 = require("node:path");
|
|
2457
2690
|
var BRIDGE_MARKER = "# harlay-usage";
|
|
2458
2691
|
function claudeSettingsPath(home2) {
|
|
2459
|
-
return (0,
|
|
2692
|
+
return (0, import_node_path17.join)(home2, ".claude", "settings.json");
|
|
2460
2693
|
}
|
|
2461
2694
|
function claudeStatusFile(dataDir2) {
|
|
2462
|
-
return (0,
|
|
2695
|
+
return (0, import_node_path17.join)(dataDir2, "claude-status.json");
|
|
2463
2696
|
}
|
|
2464
|
-
var scriptPath = (dataDir2) => (0,
|
|
2465
|
-
var previousJson = (dataDir2) => (0,
|
|
2466
|
-
var previousCommand = (dataDir2) => (0,
|
|
2697
|
+
var scriptPath = (dataDir2) => (0, import_node_path17.join)(dataDir2, "claude-statusline.sh");
|
|
2698
|
+
var previousJson = (dataDir2) => (0, import_node_path17.join)(dataDir2, "claude-statusline.previous.json");
|
|
2699
|
+
var previousCommand = (dataDir2) => (0, import_node_path17.join)(dataDir2, "claude-statusline.previous");
|
|
2467
2700
|
var BRIDGE_SCRIPT = `#!/bin/sh
|
|
2468
2701
|
# Harlay's Claude Code status line. Saves Claude Code's status line input (it includes your exact usage)
|
|
2469
2702
|
# for Harlay, then shows your previous status line, or a short usage summary if you had none.
|
|
@@ -2498,7 +2731,7 @@ function isOurs(statusLine) {
|
|
|
2498
2731
|
async function readSettings(path) {
|
|
2499
2732
|
let text;
|
|
2500
2733
|
try {
|
|
2501
|
-
text = await (0,
|
|
2734
|
+
text = await (0, import_promises14.readFile)(path, "utf8");
|
|
2502
2735
|
} catch (e) {
|
|
2503
2736
|
if (e.code === "ENOENT") return {};
|
|
2504
2737
|
throw e;
|
|
@@ -2509,8 +2742,8 @@ async function readSettings(path) {
|
|
|
2509
2742
|
}
|
|
2510
2743
|
async function writeAtomic(path, text) {
|
|
2511
2744
|
const tmp = `${path}.harlay-tmp`;
|
|
2512
|
-
await (0,
|
|
2513
|
-
await (0,
|
|
2745
|
+
await (0, import_promises14.writeFile)(tmp, text);
|
|
2746
|
+
await (0, import_promises14.rename)(tmp, path);
|
|
2514
2747
|
}
|
|
2515
2748
|
async function bridgeState(home2) {
|
|
2516
2749
|
try {
|
|
@@ -2522,9 +2755,9 @@ async function bridgeState(home2) {
|
|
|
2522
2755
|
async function installBridge(home2, dataDir2) {
|
|
2523
2756
|
const path = claudeSettingsPath(home2);
|
|
2524
2757
|
const s = await readSettings(path);
|
|
2525
|
-
await (0,
|
|
2526
|
-
await (0,
|
|
2527
|
-
await (0,
|
|
2758
|
+
await (0, import_promises14.mkdir)(dataDir2, { recursive: true });
|
|
2759
|
+
await (0, import_promises14.writeFile)(scriptPath(dataDir2), BRIDGE_SCRIPT);
|
|
2760
|
+
await (0, import_promises14.chmod)(scriptPath(dataDir2), 493);
|
|
2528
2761
|
if (isOurs(s.statusLine)) return;
|
|
2529
2762
|
const prev = s.statusLine;
|
|
2530
2763
|
if (prev !== void 0) {
|
|
@@ -2532,11 +2765,11 @@ async function installBridge(home2, dataDir2) {
|
|
|
2532
2765
|
const cmd = isObj3(prev) && typeof prev.command === "string" ? prev.command : "";
|
|
2533
2766
|
await writeAtomic(previousCommand(dataDir2), cmd);
|
|
2534
2767
|
} else {
|
|
2535
|
-
await (0,
|
|
2536
|
-
await (0,
|
|
2768
|
+
await (0, import_promises14.rm)(previousJson(dataDir2), { force: true });
|
|
2769
|
+
await (0, import_promises14.rm)(previousCommand(dataDir2), { force: true });
|
|
2537
2770
|
}
|
|
2538
2771
|
s.statusLine = { ...isObj3(prev) ? prev : {}, type: "command", command: bridgeCommand(dataDir2) };
|
|
2539
|
-
await (0,
|
|
2772
|
+
await (0, import_promises14.mkdir)((0, import_node_path17.join)(home2, ".claude"), { recursive: true });
|
|
2540
2773
|
await writeAtomic(path, JSON.stringify(s, null, 2) + "\n");
|
|
2541
2774
|
}
|
|
2542
2775
|
async function removeBridge(home2, dataDir2) {
|
|
@@ -2545,19 +2778,19 @@ async function removeBridge(home2, dataDir2) {
|
|
|
2545
2778
|
if (!isOurs(s.statusLine)) return;
|
|
2546
2779
|
let prev;
|
|
2547
2780
|
try {
|
|
2548
|
-
prev = JSON.parse(await (0,
|
|
2781
|
+
prev = JSON.parse(await (0, import_promises14.readFile)(previousJson(dataDir2), "utf8"));
|
|
2549
2782
|
} catch {
|
|
2550
2783
|
prev = void 0;
|
|
2551
2784
|
}
|
|
2552
2785
|
if (prev === void 0) delete s.statusLine;
|
|
2553
2786
|
else s.statusLine = prev;
|
|
2554
2787
|
await writeAtomic(path, JSON.stringify(s, null, 2) + "\n");
|
|
2555
|
-
await (0,
|
|
2556
|
-
await (0,
|
|
2788
|
+
await (0, import_promises14.rm)(previousJson(dataDir2), { force: true });
|
|
2789
|
+
await (0, import_promises14.rm)(previousCommand(dataDir2), { force: true });
|
|
2557
2790
|
}
|
|
2558
2791
|
|
|
2559
2792
|
// ../../packages/core/src/health/context.ts
|
|
2560
|
-
var
|
|
2793
|
+
var import_promises15 = require("node:fs/promises");
|
|
2561
2794
|
var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2562
2795
|
function parse(line) {
|
|
2563
2796
|
try {
|
|
@@ -2591,7 +2824,7 @@ function scanLine(tool, line, s) {
|
|
|
2591
2824
|
}
|
|
2592
2825
|
}
|
|
2593
2826
|
async function readFrom(file, start, end) {
|
|
2594
|
-
const fh = await (0,
|
|
2827
|
+
const fh = await (0, import_promises15.open)(file, "r");
|
|
2595
2828
|
try {
|
|
2596
2829
|
const buf = Buffer.alloc(end - start);
|
|
2597
2830
|
const { bytesRead } = await fh.read(buf, 0, buf.length, start);
|
|
@@ -2606,7 +2839,7 @@ var ContextTracker = class {
|
|
|
2606
2839
|
if (ref.tool === "cursor") return null;
|
|
2607
2840
|
let size;
|
|
2608
2841
|
try {
|
|
2609
|
-
size = (await (0,
|
|
2842
|
+
size = (await (0, import_promises15.stat)(ref.file)).size;
|
|
2610
2843
|
} catch {
|
|
2611
2844
|
return null;
|
|
2612
2845
|
}
|
|
@@ -2628,10 +2861,10 @@ var ContextTracker = class {
|
|
|
2628
2861
|
};
|
|
2629
2862
|
|
|
2630
2863
|
// ../../packages/core/src/health/tail.ts
|
|
2631
|
-
var
|
|
2864
|
+
var import_promises16 = require("node:fs/promises");
|
|
2632
2865
|
var TAIL_BYTES2 = 15e5;
|
|
2633
2866
|
async function readSessionTail(ref, maxBytes = TAIL_BYTES2) {
|
|
2634
|
-
const fh = await (0,
|
|
2867
|
+
const fh = await (0, import_promises16.open)(ref.file, "r");
|
|
2635
2868
|
let text;
|
|
2636
2869
|
try {
|
|
2637
2870
|
const { size } = await fh.stat();
|
|
@@ -2651,7 +2884,7 @@ async function readSessionTail(ref, maxBytes = TAIL_BYTES2) {
|
|
|
2651
2884
|
}
|
|
2652
2885
|
|
|
2653
2886
|
// ../../packages/core/src/health/signals.ts
|
|
2654
|
-
var
|
|
2887
|
+
var import_node_path18 = require("node:path");
|
|
2655
2888
|
|
|
2656
2889
|
// ../../packages/core/src/extract/rules.ts
|
|
2657
2890
|
var RULES2 = { maxRules: 10, budgetTokens: 300, words: 40, pastedWords: 400 };
|
|
@@ -2842,7 +3075,7 @@ function rereadSignals(t, cwd = null) {
|
|
|
2842
3075
|
}
|
|
2843
3076
|
}
|
|
2844
3077
|
const [path, n] = [...reads].reduce((best, cur) => cur[1] > best[1] ? cur : best, ["", 0]);
|
|
2845
|
-
if (n >= SIGNALS.rereads) return [{ weight: 1, behavior: true, text: `Re-read ${(0,
|
|
3078
|
+
if (n >= SIGNALS.rereads) return [{ weight: 1, behavior: true, text: `Re-read ${(0, import_node_path18.basename)(path)} ${n} times without changing it` }];
|
|
2846
3079
|
const [cmd, m] = [...lookups].reduce((best, cur) => cur[1] > best[1] ? cur : best, ["", 0]);
|
|
2847
3080
|
return m >= SIGNALS.rereads ? [{ weight: 1, behavior: true, text: `Ran \`${oneLine(shown.get(cmd) ?? cmd, 40)}\` ${m} times` }] : [];
|
|
2848
3081
|
}
|
|
@@ -2851,7 +3084,7 @@ function churnSignals(t) {
|
|
|
2851
3084
|
const edits = /* @__PURE__ */ new Map();
|
|
2852
3085
|
for (const i of t) if (i.kind === "edit") edits.set(i.edit.path, (edits.get(i.edit.path) ?? 0) + 1);
|
|
2853
3086
|
const [path, n] = [...edits].reduce((best, cur) => cur[1] > best[1] ? cur : best, ["", 0]);
|
|
2854
|
-
return n >= SIGNALS.churnEdits ? [{ weight: 1, behavior: true, text: `Edited ${(0,
|
|
3087
|
+
return n >= SIGNALS.churnEdits ? [{ weight: 1, behavior: true, text: `Edited ${(0, import_node_path18.basename)(path)} ${n} times while things kept failing` }] : [];
|
|
2855
3088
|
}
|
|
2856
3089
|
var NEGATIVE_RE = /\b(never|don'?t|do not|avoid|stop|no)\b/i;
|
|
2857
3090
|
var JS_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
@@ -2888,7 +3121,7 @@ function ruleCheck(rule) {
|
|
|
2888
3121
|
const targets = [...rule.matchAll(PATH_RE)].map((m) => (m[1] ?? m[2]).trim()).filter((x) => x.length > 1);
|
|
2889
3122
|
if (/\b(edit|touch|modify|change|delete|remove|rename|overwrite)\b/.test(r) && targets.some((x) => /[./]/.test(x))) {
|
|
2890
3123
|
const files = targets.filter((x) => /[./]/.test(x));
|
|
2891
|
-
return (i) => i.kind === "edit" && files.some((f) => i.edit.path.endsWith(f) || i.edit.path.includes(`/${f}`)) ? `edited ${(0,
|
|
3124
|
+
return (i) => i.kind === "edit" && files.some((f) => i.edit.path.endsWith(f) || i.edit.path.includes(`/${f}`)) ? `edited ${(0, import_node_path18.basename)(i.edit.path)}` : null;
|
|
2892
3125
|
}
|
|
2893
3126
|
const quoted = [...rule.matchAll(/`([^`]+)`/g)].map((m) => m[1].trim());
|
|
2894
3127
|
if (quoted.length && /\b(run|use|call|execute)\b/.test(r)) return (i) => i.kind === "command" && quoted.some((q) => bare(i.run.cmd).startsWith(q)) ? ran(i) : null;
|
|
@@ -2960,12 +3193,125 @@ function assessHealth(s, context) {
|
|
|
2960
3193
|
return { state: state2, reasons, context };
|
|
2961
3194
|
}
|
|
2962
3195
|
|
|
3196
|
+
// ../../packages/core/src/webchat.ts
|
|
3197
|
+
var import_node_path19 = require("node:path");
|
|
3198
|
+
function isLimitText(s) {
|
|
3199
|
+
const t = s.replace(/\s+/g, " ");
|
|
3200
|
+
if (t.length > 400) return false;
|
|
3201
|
+
if (!/\blimits?\b|out of (?:free )?messages/i.test(t)) return false;
|
|
3202
|
+
return /\b(?:hit|reached|exceeded|out of|resets?|reset at|until|try again)\b/i.test(t);
|
|
3203
|
+
}
|
|
3204
|
+
function limitResetsAt(s, now) {
|
|
3205
|
+
const inHours = /\bin (\d+) hours?\b/i.exec(s);
|
|
3206
|
+
if (inHours) return new Date(now.getTime() + Number(inHours[1]) * 36e5);
|
|
3207
|
+
const inMinutes = /\bin (\d+) min(?:ute)?s?\b/i.exec(s);
|
|
3208
|
+
if (inMinutes) return new Date(now.getTime() + Number(inMinutes[1]) * 6e4);
|
|
3209
|
+
const m = /\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i.exec(s);
|
|
3210
|
+
if (!m) return null;
|
|
3211
|
+
let h = Number(m[1]) % 12;
|
|
3212
|
+
if (m[3].toLowerCase() === "pm") h += 12;
|
|
3213
|
+
const at = new Date(now);
|
|
3214
|
+
at.setHours(h, m[2] ? Number(m[2]) : 0, 0, 0);
|
|
3215
|
+
if (at.getTime() <= now.getTime()) at.setDate(at.getDate() + 1);
|
|
3216
|
+
return at;
|
|
3217
|
+
}
|
|
3218
|
+
function webChatTitle(c) {
|
|
3219
|
+
const t = c.title.replace(/\s*[-|–]\s*(?:Claude|ChatGPT(?: Mobile)?)\s*$/i, "").trim();
|
|
3220
|
+
if (t && !/^(?:Claude|ChatGPT|New chat|New session)$/i.test(t)) return t;
|
|
3221
|
+
const first = c.turns.find((x) => x.who === "me");
|
|
3222
|
+
return first ? oneLine(first.text, 60) : "Web chat";
|
|
3223
|
+
}
|
|
3224
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
3225
|
+
function webChatData(c, o) {
|
|
3226
|
+
const r = new Redactor();
|
|
3227
|
+
const a = (s) => wellFormed(r.apply(stripControl(s))).trim();
|
|
3228
|
+
const turns = c.turns.map((t) => ({ who: t.who, text: a(t.text) })).filter((t) => t.text);
|
|
3229
|
+
const mine = turns.filter((t) => t.who === "me").map((t) => t.text);
|
|
3230
|
+
const picked = pickUserMessages(mine);
|
|
3231
|
+
const lastReply = [...turns].reverse().find((t) => t.who === "agent") ?? null;
|
|
3232
|
+
const title = a(webChatTitle(c));
|
|
3233
|
+
const speaker = speakerLabel(c.chat);
|
|
3234
|
+
const n = o.now;
|
|
3235
|
+
return {
|
|
3236
|
+
baseId: `${pad(n.getMonth() + 1)}${pad(n.getDate())}-${pad(n.getHours())}${pad(n.getMinutes())}-${slugify(title)}`,
|
|
3237
|
+
title,
|
|
3238
|
+
createdAt: n,
|
|
3239
|
+
from: {
|
|
3240
|
+
tool: c.chat,
|
|
3241
|
+
model: null,
|
|
3242
|
+
session: c.url,
|
|
3243
|
+
reason: c.limit ? "usage_limit" : "manual",
|
|
3244
|
+
resetsAt: c.limit ? limitResetsAt(c.limit, n) : null,
|
|
3245
|
+
message: c.limit
|
|
3246
|
+
},
|
|
3247
|
+
workdir: o.git?.root ?? o.workdir,
|
|
3248
|
+
git: o.git,
|
|
3249
|
+
originalGoal: mine[0] ?? null,
|
|
3250
|
+
userMessages: picked.messages,
|
|
3251
|
+
omittedMessages: picked.omitted,
|
|
3252
|
+
whereItStopped: lastReply && turns.at(-1)?.who === "agent" ? clipWords(lastReply.text, 150) : null,
|
|
3253
|
+
lastRequest: picked.messages.at(-1) ?? null,
|
|
3254
|
+
todos: [],
|
|
3255
|
+
results: [],
|
|
3256
|
+
references: { read: [], attached: [], urls: [c.url] },
|
|
3257
|
+
filesTouched: [],
|
|
3258
|
+
lastFailure: null,
|
|
3259
|
+
transcript: [`# Conversation: ${title}`, "", `Source: ${targetLabel(c.chat)}, ${c.url}`, "", ...turns.map((t) => `**${t.who === "me" ? "User" : speaker}:**
|
|
3260
|
+
${t.text}
|
|
3261
|
+
`)].join("\n"),
|
|
3262
|
+
unknownRecords: 0,
|
|
3263
|
+
redactions: { ...r.counts },
|
|
3264
|
+
carriedFrom: null
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
function webChatTimeline(c) {
|
|
3268
|
+
return c.turns.map((t) => t.who === "me" ? { kind: "user", text: t.text } : { kind: "assistant", text: t.text });
|
|
3269
|
+
}
|
|
3270
|
+
async function runWebChatHandoff(o) {
|
|
3271
|
+
const now = o.now ?? /* @__PURE__ */ new Date();
|
|
3272
|
+
if (isWebChat(o.target)) {
|
|
3273
|
+
if (o.target === o.capture.chat) throw new HarlayError(`This chat is already in ${targetLabel(o.target)}.`);
|
|
3274
|
+
const d2 = webChatData(o.capture, { now, workdir: "", git: null });
|
|
3275
|
+
let written2 = null;
|
|
3276
|
+
let prompt;
|
|
3277
|
+
if (o.attach) {
|
|
3278
|
+
prompt = webAttachPrompt(o.capture.chat, d2.from.reason);
|
|
3279
|
+
written2 = await writeHandoff(d2, { target: o.target, mode: "web", previousId: null, store: o.store, prompt, dir: (0, import_node_path19.join)(o.store, "web") });
|
|
3280
|
+
} else prompt = webMessage(d2, { timeline: webChatTimeline(o.capture) }, "chat").text;
|
|
3281
|
+
if (o.launch === false) return { written: written2, prompt, plan: null, data: d2 };
|
|
3282
|
+
const plan = await launchHandoff({ target: o.target, mode: o.mode, cwd: d2.workdir, id: d2.baseId, prompt, runner: o.runner });
|
|
3283
|
+
return { written: written2, prompt, plan, data: d2 };
|
|
3284
|
+
}
|
|
3285
|
+
if (!o.workdir) throw new HarlayError(`Pick a folder to open ${targetLabel(o.target)} in.`);
|
|
3286
|
+
const git2 = await takeGitSnapshot(o.workdir);
|
|
3287
|
+
const d = webChatData(o.capture, { now, workdir: o.workdir, git: git2 });
|
|
3288
|
+
const written = await writeHandoff(d, { target: o.target, mode: o.mode, previousId: null, store: o.store });
|
|
3289
|
+
if (o.launch === false) return { written, prompt: written.prompt, plan: null, data: d };
|
|
3290
|
+
try {
|
|
3291
|
+
const plan = await launchHandoff({
|
|
3292
|
+
target: o.target,
|
|
3293
|
+
mode: o.mode,
|
|
3294
|
+
cwd: d.workdir,
|
|
3295
|
+
id: written.id,
|
|
3296
|
+
prompt: written.prompt,
|
|
3297
|
+
runner: o.runner,
|
|
3298
|
+
resolve: o.resolve,
|
|
3299
|
+
terminalApp: o.terminalApp,
|
|
3300
|
+
scriptDir: o.scriptDir
|
|
3301
|
+
});
|
|
3302
|
+
return { written, prompt: written.prompt, plan, data: d };
|
|
3303
|
+
} catch (e) {
|
|
3304
|
+
const reason = (e instanceof Error ? e.message : String(e)).trim().replace(/\.$/, "");
|
|
3305
|
+
throw new LaunchError(`${reason}. The handoff is ready at ${written.path}`, written);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
2963
3309
|
// src/menu.ts
|
|
2964
|
-
var
|
|
3310
|
+
var import_node_path21 = require("node:path");
|
|
2965
3311
|
|
|
2966
3312
|
// src/settings.ts
|
|
2967
|
-
var
|
|
2968
|
-
var
|
|
3313
|
+
var import_promises17 = require("node:fs/promises");
|
|
3314
|
+
var import_node_path20 = require("node:path");
|
|
2969
3315
|
var THEMES = ["system", "light", "dark"];
|
|
2970
3316
|
var isTheme = (x) => THEMES.includes(x);
|
|
2971
3317
|
var MAX_PINNED = 12;
|
|
@@ -2983,12 +3329,14 @@ var DEFAULT_SETTINGS = {
|
|
|
2983
3329
|
openAtLogin: true,
|
|
2984
3330
|
theme: "system",
|
|
2985
3331
|
claudeBridgeAsked: false,
|
|
2986
|
-
pinned: []
|
|
3332
|
+
pinned: [],
|
|
3333
|
+
webChats: false,
|
|
3334
|
+
agentFolder: null
|
|
2987
3335
|
};
|
|
2988
3336
|
async function loadSettings(file) {
|
|
2989
3337
|
let raw = {};
|
|
2990
3338
|
try {
|
|
2991
|
-
raw = JSON.parse(await (0,
|
|
3339
|
+
raw = JSON.parse(await (0, import_promises17.readFile)(file, "utf8"));
|
|
2992
3340
|
} catch {
|
|
2993
3341
|
raw = {};
|
|
2994
3342
|
}
|
|
@@ -3000,6 +3348,8 @@ async function loadSettings(file) {
|
|
|
3000
3348
|
if (typeof r.openAtLogin === "boolean") s.openAtLogin = r.openAtLogin;
|
|
3001
3349
|
if (isTheme(r.theme)) s.theme = r.theme;
|
|
3002
3350
|
if (typeof r.claudeBridgeAsked === "boolean") s.claudeBridgeAsked = r.claudeBridgeAsked;
|
|
3351
|
+
if (typeof r.webChats === "boolean") s.webChats = r.webChats;
|
|
3352
|
+
if (typeof r.agentFolder === "string" && r.agentFolder.startsWith("/")) s.agentFolder = r.agentFolder;
|
|
3003
3353
|
if (typeof r.terminalApp === "string" && r.terminalApp.trim()) s.terminalApp = r.terminalApp.trim();
|
|
3004
3354
|
if (typeof r.usageThreshold === "number" && r.usageThreshold > 0 && r.usageThreshold <= 100) s.usageThreshold = r.usageThreshold;
|
|
3005
3355
|
if (typeof r.modes === "object" && r.modes !== null) {
|
|
@@ -3011,8 +3361,8 @@ async function loadSettings(file) {
|
|
|
3011
3361
|
return s;
|
|
3012
3362
|
}
|
|
3013
3363
|
async function saveSettings(file, s) {
|
|
3014
|
-
await (0,
|
|
3015
|
-
await (0,
|
|
3364
|
+
await (0, import_promises17.mkdir)((0, import_node_path20.dirname)(file), { recursive: true });
|
|
3365
|
+
await (0, import_promises17.writeFile)(file, JSON.stringify(s, null, 2) + "\n");
|
|
3016
3366
|
}
|
|
3017
3367
|
|
|
3018
3368
|
// src/state.ts
|
|
@@ -3074,8 +3424,8 @@ function markHandedOff(state2, key, target, now, delivery) {
|
|
|
3074
3424
|
if (!t) return null;
|
|
3075
3425
|
t.status = "handed_off";
|
|
3076
3426
|
t.updatedAt = now;
|
|
3077
|
-
t.delivery = delivery ? { ...delivery, target, sentAt: now, state: "waiting" } : void 0;
|
|
3078
|
-
t.detail = t.delivery ? deliveryDetail(t.delivery) : `Handed off to ${
|
|
3427
|
+
t.delivery = delivery && !isWebChat(target) ? { ...delivery, target, sentAt: now, state: "waiting" } : void 0;
|
|
3428
|
+
t.detail = t.delivery ? deliveryDetail(t.delivery) : `Handed off to ${targetLabel(target)}${isWebChat(target) ? " \xB7 press Send there" : ""}`;
|
|
3079
3429
|
return t;
|
|
3080
3430
|
}
|
|
3081
3431
|
function pendingDeliveries(state2, now) {
|
|
@@ -3113,6 +3463,30 @@ function latestSession(state2) {
|
|
|
3113
3463
|
}
|
|
3114
3464
|
return best;
|
|
3115
3465
|
}
|
|
3466
|
+
var MAX_WEB_CHATS = 4;
|
|
3467
|
+
var webKey = (url) => `web:${url}`;
|
|
3468
|
+
var isWebKey = (key) => key.startsWith("web:");
|
|
3469
|
+
function rememberWebChat(state2, chats, capture, now) {
|
|
3470
|
+
const key = webKey(capture.url);
|
|
3471
|
+
const resets = capture.limit ? limitResetsAt(capture.limit, now) : null;
|
|
3472
|
+
const entry = {
|
|
3473
|
+
key,
|
|
3474
|
+
capture,
|
|
3475
|
+
status: capture.limit ? "limit" : "idle",
|
|
3476
|
+
detail: capture.limit ? `Limit hit${resets ? ` \xB7 resets ${clock(resets)}` : ""}` : "Idle",
|
|
3477
|
+
updatedAt: now
|
|
3478
|
+
};
|
|
3479
|
+
if (capture.limit && capture.chat === "claude-web") state2.limitedUntil["claude-code"] = resets ?? new Date(now.getTime() + DEFAULT_LIMIT_MS);
|
|
3480
|
+
chats.delete(key);
|
|
3481
|
+
chats.set(key, entry);
|
|
3482
|
+
while (chats.size > MAX_WEB_CHATS) chats.delete(chats.keys().next().value);
|
|
3483
|
+
return entry;
|
|
3484
|
+
}
|
|
3485
|
+
function markWebHandedOff(e, target, now) {
|
|
3486
|
+
e.status = "handed_off";
|
|
3487
|
+
e.updatedAt = now;
|
|
3488
|
+
e.detail = `Handed off to ${targetLabel(target)}${isWebChat(target) ? " \xB7 press Send there" : ""}`;
|
|
3489
|
+
}
|
|
3116
3490
|
|
|
3117
3491
|
// src/menu.ts
|
|
3118
3492
|
var HOTKEY = { accelerator: "Alt+H", label: "\u2325H" };
|
|
@@ -3120,7 +3494,7 @@ var DOT = { limit: "\u25CF", warning: "\u25D0", idle: "\u25CB", handed_off: "\u2
|
|
|
3120
3494
|
var MODES = ["app", "terminal"];
|
|
3121
3495
|
var THEME_LABEL = { system: "System", light: "Light", dark: "Dark" };
|
|
3122
3496
|
function sessionTitle(t) {
|
|
3123
|
-
return t.ref.title ?? (t.ref.cwd ? (0,
|
|
3497
|
+
return t.ref.title ?? (t.ref.cwd ? (0, import_node_path21.basename)(t.ref.cwd) : t.ref.id.slice(0, 8));
|
|
3124
3498
|
}
|
|
3125
3499
|
function buildMenuModel(sessions, settings2, available2) {
|
|
3126
3500
|
const nodes = [
|
|
@@ -3137,7 +3511,8 @@ function buildMenuModel(sessions, settings2, available2) {
|
|
|
3137
3511
|
{ type: "separator" },
|
|
3138
3512
|
...TOOLS.filter((x) => x !== t.ref.tool).map(
|
|
3139
3513
|
(x) => ({ label: `Hand off to ${TOOL_LABEL[x]}`, enabled: available2[x][settings2.modes[x]], action: { type: "handoff", key, target: x } })
|
|
3140
|
-
)
|
|
3514
|
+
),
|
|
3515
|
+
...WEB_CHATS.map((x) => ({ label: `Continue in ${WEB_LABEL[x]}`, action: { type: "handoff", key, target: x } }))
|
|
3141
3516
|
]
|
|
3142
3517
|
});
|
|
3143
3518
|
}
|
|
@@ -3178,33 +3553,502 @@ function buildMenuModel(sessions, settings2, available2) {
|
|
|
3178
3553
|
}
|
|
3179
3554
|
|
|
3180
3555
|
// src/health.ts
|
|
3181
|
-
var
|
|
3556
|
+
var import_promises18 = require("node:fs/promises");
|
|
3182
3557
|
var HealthMonitor = class {
|
|
3183
3558
|
context = new ContextTracker();
|
|
3184
3559
|
cache = /* @__PURE__ */ new Map();
|
|
3560
|
+
/** Whether each chat folder is in a git repo (see chatKind), looked up once. */
|
|
3561
|
+
repos = /* @__PURE__ */ new Map();
|
|
3185
3562
|
get(key) {
|
|
3186
3563
|
return this.cache.get(key)?.health ?? null;
|
|
3187
3564
|
}
|
|
3188
|
-
|
|
3565
|
+
kind(key) {
|
|
3566
|
+
return this.cache.get(key)?.kind ?? null;
|
|
3567
|
+
}
|
|
3568
|
+
/** Reads the given chats (one at a time, to stay light) and returns those whose state (or kind) changed. */
|
|
3189
3569
|
async update(refs) {
|
|
3190
3570
|
const changes = [];
|
|
3191
3571
|
for (const ref of refs) {
|
|
3192
3572
|
const key = sessionKey(ref);
|
|
3193
3573
|
try {
|
|
3194
|
-
const st = await (0,
|
|
3574
|
+
const st = await (0, import_promises18.stat)(ref.file);
|
|
3195
3575
|
const cached = this.cache.get(key);
|
|
3196
3576
|
if (cached && cached.size === st.size && cached.mtimeMs === st.mtimeMs) continue;
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3577
|
+
const session = await readSessionTail(ref);
|
|
3578
|
+
const health2 = assessHealth(session, await this.context.read(ref));
|
|
3579
|
+
const kind = chatKind(session, ref.cwd ? await this.inRepo(ref.cwd) : false);
|
|
3580
|
+
this.cache.set(key, { size: st.size, mtimeMs: st.mtimeMs, health: health2, kind });
|
|
3581
|
+
if (cached?.health.state !== health2.state || cached?.kind !== kind) changes.push({ ref, before: cached?.health.state ?? null, after: health2 });
|
|
3200
3582
|
} catch {
|
|
3201
3583
|
this.cache.delete(key);
|
|
3202
3584
|
}
|
|
3203
3585
|
}
|
|
3204
3586
|
return changes;
|
|
3205
3587
|
}
|
|
3588
|
+
inRepo(dir) {
|
|
3589
|
+
let known = this.repos.get(dir);
|
|
3590
|
+
if (!known) {
|
|
3591
|
+
known = inGitRepo(dir);
|
|
3592
|
+
this.repos.set(dir, known);
|
|
3593
|
+
}
|
|
3594
|
+
return known;
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
|
|
3598
|
+
// src/browser.ts
|
|
3599
|
+
var import_node_child_process4 = require("node:child_process");
|
|
3600
|
+
var import_node_events2 = require("node:events");
|
|
3601
|
+
function scanPage(root, ax, watch3, isLimit) {
|
|
3602
|
+
const SPEAKER = /^(?:You said|Claude responded|ChatGPT said)\b:?/i;
|
|
3603
|
+
const ME = /^You said\b/i;
|
|
3604
|
+
const SKIP = ["AXButton", "AXPopUpButton", "AXMenuButton", "AXCheckBox", "AXRadioButton", "AXToolbar", "AXTextArea", "AXTextField", "AXComboBox", "AXImage", "AXMenu", "AXMenuBar"];
|
|
3605
|
+
const SIDE = ["AXLandmarkNavigation", "AXLandmarkComplementary"];
|
|
3606
|
+
const LIST = /^(?:Chat messages|Conversation)$/;
|
|
3607
|
+
const AGO = /^\d+ (?:second|minute|hour|day|week|month|year)s? ago$/;
|
|
3608
|
+
const turns = [];
|
|
3609
|
+
let limit = null;
|
|
3610
|
+
const headingText = (h) => {
|
|
3611
|
+
const own = ax.text(h);
|
|
3612
|
+
if (own) return own;
|
|
3613
|
+
const first = ax.kids(h).find((k) => ax.role(k) === "AXStaticText");
|
|
3614
|
+
return first && ax.text(first) || "";
|
|
3615
|
+
};
|
|
3616
|
+
const isSpeaker = (n) => ax.role(n) === "AXHeading" && SPEAKER.test(headingText(n).trim());
|
|
3617
|
+
const speakerOf = (n) => {
|
|
3618
|
+
const article = ax.subrole(n) === "AXDocumentArticle";
|
|
3619
|
+
const search = (m, depth) => {
|
|
3620
|
+
for (const k of ax.kids(m)) {
|
|
3621
|
+
if (isSpeaker(k)) return k;
|
|
3622
|
+
if (article && depth < 3 && ax.role(k) === "AXGroup") {
|
|
3623
|
+
const found = search(k, depth + 1);
|
|
3624
|
+
if (found) return found;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return null;
|
|
3628
|
+
};
|
|
3629
|
+
return search(n, 0);
|
|
3630
|
+
};
|
|
3631
|
+
const message = (container, heading) => {
|
|
3632
|
+
const who = ME.test(headingText(heading).trim()) ? "me" : "agent";
|
|
3633
|
+
const read = (withHeading) => {
|
|
3634
|
+
let parts = [];
|
|
3635
|
+
const status = /* @__PURE__ */ new Set();
|
|
3636
|
+
const announced = (n) => {
|
|
3637
|
+
const t = ax.role(n) === "AXStaticText" ? ax.text(n)?.trim() : null;
|
|
3638
|
+
if (t) status.add(t);
|
|
3639
|
+
for (const k of ax.kids(n)) announced(k);
|
|
3640
|
+
};
|
|
3641
|
+
let ids = 0;
|
|
3642
|
+
const walk2 = (n, parent) => {
|
|
3643
|
+
const role = ax.role(n);
|
|
3644
|
+
const bubble = who === "me" && (role === "AXPopUpButton" || role === "AXButton");
|
|
3645
|
+
if (role && SKIP.includes(role) && !bubble) return;
|
|
3646
|
+
const sub = ax.subrole(n);
|
|
3647
|
+
if (sub === "AXApplicationGroup" && /actions/i.test(ax.desc(n) ?? "")) return;
|
|
3648
|
+
if (role === "AXGroup") {
|
|
3649
|
+
const isFile = (s) => !!s && /^[^\s/]+\.[A-Za-z0-9]{1,8}$/.test(s.trim());
|
|
3650
|
+
const label = sub === "AXApplicationGroup" ? ax.desc(n) : null;
|
|
3651
|
+
const button = ax.kids(n).find((k) => (ax.role(k) === "AXButton" || ax.role(k) === "AXPopUpButton") && isFile(ax.text(k)));
|
|
3652
|
+
const name = isFile(label) ? label : button ? ax.text(button) : null;
|
|
3653
|
+
if (name) {
|
|
3654
|
+
parts.push({ text: `(attached: ${name.trim()})`, parent: ++ids });
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
if (sub === "AXApplicationStatus") return announced(n);
|
|
3659
|
+
if (n === heading) {
|
|
3660
|
+
if (!withHeading) return;
|
|
3661
|
+
const id2 = ++ids;
|
|
3662
|
+
for (const k of ax.kids(n)) {
|
|
3663
|
+
if (ax.role(k) === "AXStaticText" && SPEAKER.test((ax.text(k) ?? "").trim())) continue;
|
|
3664
|
+
walk2(k, id2);
|
|
3665
|
+
}
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (role === "AXStaticText" || role === "AXListMarker") {
|
|
3669
|
+
const t = ax.text(n);
|
|
3670
|
+
if (t && !AGO.test(t.trim())) parts.push({ text: t, parent });
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
const id = ++ids;
|
|
3674
|
+
for (const k of ax.kids(n)) walk2(k, id);
|
|
3675
|
+
};
|
|
3676
|
+
walk2(container, 0);
|
|
3677
|
+
parts = parts.filter((p) => !status.has(p.text.trim()));
|
|
3678
|
+
let out = "";
|
|
3679
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3680
|
+
const p = parts[i];
|
|
3681
|
+
if (i > 0) {
|
|
3682
|
+
const prev = parts[i - 1];
|
|
3683
|
+
const spaced = /\s$/.test(prev.text) || /^[\s.,;:!?)\]]/.test(p.text);
|
|
3684
|
+
if (!spaced) out += prev.parent === p.parent ? " " : "\n";
|
|
3685
|
+
}
|
|
3686
|
+
out += p.text;
|
|
3687
|
+
}
|
|
3688
|
+
return out.replace(//g, "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3689
|
+
};
|
|
3690
|
+
const text = who === "me" ? read(true) : read(false) || read(true);
|
|
3691
|
+
return { who, text };
|
|
3692
|
+
};
|
|
3693
|
+
let last = null;
|
|
3694
|
+
const lastMessage = (list) => {
|
|
3695
|
+
let cur = list;
|
|
3696
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
3697
|
+
const kids = ax.kids(cur);
|
|
3698
|
+
let next = null;
|
|
3699
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
3700
|
+
const h = speakerOf(kids[i]);
|
|
3701
|
+
if (h) {
|
|
3702
|
+
last = { n: kids[i], h };
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (!next && ax.kids(kids[i]).length) next = kids[i];
|
|
3706
|
+
}
|
|
3707
|
+
if (!next) return;
|
|
3708
|
+
cur = next;
|
|
3709
|
+
}
|
|
3710
|
+
};
|
|
3711
|
+
const walk = (n, depth) => {
|
|
3712
|
+
if (depth > 60) return;
|
|
3713
|
+
const role = ax.role(n);
|
|
3714
|
+
if (role && SKIP.includes(role)) return;
|
|
3715
|
+
const sub = ax.subrole(n);
|
|
3716
|
+
if (sub && SIDE.includes(sub)) return;
|
|
3717
|
+
const h = speakerOf(n);
|
|
3718
|
+
if (h) {
|
|
3719
|
+
if (watch3) last = { n, h };
|
|
3720
|
+
else turns.push(message(n, h));
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
if (role === "AXStaticText" || sub === "AXApplicationAlert" || sub === "AXApplicationStatus") {
|
|
3724
|
+
const t = (role === "AXStaticText" ? ax.text(n) : ax.desc(n))?.trim();
|
|
3725
|
+
if (t && !limit && isLimit(t)) limit = t;
|
|
3726
|
+
if (role === "AXStaticText") return;
|
|
3727
|
+
}
|
|
3728
|
+
if (watch3 && LIST.test(ax.desc(n) ?? "")) {
|
|
3729
|
+
lastMessage(n);
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
for (const k of ax.kids(n)) walk(k, depth + 1);
|
|
3733
|
+
};
|
|
3734
|
+
walk(root, 0);
|
|
3735
|
+
const NOTICE = /^(?:you['’]?ve|you have|you['’]re|you are|usage limit|message limit|rate limit|limit reached)\b/i;
|
|
3736
|
+
const tail2 = watch3 ? last ? message(last.n, last.h) : void 0 : turns.at(-1);
|
|
3737
|
+
if (tail2 && tail2.who === "agent" && tail2.text.length < 250 && NOTICE.test(tail2.text) && isLimit(tail2.text)) {
|
|
3738
|
+
if (!limit) limit = tail2.text;
|
|
3739
|
+
if (!watch3) turns.pop();
|
|
3740
|
+
}
|
|
3741
|
+
return { turns: turns.filter((t) => t.text), limit };
|
|
3742
|
+
}
|
|
3743
|
+
var BROWSERS = {
|
|
3744
|
+
"com.google.Chrome": { name: "Google Chrome", chromium: true },
|
|
3745
|
+
"com.google.Chrome.beta": { name: "Google Chrome Beta", chromium: true },
|
|
3746
|
+
"com.google.Chrome.canary": { name: "Google Chrome Canary", chromium: true },
|
|
3747
|
+
"company.thebrowser.Browser": { name: "Arc", chromium: true },
|
|
3748
|
+
"company.thebrowser.dia": { name: "Dia", chromium: true },
|
|
3749
|
+
"com.brave.Browser": { name: "Brave Browser", chromium: true },
|
|
3750
|
+
"com.microsoft.edgemac": { name: "Microsoft Edge", chromium: true },
|
|
3751
|
+
"com.vivaldi.Vivaldi": { name: "Vivaldi", chromium: true },
|
|
3752
|
+
"com.operasoftware.Opera": { name: "Opera", chromium: true },
|
|
3753
|
+
"ai.perplexity.comet": { name: "Comet", chromium: true },
|
|
3754
|
+
"com.openai.atlas": { name: "ChatGPT Atlas", chromium: true },
|
|
3755
|
+
"com.apple.Safari": { name: "Safari", chromium: false },
|
|
3756
|
+
"com.apple.SafariTechnologyPreview": { name: "Safari Technology Preview", chromium: false },
|
|
3757
|
+
"org.mozilla.firefox": { name: "Firefox", chromium: false }
|
|
3758
|
+
};
|
|
3759
|
+
var CHAT_HOSTS = { "claude.ai": "claude-web", "chatgpt.com": "chatgpt", "chat.openai.com": "chatgpt" };
|
|
3760
|
+
function chatHosts(env = process.env) {
|
|
3761
|
+
const out = { ...CHAT_HOSTS };
|
|
3762
|
+
for (const pair of (env.HARLAY_WEBCHAT_TEST_HOSTS ?? "").split(",").filter(Boolean)) {
|
|
3763
|
+
const i = pair.lastIndexOf("=");
|
|
3764
|
+
const chat = pair.slice(i + 1);
|
|
3765
|
+
if (i > 0 && (chat === "claude-web" || chat === "chatgpt")) out[pair.slice(0, i)] = chat;
|
|
3766
|
+
}
|
|
3767
|
+
return out;
|
|
3768
|
+
}
|
|
3769
|
+
var SCRIPT = `
|
|
3770
|
+
ObjC.import('Cocoa');
|
|
3771
|
+
ObjC.import('CoreGraphics');
|
|
3772
|
+
ObjC.bindFunction('AXIsProcessTrusted', ['bool', []]);
|
|
3773
|
+
ObjC.bindFunction('AXUIElementCreateApplication', ['id', ['int']]);
|
|
3774
|
+
ObjC.bindFunction('AXUIElementCopyAttributeValue', ['int', ['id', 'id', 'id*']]);
|
|
3775
|
+
ObjC.bindFunction('AXUIElementSetAttributeValue', ['int', ['id', 'id', 'id']]);
|
|
3776
|
+
ObjC.bindFunction('getppid', ['int', []]);
|
|
3777
|
+
const isLimitText = ${isLimitText.toString()};
|
|
3778
|
+
const scanPage = ${scanPage.toString()};
|
|
3779
|
+
function get(el, name) { const r = Ref(); return $.AXUIElementCopyAttributeValue(el, name, r) === 0 ? r[0] : null; }
|
|
3780
|
+
function str(v) { if (!v) return null; try { const s = ObjC.unwrap(v); return typeof s === 'string' ? s : null; } catch (e) { return null; } }
|
|
3781
|
+
function node(el) { return { el: el, k: null, r: undefined, s: undefined, t: undefined, d: undefined }; }
|
|
3782
|
+
const ax = {
|
|
3783
|
+
role: (n) => (n.r === undefined ? (n.r = str(get(n.el, 'AXRole'))) : n.r),
|
|
3784
|
+
subrole: (n) => (n.s === undefined ? (n.s = str(get(n.el, 'AXSubrole'))) : n.s),
|
|
3785
|
+
desc: (n) => (n.d === undefined ? (n.d = str(get(n.el, 'AXDescription'))) : n.d),
|
|
3786
|
+
text: (n) => (n.t === undefined ? (n.t = str(get(n.el, 'AXValue')) || str(get(n.el, 'AXTitle')) || ax.desc(n)) : n.t),
|
|
3787
|
+
kids: (n) => {
|
|
3788
|
+
if (n.k) return n.k;
|
|
3789
|
+
const v = get(n.el, 'AXChildren');
|
|
3790
|
+
const out = [];
|
|
3791
|
+
if (v) for (let i = 0; i < v.count; i++) out.push(node(v.objectAtIndex(i)));
|
|
3792
|
+
return (n.k = out);
|
|
3793
|
+
},
|
|
3794
|
+
};
|
|
3795
|
+
function hostOf(url) { const m = /^https?:\\/\\/([^/?#]+)/i.exec(url || ''); return m ? m[1].toLowerCase() : ''; }
|
|
3796
|
+
function webArea(win, hosts) {
|
|
3797
|
+
let first = null;
|
|
3798
|
+
const find = (n, depth) => {
|
|
3799
|
+
if (depth > 14) return null;
|
|
3800
|
+
if (ax.role(n) === 'AXWebArea') {
|
|
3801
|
+
const u = get(n.el, 'AXURL');
|
|
3802
|
+
const url = u ? ObjC.unwrap(u.absoluteString) : '';
|
|
3803
|
+
if (hosts[hostOf(url)]) return { n: n, url: url };
|
|
3804
|
+
if (!first) first = { n: n, url: url };
|
|
3805
|
+
return null;
|
|
3806
|
+
}
|
|
3807
|
+
for (const k of ax.kids(n)) { const f = find(k, depth + 1); if (f) return f; }
|
|
3808
|
+
return null;
|
|
3809
|
+
};
|
|
3810
|
+
return find(win, 0) || first;
|
|
3811
|
+
}
|
|
3812
|
+
/** The web chat in the front browser window: its page, URL and title; or why there's none. */
|
|
3813
|
+
function front(cfg) {
|
|
3814
|
+
const app = $.NSWorkspace.sharedWorkspace.frontmostApplication;
|
|
3815
|
+
const id = app ? ObjC.unwrap(app.bundleIdentifier) : null;
|
|
3816
|
+
const browser = id && cfg.browsers[id];
|
|
3817
|
+
if (!browser) return { ok: false, reason: 'not-browser' };
|
|
3818
|
+
const el = $.AXUIElementCreateApplication(app.processIdentifier);
|
|
3819
|
+
if (browser.chromium) $.AXUIElementSetAttributeValue(el, 'AXManualAccessibility', $.NSNumber.numberWithBool(true));
|
|
3820
|
+
const win = get(el, 'AXFocusedWindow');
|
|
3821
|
+
if (!win) return { ok: false, reason: 'no-window' };
|
|
3822
|
+
const area = webArea(node(win), cfg.hosts);
|
|
3823
|
+
const chat = area && cfg.hosts[hostOf(area.url)];
|
|
3824
|
+
if (!chat) return { ok: false, reason: 'not-chat' };
|
|
3825
|
+
const title = str(get(area.n.el, 'AXTitle')) || ax.desc(area.n) || '';
|
|
3826
|
+
return { ok: true, browser: browser.name, chat: chat, url: area.url, title: title, page: area.n };
|
|
3827
|
+
}
|
|
3828
|
+
function wait(seconds) { $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(seconds)); }
|
|
3829
|
+
/** The page the browser's focused element is in, and whether that element takes text. */
|
|
3830
|
+
function focusedField(pid) {
|
|
3831
|
+
let n = get($.AXUIElementCreateApplication(pid), 'AXFocusedUIElement');
|
|
3832
|
+
const role = n ? str(get(n, 'AXRole')) : null;
|
|
3833
|
+
for (let i = 0; n && i < 40; i++) {
|
|
3834
|
+
if (str(get(n, 'AXRole')) === 'AXWebArea') {
|
|
3835
|
+
const u = get(n, 'AXURL');
|
|
3836
|
+
return { text: role === 'AXTextArea' || role === 'AXTextField', url: u ? ObjC.unwrap(u.absoluteString) : '' };
|
|
3837
|
+
}
|
|
3838
|
+
n = get(n, 'AXParent');
|
|
3839
|
+
}
|
|
3840
|
+
return null;
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* Attaches a file to the new chat Harlay just opened: once the browser is in front with the cursor in that chat's
|
|
3844
|
+
* message box (its site, on a new chat's page), it puts the file on the clipboard, presses \u2318V there, and puts the
|
|
3845
|
+
* clipboard back. Otherwise the file is left on the clipboard for the user to paste.
|
|
3846
|
+
*/
|
|
3847
|
+
function paste(cfg) {
|
|
3848
|
+
if (!$.NSFileManager.defaultManager.fileExistsAtPath(cfg.path)) return { ok: false, reason: 'no-file' };
|
|
3849
|
+
const pb = $.NSPasteboard.generalPasteboard;
|
|
3850
|
+
const end = Date.now() + cfg.timeout * 1000;
|
|
3851
|
+
let pid = null;
|
|
3852
|
+
while (Date.now() < end) {
|
|
3853
|
+
const app = $.NSWorkspace.sharedWorkspace.frontmostApplication;
|
|
3854
|
+
const id = app ? ObjC.unwrap(app.bundleIdentifier) : null;
|
|
3855
|
+
if (id && cfg.browsers[id]) {
|
|
3856
|
+
const f = focusedField(app.processIdentifier);
|
|
3857
|
+
const m = f && /^https?:\\/\\/([^/?#]+)(\\/[^?#]*)?/i.exec(f.url);
|
|
3858
|
+
if (f && f.text && m && cfg.hosts[m[1].toLowerCase()] === cfg.chat && cfg.paths.indexOf(m[2] || '/') !== -1) {
|
|
3859
|
+
pid = app.processIdentifier;
|
|
3860
|
+
break;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
wait(0.25);
|
|
3864
|
+
}
|
|
3865
|
+
const saved = [];
|
|
3866
|
+
if (pid) {
|
|
3867
|
+
const items = pb.pasteboardItems;
|
|
3868
|
+
for (let i = 0; i < items.count; i++) {
|
|
3869
|
+
const item = items.objectAtIndex(i);
|
|
3870
|
+
const kept = [];
|
|
3871
|
+
for (let j = 0; j < item.types.count; j++) {
|
|
3872
|
+
const t = item.types.objectAtIndex(j);
|
|
3873
|
+
const d = item.dataForType(t);
|
|
3874
|
+
if (d) kept.push([t, d]);
|
|
3875
|
+
}
|
|
3876
|
+
saved.push(kept);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
pb.clearContents;
|
|
3880
|
+
pb.writeObjects($([$.NSURL.fileURLWithPath(cfg.path)]));
|
|
3881
|
+
if (!pid) return { ok: false, reason: 'no-message-box' };
|
|
3882
|
+
// The page fills its message box as it loads: give it a moment.
|
|
3883
|
+
wait(0.6);
|
|
3884
|
+
for (const down of [true, false]) {
|
|
3885
|
+
const e = $.CGEventCreateKeyboardEvent($(), 9, down);
|
|
3886
|
+
$.CGEventSetFlags(e, $.kCGEventFlagMaskCommand);
|
|
3887
|
+
$.CGEventPostToPid(pid, e);
|
|
3888
|
+
}
|
|
3889
|
+
wait(1.5);
|
|
3890
|
+
pb.clearContents;
|
|
3891
|
+
const back = saved.map((kept) => {
|
|
3892
|
+
const item = $.NSPasteboardItem.alloc.init;
|
|
3893
|
+
for (const [t, d] of kept) item.setDataForType(d, t);
|
|
3894
|
+
return item;
|
|
3895
|
+
});
|
|
3896
|
+
if (back.length) pb.writeObjects($(back));
|
|
3897
|
+
return { ok: true };
|
|
3898
|
+
}
|
|
3899
|
+
function write(s) {
|
|
3900
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData($(s + '\\n').dataUsingEncoding($.NSUTF8StringEncoding));
|
|
3901
|
+
}
|
|
3902
|
+
function run(argv) {
|
|
3903
|
+
const cfg = JSON.parse(argv[0]);
|
|
3904
|
+
if (!$.AXIsProcessTrusted()) return JSON.stringify({ ok: false, reason: 'untrusted' });
|
|
3905
|
+
if (cfg.mode === 'paste') return JSON.stringify(paste(cfg));
|
|
3906
|
+
if (cfg.mode === 'read') {
|
|
3907
|
+
const f = front(cfg);
|
|
3908
|
+
if (!f.ok) return JSON.stringify(f);
|
|
3909
|
+
const scan = scanPage(f.page, ax, false, isLimitText);
|
|
3910
|
+
return JSON.stringify({ ok: true, browser: f.browser, chat: f.chat, url: f.url, title: f.title, turns: scan.turns, limit: scan.limit });
|
|
3911
|
+
}
|
|
3912
|
+
const parent = $.getppid();
|
|
3913
|
+
let last = '';
|
|
3914
|
+
let said = '';
|
|
3915
|
+
for (;;) {
|
|
3916
|
+
if ($.getppid() !== parent) return '';
|
|
3917
|
+
if (!$.AXIsProcessTrusted()) { write(JSON.stringify({ event: 'untrusted' })); return ''; }
|
|
3918
|
+
let key = '';
|
|
3919
|
+
try {
|
|
3920
|
+
const f = front(cfg);
|
|
3921
|
+
// HARLAY_DEBUG: what it sees, each time that changes.
|
|
3922
|
+
const now = f.ok ? f.url : f.reason;
|
|
3923
|
+
if (cfg.debug && now !== said) write(JSON.stringify({ event: 'status', front: now }));
|
|
3924
|
+
said = now;
|
|
3925
|
+
if (f.ok) {
|
|
3926
|
+
const scan = scanPage(f.page, ax, true, isLimitText);
|
|
3927
|
+
if (scan.limit) {
|
|
3928
|
+
key = f.url + '\\n' + scan.limit;
|
|
3929
|
+
if (key !== last) write(JSON.stringify({ event: 'limit', chat: f.chat, url: f.url, title: f.title, limit: scan.limit }));
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
} catch (e) {
|
|
3933
|
+
// a window that closed mid-read: try again next time
|
|
3934
|
+
if (cfg.debug) write(JSON.stringify({ event: 'status', error: String(e) }));
|
|
3935
|
+
}
|
|
3936
|
+
last = key;
|
|
3937
|
+
// Waits by running the event loop, not delay(): that's how macOS tells this process which app is in front now.
|
|
3938
|
+
$.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(cfg.interval));
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
`;
|
|
3942
|
+
function config(mode, hosts, interval = 2.5) {
|
|
3943
|
+
return JSON.stringify({ mode, hosts, browsers: BROWSERS, interval, debug: !!process.env.HARLAY_DEBUG });
|
|
3944
|
+
}
|
|
3945
|
+
function readFrontChat(hosts = chatHosts(), timeoutMs = 8e3) {
|
|
3946
|
+
return new Promise((resolve) => {
|
|
3947
|
+
(0, import_node_child_process4.execFile)("osascript", ["-l", "JavaScript", "-e", SCRIPT, config("read", hosts)], { timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024 }, (err, out) => {
|
|
3948
|
+
if (err) return resolve({ ok: false, reason: "failed" });
|
|
3949
|
+
try {
|
|
3950
|
+
const r = JSON.parse(out);
|
|
3951
|
+
if (r.ok !== true) return resolve({ ok: false, reason: r.reason ?? "failed" });
|
|
3952
|
+
const turns = Array.isArray(r.turns) ? r.turns.filter((t) => (t.who === "me" || t.who === "agent") && typeof t.text === "string") : [];
|
|
3953
|
+
resolve({
|
|
3954
|
+
ok: true,
|
|
3955
|
+
browser: String(r.browser),
|
|
3956
|
+
capture: { chat: r.chat, url: String(r.url), title: String(r.title ?? ""), turns, limit: typeof r.limit === "string" ? r.limit : null }
|
|
3957
|
+
});
|
|
3958
|
+
} catch {
|
|
3959
|
+
resolve({ ok: false, reason: "failed" });
|
|
3960
|
+
}
|
|
3961
|
+
});
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
var NEW_CHAT_PATHS = { "claude-web": ["/new"], chatgpt: ["/"] };
|
|
3965
|
+
function pasteFileInto(chat, file, hosts = chatHosts(), timeoutSec = 15) {
|
|
3966
|
+
const paths = Object.entries(hosts).some(([h, c]) => c === chat && !(h in CHAT_HOSTS)) ? [...NEW_CHAT_PATHS[chat], "/"] : NEW_CHAT_PATHS[chat];
|
|
3967
|
+
const cfg = JSON.stringify({ mode: "paste", chat, path: file, hosts, browsers: BROWSERS, paths, timeout: timeoutSec });
|
|
3968
|
+
return new Promise((resolve) => {
|
|
3969
|
+
(0, import_node_child_process4.execFile)("osascript", ["-l", "JavaScript", "-e", SCRIPT, cfg], { timeout: (timeoutSec + 10) * 1e3 }, (err, out) => {
|
|
3970
|
+
if (err) return resolve({ ok: false, reason: "failed" });
|
|
3971
|
+
try {
|
|
3972
|
+
resolve(JSON.parse(out));
|
|
3973
|
+
} catch {
|
|
3974
|
+
resolve({ ok: false, reason: "failed" });
|
|
3975
|
+
}
|
|
3976
|
+
});
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
var BrowserWatcher = class extends import_node_events2.EventEmitter {
|
|
3980
|
+
constructor(hosts = chatHosts()) {
|
|
3981
|
+
super();
|
|
3982
|
+
this.hosts = hosts;
|
|
3983
|
+
}
|
|
3984
|
+
hosts;
|
|
3985
|
+
child = null;
|
|
3986
|
+
wanted = false;
|
|
3987
|
+
timer = null;
|
|
3988
|
+
start() {
|
|
3989
|
+
this.wanted = true;
|
|
3990
|
+
if (this.child) return;
|
|
3991
|
+
const child = (0, import_node_child_process4.spawn)("osascript", ["-l", "JavaScript", "-e", SCRIPT, config("watch", this.hosts)], { stdio: ["ignore", "pipe", "ignore"] });
|
|
3992
|
+
this.child = child;
|
|
3993
|
+
let buf = "";
|
|
3994
|
+
let untrusted = false;
|
|
3995
|
+
child.stdout?.setEncoding("utf8");
|
|
3996
|
+
child.stdout?.on("data", (chunk) => {
|
|
3997
|
+
buf += chunk;
|
|
3998
|
+
for (let nl = buf.indexOf("\n"); nl !== -1; nl = buf.indexOf("\n")) {
|
|
3999
|
+
const line = buf.slice(0, nl);
|
|
4000
|
+
buf = buf.slice(nl + 1);
|
|
4001
|
+
try {
|
|
4002
|
+
const ev = JSON.parse(line);
|
|
4003
|
+
if (ev.event === "untrusted") untrusted = true;
|
|
4004
|
+
else if (ev.event === "status") this.emit("status", ev);
|
|
4005
|
+
else if (ev.event === "limit") this.emit("limit", { chat: ev.chat, url: ev.url, title: ev.title, limit: ev.limit });
|
|
4006
|
+
} catch {
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
});
|
|
4010
|
+
child.on("exit", () => {
|
|
4011
|
+
this.child = null;
|
|
4012
|
+
if (untrusted) this.emit("untrusted");
|
|
4013
|
+
if (this.wanted && !untrusted) this.timer = setTimeout(() => this.start(), 5e3);
|
|
4014
|
+
else this.wanted = false;
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
get running() {
|
|
4018
|
+
return this.child !== null;
|
|
4019
|
+
}
|
|
4020
|
+
stop() {
|
|
4021
|
+
this.wanted = false;
|
|
4022
|
+
if (this.timer) clearTimeout(this.timer);
|
|
4023
|
+
this.timer = null;
|
|
4024
|
+
this.child?.kill();
|
|
4025
|
+
this.child = null;
|
|
4026
|
+
}
|
|
3206
4027
|
};
|
|
3207
4028
|
|
|
4029
|
+
// src/targets.ts
|
|
4030
|
+
function pickTargets(source, defaultTarget, available2, modes, limitedUntil = {}, now = /* @__PURE__ */ new Date()) {
|
|
4031
|
+
const order = [defaultTarget, ...TOOLS.filter((t) => t !== defaultTarget)];
|
|
4032
|
+
return order.filter((t) => t !== source && available2[t][modes[t]] && !((limitedUntil[t]?.getTime() ?? 0) > now.getTime()));
|
|
4033
|
+
}
|
|
4034
|
+
var SHARES_USAGE = { "claude-web": "claude-code" };
|
|
4035
|
+
function webTargets(source, hitLimit, limitedUntil = {}, now = /* @__PURE__ */ new Date()) {
|
|
4036
|
+
const out = [];
|
|
4037
|
+
for (const w of ["claude-web", "chatgpt"]) {
|
|
4038
|
+
if (w === source) continue;
|
|
4039
|
+
const shared = SHARES_USAGE[w];
|
|
4040
|
+
if (shared && (hitLimit && shared === source || (limitedUntil[shared]?.getTime() ?? 0) > now.getTime())) continue;
|
|
4041
|
+
out.push(w);
|
|
4042
|
+
}
|
|
4043
|
+
return out;
|
|
4044
|
+
}
|
|
4045
|
+
function orderTargets(agents, web, kind) {
|
|
4046
|
+
return kind === "chat" ? [...web, ...agents] : [...agents, ...web];
|
|
4047
|
+
}
|
|
4048
|
+
function isPrimary(t, kind) {
|
|
4049
|
+
return isWebChat(t) === (kind === "chat");
|
|
4050
|
+
}
|
|
4051
|
+
|
|
3208
4052
|
// src/popover.ts
|
|
3209
4053
|
var POPOVER_WIDTH = 392;
|
|
3210
4054
|
var PER_AGENT = 3;
|
|
@@ -3263,11 +4107,14 @@ function shownByAgent(sessions, settings2) {
|
|
|
3263
4107
|
}
|
|
3264
4108
|
return out;
|
|
3265
4109
|
}
|
|
3266
|
-
function buildPopoverModel(sessions, settings2, available2, busy, now, usage2 = [], claudeBridge2 = "off", askClaudeBridge = false, icons = {}, health2 = () => null) {
|
|
4110
|
+
function buildPopoverModel(sessions, settings2, available2, busy, now, usage2 = [], claudeBridge2 = "off", askClaudeBridge = false, icons = {}, health2 = () => null, kind = () => null, limitedUntil = {}, web = { chats: [], folder: null, allowed: false }) {
|
|
3267
4111
|
const pinned = new Set(settings2.pinned);
|
|
3268
4112
|
const order = [settings2.defaultTarget, ...TOOLS.filter((t) => t !== settings2.defaultTarget)];
|
|
3269
4113
|
const toSession = (t) => {
|
|
3270
4114
|
const key = sessionKey(t.ref);
|
|
4115
|
+
const k = kind(key);
|
|
4116
|
+
const agents = order.filter((x) => x !== t.ref.tool && available2[x][settings2.modes[x]]);
|
|
4117
|
+
const web2 = webTargets(t.ref.tool, t.status === "limit", limitedUntil, now);
|
|
3271
4118
|
return {
|
|
3272
4119
|
key,
|
|
3273
4120
|
title: sessionTitle(t),
|
|
@@ -3277,7 +4124,8 @@ function buildPopoverModel(sessions, settings2, available2, busy, now, usage2 =
|
|
|
3277
4124
|
ago: timeAgo(t.updatedAt, now),
|
|
3278
4125
|
busy: busy.has(key),
|
|
3279
4126
|
pinned: pinned.has(key),
|
|
3280
|
-
targets:
|
|
4127
|
+
targets: orderTargets(agents, web2, k).map((x) => ({ tool: x, label: targetLabel(x), mode: isWebChat(x) ? "web" : settings2.modes[x], primary: isPrimary(x, k) })),
|
|
4128
|
+
kind: k,
|
|
3281
4129
|
health: health2(key),
|
|
3282
4130
|
canStartFresh: available2[t.ref.tool][settings2.modes[t.ref.tool]],
|
|
3283
4131
|
delivery: t.status === "handed_off" ? t.delivery?.state ?? null : null
|
|
@@ -3294,20 +4142,53 @@ function buildPopoverModel(sessions, settings2, available2, busy, now, usage2 =
|
|
|
3294
4142
|
usage: usage2,
|
|
3295
4143
|
icons,
|
|
3296
4144
|
askClaudeBridge,
|
|
3297
|
-
settings: {
|
|
4145
|
+
settings: {
|
|
4146
|
+
autoContinue: settings2.autoContinue,
|
|
4147
|
+
openAtLogin: settings2.openAtLogin,
|
|
4148
|
+
theme: settings2.theme,
|
|
4149
|
+
claudeBridge: claudeBridge2,
|
|
4150
|
+
webChats: settings2.webChats,
|
|
4151
|
+
webChatsAllowed: web.allowed
|
|
4152
|
+
},
|
|
4153
|
+
webChats: web.chats,
|
|
4154
|
+
agentFolder: web.folder,
|
|
3298
4155
|
hotkey: HOTKEY.label
|
|
3299
4156
|
};
|
|
3300
4157
|
}
|
|
4158
|
+
function buildWebSessions(entries, settings2, available2, busy, now, limitedUntil = {}) {
|
|
4159
|
+
const order = [settings2.defaultTarget, ...TOOLS.filter((t) => t !== settings2.defaultTarget)];
|
|
4160
|
+
const agents = order.filter((x) => available2[x][settings2.modes[x]] && !((limitedUntil[x]?.getTime() ?? 0) > now.getTime()));
|
|
4161
|
+
return [...entries].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()).map((e) => {
|
|
4162
|
+
const web = webTargets(e.capture.chat, e.status === "limit", limitedUntil, now);
|
|
4163
|
+
return {
|
|
4164
|
+
key: e.key,
|
|
4165
|
+
title: webChatTitle(e.capture),
|
|
4166
|
+
status: e.status,
|
|
4167
|
+
detail: e.detail,
|
|
4168
|
+
cwd: null,
|
|
4169
|
+
ago: timeAgo(e.updatedAt, now),
|
|
4170
|
+
busy: busy.has(e.key),
|
|
4171
|
+
pinned: false,
|
|
4172
|
+
targets: orderTargets(agents, web, "chat").map((x) => ({ tool: x, label: targetLabel(x), mode: isWebChat(x) ? "web" : settings2.modes[x], primary: isPrimary(x, "chat") })),
|
|
4173
|
+
kind: "chat",
|
|
4174
|
+
health: null,
|
|
4175
|
+
canStartFresh: false,
|
|
4176
|
+
delivery: null,
|
|
4177
|
+
web: e.capture.chat
|
|
4178
|
+
};
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
3301
4181
|
function isMenuAction(x) {
|
|
3302
4182
|
if (typeof x !== "object" || x === null) return false;
|
|
3303
4183
|
const a = x;
|
|
3304
4184
|
switch (a.type) {
|
|
3305
4185
|
case "handoff":
|
|
3306
|
-
return typeof a.key === "string" &&
|
|
4186
|
+
return typeof a.key === "string" && isTarget(a.target);
|
|
3307
4187
|
case "unpinChat":
|
|
3308
4188
|
case "freshStart":
|
|
3309
4189
|
case "resendHandoff":
|
|
3310
4190
|
case "copyPrompt":
|
|
4191
|
+
case "pickFolder":
|
|
3311
4192
|
return typeof a.key === "string";
|
|
3312
4193
|
case "setTheme":
|
|
3313
4194
|
return isTheme(a.theme);
|
|
@@ -3320,6 +4201,7 @@ function isMenuAction(x) {
|
|
|
3320
4201
|
case "toggleAuto":
|
|
3321
4202
|
case "toggleLogin":
|
|
3322
4203
|
case "toggleClaudeBridge":
|
|
4204
|
+
case "toggleWebChats":
|
|
3323
4205
|
case "openCursorUsage":
|
|
3324
4206
|
case "quit":
|
|
3325
4207
|
return true;
|
|
@@ -3336,7 +4218,7 @@ function panelPosition(icon, iconArea, userArea, width) {
|
|
|
3336
4218
|
|
|
3337
4219
|
// src/overlay.ts
|
|
3338
4220
|
var import_electron = require("electron");
|
|
3339
|
-
var
|
|
4221
|
+
var import_node_path22 = require("node:path");
|
|
3340
4222
|
var MAX_SHOW_MS = 16e3;
|
|
3341
4223
|
var Overlay = class {
|
|
3342
4224
|
win = null;
|
|
@@ -3392,21 +4274,21 @@ var Overlay = class {
|
|
|
3392
4274
|
type: "panel",
|
|
3393
4275
|
enableLargerThanScreen: true,
|
|
3394
4276
|
alwaysOnTop: true,
|
|
3395
|
-
webPreferences: { preload: (0,
|
|
4277
|
+
webPreferences: { preload: (0, import_node_path22.join)(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false }
|
|
3396
4278
|
});
|
|
3397
4279
|
win.setAlwaysOnTop(true, "screen-saver");
|
|
3398
4280
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
3399
4281
|
win.setIgnoreMouseEvents(true);
|
|
3400
4282
|
win.webContents.on("will-navigate", (e) => e.preventDefault());
|
|
3401
4283
|
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
3402
|
-
void win.loadFile((0,
|
|
4284
|
+
void win.loadFile((0, import_node_path22.join)(__dirname, "..", "renderer", "overlay.html"));
|
|
3403
4285
|
this.win = win;
|
|
3404
4286
|
return win;
|
|
3405
4287
|
}
|
|
3406
4288
|
};
|
|
3407
4289
|
|
|
3408
4290
|
// src/show.ts
|
|
3409
|
-
var
|
|
4291
|
+
var import_node_child_process5 = require("node:child_process");
|
|
3410
4292
|
var tokensIn = (chars) => Math.max(1, Math.round(chars / 4));
|
|
3411
4293
|
var plural = (n, one) => `${n.toLocaleString("en-US")} ${one}${n === 1 ? "" : "s"}`;
|
|
3412
4294
|
function startView(o) {
|
|
@@ -3415,14 +4297,14 @@ function startView(o) {
|
|
|
3415
4297
|
const why = stopped || o.status === "warning" ? o.detail.replace(/^Limit hit/, "usage limit").replace(/ · /g, ", ") : fresh ? "new chat, same task" : "moved by you";
|
|
3416
4298
|
return {
|
|
3417
4299
|
key: o.key,
|
|
3418
|
-
from: { tool: o.from, label:
|
|
3419
|
-
to: { tool: o.to, label: fresh ? "New chat" :
|
|
4300
|
+
from: { tool: o.from, label: targetLabel(o.from) },
|
|
4301
|
+
to: { tool: o.to, label: fresh ? "New chat" : targetLabel(o.to), mode: o.mode },
|
|
3420
4302
|
title: o.title,
|
|
3421
4303
|
tag: stopped ? "stopped" : "moving",
|
|
3422
4304
|
steps: [
|
|
3423
4305
|
{ name: "Detect", state: "run", detail: why.charAt(0).toLowerCase() + why.slice(1) },
|
|
3424
4306
|
{ name: "Extract", state: "wait", detail: "reading the chat and git" },
|
|
3425
|
-
{ name: "Distill", state: "wait", detail: "into one .harlay file" },
|
|
4307
|
+
{ name: "Distill", state: "wait", detail: isWebChat(o.to) ? "into one message" : "into one .harlay file" },
|
|
3426
4308
|
{ name: "Rehydrate", state: "wait", detail: rehydrateDetail(o.to, o.mode, fresh, false) }
|
|
3427
4309
|
],
|
|
3428
4310
|
count: null,
|
|
@@ -3431,6 +4313,7 @@ function startView(o) {
|
|
|
3431
4313
|
};
|
|
3432
4314
|
}
|
|
3433
4315
|
function rehydrateDetail(to, mode, fresh, done) {
|
|
4316
|
+
if (isWebChat(to)) return done ? `${targetLabel(to)} \xB7 press \u23CE to send` : `opening ${targetLabel(to)} in your browser`;
|
|
3434
4317
|
const where = `${fresh ? `new ${TOOL_LABEL[to]} chat` : TOOL_LABEL[to]}${mode === "terminal" ? " in Terminal" : ""}`;
|
|
3435
4318
|
if (!done) return `opening ${where}`;
|
|
3436
4319
|
return mode === "app" ? `${where} \xB7 press \u23CE to send` : `${where} \xB7 picking it up`;
|
|
@@ -3475,7 +4358,7 @@ JSON.stringify(l.filter(function (w) { return w.kCGWindowLayer === 0 && w.kCGWin
|
|
|
3475
4358
|
}));`;
|
|
3476
4359
|
function listWindows() {
|
|
3477
4360
|
return new Promise((resolve) => {
|
|
3478
|
-
(0,
|
|
4361
|
+
(0, import_node_child_process5.execFile)("osascript", ["-l", "JavaScript", "-e", LIST_WINDOWS], { timeout: 2e3 }, (err, out) => {
|
|
3479
4362
|
if (err) return resolve([]);
|
|
3480
4363
|
try {
|
|
3481
4364
|
const list = JSON.parse(out);
|
|
@@ -3486,7 +4369,9 @@ function listWindows() {
|
|
|
3486
4369
|
});
|
|
3487
4370
|
});
|
|
3488
4371
|
}
|
|
4372
|
+
var BROWSERS2 = ["Google Chrome", "Safari", "Arc", "Firefox", "Brave Browser", "Microsoft Edge", "Dia", "Comet", "Opera", "Vivaldi", "Orion", "Zen", "ChatGPT Atlas"];
|
|
3489
4373
|
function windowOwners(target, mode, terminalApp) {
|
|
4374
|
+
if (isWebChat(target)) return BROWSERS2;
|
|
3490
4375
|
return mode === "terminal" ? [terminalApp] : APP_BUNDLES[target].map((b) => b.replace(/\.app$/, ""));
|
|
3491
4376
|
}
|
|
3492
4377
|
function ownedBy(app2, owners) {
|
|
@@ -3497,6 +4382,10 @@ function ownedBy(app2, owners) {
|
|
|
3497
4382
|
});
|
|
3498
4383
|
}
|
|
3499
4384
|
var isMain = (w) => w.width >= 320 && w.height >= 240;
|
|
4385
|
+
async function frontWindow() {
|
|
4386
|
+
const w = (await listWindows()).find(isMain);
|
|
4387
|
+
return w ? rect(w) : null;
|
|
4388
|
+
}
|
|
3500
4389
|
async function windowIds() {
|
|
3501
4390
|
return new Set((await listWindows()).map((w) => w.id));
|
|
3502
4391
|
}
|
|
@@ -3507,24 +4396,33 @@ async function findTargetWindow(owners, o) {
|
|
|
3507
4396
|
const all = (await listWindows()).filter(isMain);
|
|
3508
4397
|
const mine = all.filter((w) => ownedBy(w.app, owners));
|
|
3509
4398
|
const found = before ? mine.find((w) => !before.has(w.id)) : all[0] && ownedBy(all[0].app, owners) ? all[0] : void 0;
|
|
3510
|
-
|
|
3511
|
-
if (
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
}
|
|
4399
|
+
const r = found ? await settled(found) : null;
|
|
4400
|
+
if (r) return r;
|
|
4401
|
+
if (Date.now() >= end) return mine[0] ? settled(mine[0]) : null;
|
|
4402
|
+
await sleep(found ? 50 : 250);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
async function settled(w) {
|
|
4406
|
+
let last = rect(w);
|
|
4407
|
+
const end = Date.now() + SETTLE_MS;
|
|
4408
|
+
while (Date.now() < end) {
|
|
4409
|
+
await sleep(150);
|
|
4410
|
+
const now = (await listWindows()).find((x) => x.id === w.id);
|
|
4411
|
+
if (!now) return null;
|
|
4412
|
+
const r = rect(now);
|
|
4413
|
+
if (r.x === last.x && r.y === last.y && r.width === last.width && r.height === last.height) return r;
|
|
4414
|
+
last = r;
|
|
4415
|
+
}
|
|
4416
|
+
return last;
|
|
4417
|
+
}
|
|
4418
|
+
var SETTLE_MS = 2e3;
|
|
3515
4419
|
var rect = (w) => ({ x: w.x, y: w.y, width: w.width, height: w.height });
|
|
3516
4420
|
|
|
3517
|
-
// src/targets.ts
|
|
3518
|
-
function pickTargets(source, defaultTarget, available2, modes, limitedUntil = {}, now = /* @__PURE__ */ new Date()) {
|
|
3519
|
-
const order = [defaultTarget, ...TOOLS.filter((t) => t !== defaultTarget)];
|
|
3520
|
-
return order.filter((t) => t !== source && available2[t][modes[t]] && !((limitedUntil[t]?.getTime() ?? 0) > now.getTime()));
|
|
3521
|
-
}
|
|
3522
|
-
|
|
3523
4421
|
// src/usage.ts
|
|
3524
|
-
var
|
|
4422
|
+
var import_node_events3 = require("node:events");
|
|
3525
4423
|
var import_node_fs4 = require("node:fs");
|
|
3526
|
-
var
|
|
3527
|
-
var
|
|
4424
|
+
var import_promises19 = require("node:fs/promises");
|
|
4425
|
+
var import_node_path23 = require("node:path");
|
|
3528
4426
|
var CODEX_STALE_MS = 30 * 6e4;
|
|
3529
4427
|
var CODEX_QUERY_GAP_MS = 30 * 6e4;
|
|
3530
4428
|
var DEBOUNCE_MS = 2e3;
|
|
@@ -3535,7 +4433,7 @@ function clock2(d, now) {
|
|
|
3535
4433
|
return d.getTime() - now.getTime() < 20 * 36e5 ? time : `${d.toLocaleDateString([], { weekday: "short" })} ${time}`;
|
|
3536
4434
|
}
|
|
3537
4435
|
var NAME = { "claude-code": "Claude", codex: "Codex", cursor: "Cursor" };
|
|
3538
|
-
var UsageMonitor = class extends
|
|
4436
|
+
var UsageMonitor = class extends import_node_events3.EventEmitter {
|
|
3539
4437
|
constructor(opts) {
|
|
3540
4438
|
super();
|
|
3541
4439
|
this.opts = opts;
|
|
@@ -3555,15 +4453,15 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3555
4453
|
return claudeStatusFile(this.opts.dataDir);
|
|
3556
4454
|
}
|
|
3557
4455
|
get samplesFile() {
|
|
3558
|
-
return (0,
|
|
4456
|
+
return (0, import_node_path23.join)(this.opts.dataDir, "usage-calibration.json");
|
|
3559
4457
|
}
|
|
3560
4458
|
current() {
|
|
3561
4459
|
return this.snapshots;
|
|
3562
4460
|
}
|
|
3563
4461
|
async start() {
|
|
3564
|
-
await (0,
|
|
4462
|
+
await (0, import_promises19.mkdir)(this.opts.dataDir, { recursive: true });
|
|
3565
4463
|
try {
|
|
3566
|
-
const raw = JSON.parse(await (0,
|
|
4464
|
+
const raw = JSON.parse(await (0, import_promises19.readFile)(this.samplesFile, "utf8"));
|
|
3567
4465
|
if (Array.isArray(raw)) this.samples = raw.filter((s) => typeof s?.capacity === "number" && typeof s?.at === "number" && typeof s?.kind === "string");
|
|
3568
4466
|
} catch {
|
|
3569
4467
|
this.samples = [];
|
|
@@ -3578,8 +4476,8 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3578
4476
|
} catch {
|
|
3579
4477
|
}
|
|
3580
4478
|
};
|
|
3581
|
-
watchDir((0,
|
|
3582
|
-
watchDir((0,
|
|
4479
|
+
watchDir((0, import_node_path23.join)(this.opts.home, ".codex", "sessions"), "codex", true);
|
|
4480
|
+
watchDir((0, import_node_path23.join)(this.opts.home, ".claude", "projects"), "claude-code", true);
|
|
3583
4481
|
watchDir(this.opts.dataDir, "claude-code", false);
|
|
3584
4482
|
await Promise.all([this.update("codex"), this.update("claude-code")]);
|
|
3585
4483
|
void this.topUpCodex();
|
|
@@ -3636,8 +4534,8 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3636
4534
|
await this.claudeLog.refresh(now);
|
|
3637
4535
|
let exact = null;
|
|
3638
4536
|
try {
|
|
3639
|
-
const s = await (0,
|
|
3640
|
-
exact = parseClaudeStatusline(JSON.parse(await (0,
|
|
4537
|
+
const s = await (0, import_promises19.stat)(this.statusFile);
|
|
4538
|
+
exact = parseClaudeStatusline(JSON.parse(await (0, import_promises19.readFile)(this.statusFile, "utf8")), s.mtimeMs);
|
|
3641
4539
|
} catch {
|
|
3642
4540
|
exact = null;
|
|
3643
4541
|
}
|
|
@@ -3646,7 +4544,7 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3646
4544
|
const merged = mergeSamples(this.samples, capacitySamples(entries, hits, exact), now);
|
|
3647
4545
|
if (JSON.stringify(merged) !== JSON.stringify(this.samples)) {
|
|
3648
4546
|
this.samples = merged;
|
|
3649
|
-
await (0,
|
|
4547
|
+
await (0, import_promises19.writeFile)(this.samplesFile, JSON.stringify(merged)).catch(() => void 0);
|
|
3650
4548
|
}
|
|
3651
4549
|
return claudeSnapshot({ entries, hits, exact, samples: this.samples, now });
|
|
3652
4550
|
}
|
|
@@ -3681,7 +4579,7 @@ var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
|
3681
4579
|
var MAX_LIVE_NOTIFICATIONS = 50;
|
|
3682
4580
|
var AUTO_HANDOFF_COOLDOWN_MS = 2 * 6e4;
|
|
3683
4581
|
var home = process.env.HARLAY_HOME ?? (0, import_node_os3.homedir)();
|
|
3684
|
-
if (process.env.HARLAY_HOME) import_electron2.app.setPath("userData", (0,
|
|
4582
|
+
if (process.env.HARLAY_HOME) import_electron2.app.setPath("userData", (0, import_node_path24.join)(home, "Library", "Application Support", "Harlay"));
|
|
3685
4583
|
var readers = createReaders({ home });
|
|
3686
4584
|
var state = createState();
|
|
3687
4585
|
var liveNotifications = /* @__PURE__ */ new Set();
|
|
@@ -3698,6 +4596,9 @@ var claudeBridge = "off";
|
|
|
3698
4596
|
var lastAutoHandoffAt = 0;
|
|
3699
4597
|
var agentIcons = {};
|
|
3700
4598
|
var health = new HealthMonitor();
|
|
4599
|
+
var webChats = /* @__PURE__ */ new Map();
|
|
4600
|
+
var browserWatcher = new BrowserWatcher();
|
|
4601
|
+
var browserAllowed = false;
|
|
3701
4602
|
var overlay = new Overlay();
|
|
3702
4603
|
var showing = null;
|
|
3703
4604
|
var errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
@@ -3721,7 +4622,21 @@ function refreshUi() {
|
|
|
3721
4622
|
const rows = buildUsageRows(usage?.current() ?? {}, available, claudeBridge, now);
|
|
3722
4623
|
const claudeInstalled = available["claude-code"].app || available["claude-code"].terminal;
|
|
3723
4624
|
const ask = !settings.claudeBridgeAsked && claudeBridge === "off" && claudeInstalled;
|
|
3724
|
-
const model = buildPopoverModel(
|
|
4625
|
+
const model = buildPopoverModel(
|
|
4626
|
+
[...state.sessions.values()],
|
|
4627
|
+
settings,
|
|
4628
|
+
available,
|
|
4629
|
+
handoffsInFlight,
|
|
4630
|
+
now,
|
|
4631
|
+
rows,
|
|
4632
|
+
claudeBridge,
|
|
4633
|
+
ask,
|
|
4634
|
+
agentIcons,
|
|
4635
|
+
(k) => health.get(k),
|
|
4636
|
+
(k) => health.kind(k),
|
|
4637
|
+
state.limitedUntil,
|
|
4638
|
+
{ chats: buildWebSessions([...webChats.values()], settings, available, handoffsInFlight, now, state.limitedUntil), folder: agentFolder(), allowed: browserAllowed }
|
|
4639
|
+
);
|
|
3725
4640
|
popover.webContents.send("harlay:model", model);
|
|
3726
4641
|
}
|
|
3727
4642
|
function nativeMenu() {
|
|
@@ -3750,13 +4665,16 @@ async function handoff(ref, target) {
|
|
|
3750
4665
|
handoffsInFlight.add(key);
|
|
3751
4666
|
refreshUi();
|
|
3752
4667
|
const since = /* @__PURE__ */ new Date();
|
|
3753
|
-
const mode = settings.modes[target];
|
|
3754
|
-
const
|
|
4668
|
+
const mode = isWebChat(target) ? "web" : settings.modes[target];
|
|
4669
|
+
const t = state.sessions.get(key);
|
|
4670
|
+
const show = presentHandoff({ key, tool: ref.tool, title: t ? sessionTitle(t) : "This chat", status: t?.status ?? "idle", detail: t?.detail ?? "" }, target, mode);
|
|
3755
4671
|
try {
|
|
3756
4672
|
const res = await runHandoff({
|
|
3757
4673
|
session: ref,
|
|
3758
4674
|
target,
|
|
3759
|
-
|
|
4675
|
+
// A web chat opens in the browser whatever this says.
|
|
4676
|
+
mode: mode === "web" ? "app" : mode,
|
|
4677
|
+
attach: canAttach(),
|
|
3760
4678
|
readers,
|
|
3761
4679
|
store: handoffStore(home),
|
|
3762
4680
|
terminalApp: settings.terminalApp,
|
|
@@ -3764,9 +4682,10 @@ async function handoff(ref, target) {
|
|
|
3764
4682
|
onProgress: show.progress
|
|
3765
4683
|
});
|
|
3766
4684
|
const { id, path, prompt } = res.written;
|
|
3767
|
-
markHandedOff(state, key, target, /* @__PURE__ */ new Date(), { id, path, prompt, mode, cwd: res.data.workdir, since });
|
|
4685
|
+
markHandedOff(state, key, target, /* @__PURE__ */ new Date(), mode === "web" ? void 0 : { id, path, prompt, mode, cwd: res.data.workdir, since });
|
|
3768
4686
|
closeResolvedAlert();
|
|
3769
4687
|
void show.launched();
|
|
4688
|
+
if (isWebChat(target) && canAttach()) void attach(target, path);
|
|
3770
4689
|
} catch (e) {
|
|
3771
4690
|
show.failed(errorMessage(e));
|
|
3772
4691
|
notify("Harlay could not hand off", errorMessage(e));
|
|
@@ -3775,10 +4694,9 @@ async function handoff(ref, target) {
|
|
|
3775
4694
|
}
|
|
3776
4695
|
refreshUi();
|
|
3777
4696
|
}
|
|
3778
|
-
function presentHandoff(
|
|
3779
|
-
const key =
|
|
3780
|
-
|
|
3781
|
-
let view = startView({ key, from: ref.tool, to: target, mode, title: t ? sessionTitle(t) : "This chat", status: t?.status ?? "idle", detail: t?.detail ?? "" });
|
|
4697
|
+
function presentHandoff(src, target, mode) {
|
|
4698
|
+
const { key } = src;
|
|
4699
|
+
let view = startView({ key, from: src.tool, to: target, mode, title: src.title, status: src.status, detail: src.detail });
|
|
3782
4700
|
const set = (v) => {
|
|
3783
4701
|
view = v;
|
|
3784
4702
|
if (showing === key && popover && !popover.isDestroyed()) popover.webContents.send("harlay:handoff", v);
|
|
@@ -3802,16 +4720,17 @@ function presentHandoff(ref, target, mode) {
|
|
|
3802
4720
|
const to = tokensIn(p.chars);
|
|
3803
4721
|
const from = fromTokens ?? to;
|
|
3804
4722
|
if (from > to) void pace.next(0, () => set({ ...view, count: { from, to } }));
|
|
3805
|
-
const size = from > to ? void 0 : `${to.toLocaleString("en-US")} tokens in one .harlay file`;
|
|
4723
|
+
const size = from > to ? void 0 : `${to.toLocaleString("en-US")} tokens in one ${mode === "web" ? "message" : ".harlay file"}`;
|
|
3806
4724
|
void pace.next(from > to ? 950 : 500, () => set(withStep(withStep(view, "Distill", "done", size), "Rehydrate", "run")));
|
|
3807
4725
|
window = findTargetWindow(windowOwners(target, mode, settings.terminalApp), { newSince: before, timeoutMs: 4e3 });
|
|
3808
4726
|
},
|
|
3809
4727
|
launched: async () => {
|
|
3810
|
-
await pace.next(450, () => set({ ...withStep(view, "Rehydrate", "done", rehydrateDetail(target, mode,
|
|
4728
|
+
await pace.next(450, () => set({ ...withStep(view, "Rehydrate", "done", rehydrateDetail(target, mode, src.tool === target, true)), tag: "done" }));
|
|
3811
4729
|
await pace.next(800, () => set({ ...view, leaving: true }));
|
|
3812
4730
|
await pace.next(200, () => void 0);
|
|
3813
4731
|
if (showing !== key) {
|
|
3814
|
-
|
|
4732
|
+
const typedIn = mode === "app" || mode === "web";
|
|
4733
|
+
notify(`Continuing in ${targetLabel(target)}`, typedIn ? "The prompt is typed in: press Enter there to send it." : "It picks up where this chat left off.");
|
|
3815
4734
|
return;
|
|
3816
4735
|
}
|
|
3817
4736
|
showing = null;
|
|
@@ -3823,8 +4742,8 @@ function presentHandoff(ref, target, mode) {
|
|
|
3823
4742
|
key,
|
|
3824
4743
|
from: tb ? { x: tb.x + tb.width / 2, y: tb.y + tb.height / 2 } : { x: 0, y: 0 },
|
|
3825
4744
|
window: found,
|
|
3826
|
-
to: { tool: target, label:
|
|
3827
|
-
fromLabel:
|
|
4745
|
+
to: { tool: target, label: targetLabel(target), icon: agentIcons[target] ?? null, mode },
|
|
4746
|
+
fromLabel: targetLabel(src.tool),
|
|
3828
4747
|
// The agent may have picked it up already (a terminal starts at once).
|
|
3829
4748
|
picked: delivered === "sent" || delivered === "read"
|
|
3830
4749
|
});
|
|
@@ -3883,10 +4802,30 @@ async function onAction(a) {
|
|
|
3883
4802
|
try {
|
|
3884
4803
|
switch (a.type) {
|
|
3885
4804
|
case "handoff": {
|
|
4805
|
+
if (isWebKey(a.key)) {
|
|
4806
|
+
await handoffWeb(a.key, a.target);
|
|
4807
|
+
break;
|
|
4808
|
+
}
|
|
3886
4809
|
const t = state.sessions.get(a.key);
|
|
3887
4810
|
if (t) await handoff(t.ref, a.target);
|
|
3888
4811
|
break;
|
|
3889
4812
|
}
|
|
4813
|
+
case "toggleWebChats":
|
|
4814
|
+
settings.webChats = !settings.webChats;
|
|
4815
|
+
await saveSettings(settingsFile, settings);
|
|
4816
|
+
if (settings.webChats) browserAllowed = import_electron2.systemPreferences.isTrustedAccessibilityClient(true);
|
|
4817
|
+
syncBrowserWatch();
|
|
4818
|
+
refreshUi();
|
|
4819
|
+
break;
|
|
4820
|
+
case "pickFolder": {
|
|
4821
|
+
const r = await import_electron2.dialog.showOpenDialog({ title: "Where agents open web chats", defaultPath: agentFolder() ?? (0, import_node_os3.homedir)(), properties: ["openDirectory", "createDirectory"] });
|
|
4822
|
+
if (!r.canceled && r.filePaths[0]) {
|
|
4823
|
+
settings.agentFolder = r.filePaths[0];
|
|
4824
|
+
await saveSettings(settingsFile, settings);
|
|
4825
|
+
}
|
|
4826
|
+
revealPopover(webChats.has(a.key) ? a.key : null, "user");
|
|
4827
|
+
break;
|
|
4828
|
+
}
|
|
3890
4829
|
case "setTheme":
|
|
3891
4830
|
settings.theme = a.theme;
|
|
3892
4831
|
import_electron2.nativeTheme.themeSource = a.theme;
|
|
@@ -3958,8 +4897,9 @@ function onWatchEvent(ev) {
|
|
|
3958
4897
|
const now = /* @__PURE__ */ new Date();
|
|
3959
4898
|
const tracked = applyWatchEvent(state, ev, now);
|
|
3960
4899
|
refreshUi();
|
|
3961
|
-
const
|
|
3962
|
-
const
|
|
4900
|
+
const agents = pickTargets(ev.ref.tool, settings.defaultTarget, available, settings.modes, state.limitedUntil, now);
|
|
4901
|
+
const web = webTargets(ev.ref.tool, ev.event.kind === "halt" && ev.event.halt.reason === "usage_limit", state.limitedUntil, now);
|
|
4902
|
+
const first = orderTargets(agents, web, health.kind(sessionKey(ev.ref)))[0];
|
|
3963
4903
|
if (ev.event.kind === "halt") {
|
|
3964
4904
|
if (settings.autoContinue && first && now.getTime() - lastAutoHandoffAt >= AUTO_HANDOFF_COOLDOWN_MS) {
|
|
3965
4905
|
lastAutoHandoffAt = now.getTime();
|
|
@@ -3983,14 +4923,106 @@ function onUsageAlert(a) {
|
|
|
3983
4923
|
function dataDir() {
|
|
3984
4924
|
return import_electron2.app.getPath("userData");
|
|
3985
4925
|
}
|
|
3986
|
-
function onHotkey() {
|
|
4926
|
+
async function onHotkey() {
|
|
3987
4927
|
if (popover?.isVisible()) {
|
|
3988
4928
|
hidePopover();
|
|
3989
4929
|
return;
|
|
3990
4930
|
}
|
|
4931
|
+
if (settings.webChats && browserAllowed) {
|
|
4932
|
+
const r = await readFrontChat();
|
|
4933
|
+
debug("front chat", r.ok ? { url: r.capture.url, turns: r.capture.turns.length, limit: r.capture.limit } : r.reason);
|
|
4934
|
+
if (r.ok && r.capture.turns.length) {
|
|
4935
|
+
const e = rememberWebChat(state, webChats, r.capture, /* @__PURE__ */ new Date());
|
|
4936
|
+
revealPopover(e.key, "user");
|
|
4937
|
+
return;
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
3991
4940
|
const latest = latestSession(state);
|
|
3992
4941
|
revealPopover(latest ? sessionKey(latest.ref) : null, "user");
|
|
3993
4942
|
}
|
|
4943
|
+
function canAttach() {
|
|
4944
|
+
return import_electron2.systemPreferences.isTrustedAccessibilityClient(false);
|
|
4945
|
+
}
|
|
4946
|
+
async function attach(chat, file) {
|
|
4947
|
+
const dir = (0, import_node_path24.join)((0, import_node_os3.tmpdir)(), "harlay", "attach");
|
|
4948
|
+
await (0, import_promises20.mkdir)(dir, { recursive: true });
|
|
4949
|
+
const named = (0, import_node_path24.join)(dir, (0, import_node_path24.basename)(file).replace(/^\d{4}-\d{4}-/, ""));
|
|
4950
|
+
await (0, import_promises20.copyFile)(file, named);
|
|
4951
|
+
const r = await pasteFileInto(chat, named);
|
|
4952
|
+
debug("attach", chat, file, r);
|
|
4953
|
+
if (!r.ok) notify(`Attach the handoff in ${targetLabel(chat)}`, `It's on your clipboard: press \u2318V in ${targetLabel(chat)}, then send.`);
|
|
4954
|
+
}
|
|
4955
|
+
function agentFolder() {
|
|
4956
|
+
return settings.agentFolder ?? latestSession(state)?.ref.cwd ?? null;
|
|
4957
|
+
}
|
|
4958
|
+
function syncBrowserWatch() {
|
|
4959
|
+
if (settings.webChats && browserAllowed) browserWatcher.start();
|
|
4960
|
+
else browserWatcher.stop();
|
|
4961
|
+
}
|
|
4962
|
+
var webAlerts = /* @__PURE__ */ new Map();
|
|
4963
|
+
var WEB_ALERT_AGAIN_MS = 15 * 6e4;
|
|
4964
|
+
async function onWebLimit(ev) {
|
|
4965
|
+
const seen = `${ev.url}
|
|
4966
|
+
${ev.limit}`;
|
|
4967
|
+
const now = Date.now();
|
|
4968
|
+
if (now - (webAlerts.get(seen) ?? 0) < WEB_ALERT_AGAIN_MS) return;
|
|
4969
|
+
const r = await readFrontChat();
|
|
4970
|
+
if (!r.ok || r.capture.url !== ev.url || !r.capture.turns.length) {
|
|
4971
|
+
debug("web limit not read", r.ok ? { url: r.capture.url, turns: r.capture.turns.length } : r.reason);
|
|
4972
|
+
return;
|
|
4973
|
+
}
|
|
4974
|
+
webAlerts.set(seen, now);
|
|
4975
|
+
const near = await frontWindow();
|
|
4976
|
+
const e = rememberWebChat(state, webChats, { ...r.capture, limit: r.capture.limit ?? ev.limit }, /* @__PURE__ */ new Date());
|
|
4977
|
+
debug("web limit", ev.url, ev.limit);
|
|
4978
|
+
refreshUi();
|
|
4979
|
+
const [first] = buildWebSessions([e], settings, available, handoffsInFlight, /* @__PURE__ */ new Date(), state.limitedUntil)[0].targets;
|
|
4980
|
+
if (settings.autoContinue && first && now - lastAutoHandoffAt >= AUTO_HANDOFF_COOLDOWN_MS) {
|
|
4981
|
+
lastAutoHandoffAt = now;
|
|
4982
|
+
void handoffWeb(e.key, first.tool);
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
alertPopover(e.key, near);
|
|
4986
|
+
}
|
|
4987
|
+
async function handoffWeb(key, target) {
|
|
4988
|
+
const w = webChats.get(key);
|
|
4989
|
+
if (!w || handoffsInFlight.has(key)) return;
|
|
4990
|
+
const folder = isWebChat(target) ? null : agentFolder();
|
|
4991
|
+
if (!isWebChat(target)) {
|
|
4992
|
+
const ok = folder ? await (0, import_promises20.stat)(folder).then((s) => s.isDirectory(), () => false) : false;
|
|
4993
|
+
if (!ok) {
|
|
4994
|
+
notify("Harlay needs a folder", `Pick the folder ${targetLabel(target)} should open this chat in (Change, under the choices).`);
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
handoffsInFlight.add(key);
|
|
4999
|
+
refreshUi();
|
|
5000
|
+
const mode = isWebChat(target) ? "web" : settings.modes[target];
|
|
5001
|
+
const show = presentHandoff({ key, tool: w.capture.chat, title: webChatTitle(w.capture), status: w.status, detail: w.detail }, target, mode);
|
|
5002
|
+
try {
|
|
5003
|
+
show.progress({ stage: "extracted", messages: w.capture.turns.length, filesChanged: 0, chars: w.capture.turns.reduce((n, t) => n + t.text.length, 0) });
|
|
5004
|
+
const res = await runWebChatHandoff({
|
|
5005
|
+
capture: w.capture,
|
|
5006
|
+
target,
|
|
5007
|
+
mode: mode === "web" ? "app" : mode,
|
|
5008
|
+
attach: canAttach(),
|
|
5009
|
+
workdir: folder,
|
|
5010
|
+
store: handoffStore(home),
|
|
5011
|
+
terminalApp: settings.terminalApp,
|
|
5012
|
+
resolve: defaultResolveOptions()
|
|
5013
|
+
});
|
|
5014
|
+
show.progress({ stage: "written", path: res.written?.path ?? "", chars: res.written ? res.written.chars : res.prompt.length });
|
|
5015
|
+
markWebHandedOff(w, target, /* @__PURE__ */ new Date());
|
|
5016
|
+
void show.launched();
|
|
5017
|
+
if (isWebChat(target) && res.written) void attach(target, res.written.path);
|
|
5018
|
+
} catch (e) {
|
|
5019
|
+
show.failed(errorMessage(e));
|
|
5020
|
+
notify("Harlay could not hand off", errorMessage(e));
|
|
5021
|
+
} finally {
|
|
5022
|
+
handoffsInFlight.delete(key);
|
|
5023
|
+
}
|
|
5024
|
+
refreshUi();
|
|
5025
|
+
}
|
|
3994
5026
|
async function trackPinned() {
|
|
3995
5027
|
const refs = await Promise.all(
|
|
3996
5028
|
settings.pinned.filter((k) => !state.sessions.has(k)).map(async (k) => {
|
|
@@ -4001,7 +5033,7 @@ async function trackPinned() {
|
|
|
4001
5033
|
seedSessions(state, refs.filter((r) => r !== null));
|
|
4002
5034
|
}
|
|
4003
5035
|
async function addChat(tool, raw) {
|
|
4004
|
-
const id = (0,
|
|
5036
|
+
const id = (0, import_node_path24.basename)(raw.trim()).replace(/\.jsonl?$/, "");
|
|
4005
5037
|
if (!/^[\w.-]{4,200}$/.test(id)) return { ok: false, error: "That doesn\u2019t look like a chat ID." };
|
|
4006
5038
|
const ref = await findSessionById(readers, tool, id).catch(() => null);
|
|
4007
5039
|
if (!ref) return { ok: false, error: `No ${TOOL_LABEL[tool]} chat with that ID on this Mac.` };
|
|
@@ -4019,9 +5051,9 @@ async function loadAgentIcons() {
|
|
|
4019
5051
|
const dir = defaultResolveOptions().applicationsDir;
|
|
4020
5052
|
for (const tool of TOOLS) {
|
|
4021
5053
|
for (const bundle of APP_BUNDLES[tool]) {
|
|
4022
|
-
const path = (0,
|
|
5054
|
+
const path = (0, import_node_path24.join)(dir, bundle);
|
|
4023
5055
|
try {
|
|
4024
|
-
await (0,
|
|
5056
|
+
await (0, import_promises20.access)(path);
|
|
4025
5057
|
const thumb = await import_electron2.nativeImage.createThumbnailFromPath(path, { width: 64, height: 64 });
|
|
4026
5058
|
agentIcons[tool] = thumb.resize({ width: 36, height: 36, quality: "best" }).toDataURL();
|
|
4027
5059
|
break;
|
|
@@ -4029,7 +5061,17 @@ async function loadAgentIcons() {
|
|
|
4029
5061
|
}
|
|
4030
5062
|
}
|
|
4031
5063
|
}
|
|
5064
|
+
for (const [chat, bundle] of Object.entries(WEB_APP)) {
|
|
5065
|
+
try {
|
|
5066
|
+
const path = (0, import_node_path24.join)(dir, bundle);
|
|
5067
|
+
await (0, import_promises20.access)(path);
|
|
5068
|
+
const thumb = await import_electron2.nativeImage.createThumbnailFromPath(path, { width: 64, height: 64 });
|
|
5069
|
+
agentIcons[chat] = thumb.resize({ width: 36, height: 36, quality: "best" }).toDataURL();
|
|
5070
|
+
} catch {
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
4032
5073
|
}
|
|
5074
|
+
var WEB_APP = { "claude-web": "Claude.app", chatgpt: "ChatGPT.app" };
|
|
4033
5075
|
async function refreshSessions() {
|
|
4034
5076
|
try {
|
|
4035
5077
|
seedSessions(state, (await Promise.all(readers.map((r) => r.listSessions(PER_AGENT)))).flat());
|
|
@@ -4075,7 +5117,7 @@ function createPopover() {
|
|
|
4075
5117
|
acceptFirstMouse: true,
|
|
4076
5118
|
// backgroundThrottling off: the limit panel opens while another app is in front, and a throttled page would leave
|
|
4077
5119
|
// its fade-in (and so the whole panel) invisible.
|
|
4078
|
-
webPreferences: { preload: (0,
|
|
5120
|
+
webPreferences: { preload: (0, import_node_path24.join)(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false }
|
|
4079
5121
|
});
|
|
4080
5122
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
4081
5123
|
win.on("blur", () => {
|
|
@@ -4083,22 +5125,23 @@ function createPopover() {
|
|
|
4083
5125
|
});
|
|
4084
5126
|
win.webContents.on("will-navigate", (e) => e.preventDefault());
|
|
4085
5127
|
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
4086
|
-
void win.loadFile((0,
|
|
5128
|
+
void win.loadFile((0, import_node_path24.join)(__dirname, "..", "renderer", "popover.html"));
|
|
4087
5129
|
return win;
|
|
4088
5130
|
}
|
|
4089
5131
|
var debug = process.env.HARLAY_DEBUG ? (...a) => console.log("[harlay]", ...a) : () => void 0;
|
|
4090
5132
|
var alertKey = null;
|
|
4091
|
-
function alertPopover(key) {
|
|
5133
|
+
function alertPopover(key, near = null) {
|
|
4092
5134
|
debug("limit panel for", key, { popover: !!popover, tray: tray?.getBounds(), visible: popover?.isVisible(), focused: popover?.isFocused() });
|
|
4093
5135
|
if (!popover || !tray) return;
|
|
4094
5136
|
if (popover.isVisible() && popover.isFocused()) return;
|
|
4095
5137
|
alertKey = key;
|
|
4096
|
-
const show = () => showPopover(key, false, "user");
|
|
5138
|
+
const show = () => showPopover(key, false, near ?? "user");
|
|
4097
5139
|
if (popover.webContents.isLoading()) popover.webContents.once("did-finish-load", show);
|
|
4098
5140
|
else show();
|
|
4099
5141
|
}
|
|
4100
5142
|
function closeResolvedAlert() {
|
|
4101
5143
|
if (!alertKey || !popover?.isVisible() || popover.isFocused()) return;
|
|
5144
|
+
if (isWebKey(alertKey)) return;
|
|
4102
5145
|
if (state.sessions.get(alertKey)?.status === "limit") return;
|
|
4103
5146
|
alertKey = null;
|
|
4104
5147
|
hidePopover();
|
|
@@ -4107,7 +5150,7 @@ function showPopover(route = null, focus = true, where = "icon") {
|
|
|
4107
5150
|
if (!popover || !tray) return;
|
|
4108
5151
|
const t = tray.getBounds();
|
|
4109
5152
|
const iconArea = import_electron2.screen.getDisplayNearestPoint({ x: t.x, y: t.y }).workArea;
|
|
4110
|
-
const userArea = where === "user" ? import_electron2.screen.getDisplayNearestPoint(import_electron2.screen.getCursorScreenPoint()).workArea :
|
|
5153
|
+
const userArea = where === "icon" ? iconArea : where === "user" ? import_electron2.screen.getDisplayNearestPoint(import_electron2.screen.getCursorScreenPoint()).workArea : import_electron2.screen.getDisplayMatching(where).workArea;
|
|
4111
5154
|
const { x, y } = panelPosition(t, iconArea, userArea, POPOVER_WIDTH);
|
|
4112
5155
|
popover.setPosition(Math.round(x), Math.round(y));
|
|
4113
5156
|
refreshUi();
|
|
@@ -4180,13 +5223,13 @@ if (!import_electron2.app.requestSingleInstanceLock()) {
|
|
|
4180
5223
|
import_electron2.app.on("second-instance", () => revealPopover());
|
|
4181
5224
|
import_electron2.app.whenReady().then(async () => {
|
|
4182
5225
|
import_electron2.app.dock?.hide();
|
|
4183
|
-
settingsFile = (0,
|
|
5226
|
+
settingsFile = (0, import_node_path24.join)(import_electron2.app.getPath("userData"), "settings.json");
|
|
4184
5227
|
settings = await loadSettings(settingsFile);
|
|
4185
5228
|
import_electron2.nativeTheme.themeSource = settings.theme;
|
|
4186
5229
|
if (import_electron2.app.isPackaged) import_electron2.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
|
|
4187
5230
|
available = await availableTargets(defaultResolveOptions());
|
|
4188
5231
|
await loadAgentIcons();
|
|
4189
|
-
const iconPath = (0,
|
|
5232
|
+
const iconPath = (0, import_node_path24.join)(__dirname, "..", "assets", "trayTemplate.png");
|
|
4190
5233
|
const icon = import_electron2.nativeImage.createFromPath(iconPath);
|
|
4191
5234
|
icon.setTemplateImage(true);
|
|
4192
5235
|
tray = new import_electron2.Tray(icon);
|
|
@@ -4221,7 +5264,26 @@ if (!import_electron2.app.requestSingleInstanceLock()) {
|
|
|
4221
5264
|
claudeBridge = await bridgeState(home);
|
|
4222
5265
|
if (claudeBridge === "on") await installBridge(home, dataDir()).catch((e) => console.error("[harlay] status line refresh failed", e));
|
|
4223
5266
|
await usage.start().catch((e) => console.error("[harlay] usage monitor failed to start", e));
|
|
4224
|
-
if (!import_electron2.globalShortcut.register(HOTKEY.accelerator, onHotkey)) console.error(`[harlay] could not register ${HOTKEY.accelerator}`);
|
|
5267
|
+
if (!import_electron2.globalShortcut.register(HOTKEY.accelerator, () => void onHotkey())) console.error(`[harlay] could not register ${HOTKEY.accelerator}`);
|
|
5268
|
+
browserAllowed = import_electron2.systemPreferences.isTrustedAccessibilityClient(false);
|
|
5269
|
+
browserWatcher.on("limit", (ev) => {
|
|
5270
|
+
debug("web limit seen", ev.url, ev.limit);
|
|
5271
|
+
void onWebLimit(ev);
|
|
5272
|
+
});
|
|
5273
|
+
browserWatcher.on("status", (s) => debug("browser", s));
|
|
5274
|
+
browserWatcher.on("untrusted", () => {
|
|
5275
|
+
browserAllowed = false;
|
|
5276
|
+
refreshUi();
|
|
5277
|
+
});
|
|
5278
|
+
syncBrowserWatch();
|
|
5279
|
+
setInterval(() => {
|
|
5280
|
+
if (!settings.webChats) return;
|
|
5281
|
+
const allowed = import_electron2.systemPreferences.isTrustedAccessibilityClient(false);
|
|
5282
|
+
if (allowed === browserAllowed && browserWatcher.running === allowed) return;
|
|
5283
|
+
browserAllowed = allowed;
|
|
5284
|
+
syncBrowserWatch();
|
|
5285
|
+
refreshUi();
|
|
5286
|
+
}, 5e3);
|
|
4225
5287
|
if (!import_electron2.app.getLoginItemSettings().wasOpenedAtLogin) revealPopover();
|
|
4226
5288
|
}).catch((e) => {
|
|
4227
5289
|
console.error("[harlay] startup failed", e);
|
|
@@ -4232,5 +5294,6 @@ if (!import_electron2.app.requestSingleInstanceLock()) {
|
|
|
4232
5294
|
import_electron2.globalShortcut.unregisterAll();
|
|
4233
5295
|
watcher?.stop();
|
|
4234
5296
|
usage?.stop();
|
|
5297
|
+
browserWatcher.stop();
|
|
4235
5298
|
});
|
|
4236
5299
|
}
|