harlay 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/dist/main.cjs +823 -55
- package/app/renderer/popover.css +23 -0
- package/app/renderer/popover.js +67 -5
- package/dist/harlay.mjs +120 -27
- 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_electron = require("electron");
|
|
5
|
-
var
|
|
5
|
+
var import_promises16 = require("node:fs/promises");
|
|
6
6
|
var import_node_os2 = require("node:os");
|
|
7
|
-
var
|
|
7
|
+
var import_node_path18 = require("node:path");
|
|
8
8
|
|
|
9
9
|
// ../../packages/core/src/types.ts
|
|
10
10
|
var TOOLS = ["claude-code", "codex", "cursor"];
|
|
@@ -142,9 +142,9 @@ function blockText(content) {
|
|
|
142
142
|
|
|
143
143
|
// ../../packages/core/src/sources/todos.ts
|
|
144
144
|
var STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed", "cancelled"]);
|
|
145
|
-
function applyTodoWrite(
|
|
146
|
-
if (!isObj(input) || !Array.isArray(input.todos)) return
|
|
147
|
-
const next = input.merge === true &&
|
|
145
|
+
function applyTodoWrite(current2, input) {
|
|
146
|
+
if (!isObj(input) || !Array.isArray(input.todos)) return current2 ?? /* @__PURE__ */ new Map();
|
|
147
|
+
const next = input.merge === true && current2 ? new Map(current2) : /* @__PURE__ */ new Map();
|
|
148
148
|
input.todos.forEach((raw, i) => {
|
|
149
149
|
if (!isObj(raw)) return;
|
|
150
150
|
const id = raw.id !== void 0 && raw.id !== null ? String(raw.id) : `#${i}`;
|
|
@@ -379,25 +379,25 @@ function usageOf(p) {
|
|
|
379
379
|
if (!primary || typeof primary.used_percent !== "number") return null;
|
|
380
380
|
return { usedPercent: primary.used_percent, resetsAt: typeof primary.resets_at === "number" ? new Date(primary.resets_at * 1e3) : null };
|
|
381
381
|
}
|
|
382
|
-
function reachedHalt(p,
|
|
382
|
+
function reachedHalt(p, usage2) {
|
|
383
383
|
const rl = isObj(p.rate_limits) ? p.rate_limits : null;
|
|
384
384
|
const t = rl ? rl.rate_limit_reached_type : null;
|
|
385
385
|
if (t === null || t === void 0) return null;
|
|
386
|
-
return { reason: "usage_limit", message: `Codex usage limit reached (${String(t)})`, resetsAt:
|
|
386
|
+
return { reason: "usage_limit", message: `Codex usage limit reached (${String(t)})`, resetsAt: usage2?.resetsAt ?? null };
|
|
387
387
|
}
|
|
388
|
-
function taskHalt(p,
|
|
388
|
+
function taskHalt(p, usage2) {
|
|
389
389
|
const e = isObj(p.error) ? p.error : null;
|
|
390
390
|
const message = e ? str(e.message) : null;
|
|
391
391
|
if (!e || !message) return null;
|
|
392
392
|
const limited = e.codex_error_info === "usage_limit_exceeded" || /usage limit|rate limit/i.test(message);
|
|
393
|
-
return { reason: limited ? "usage_limit" : "crash", message, resetsAt:
|
|
393
|
+
return { reason: limited ? "usage_limit" : "crash", message, resetsAt: usage2?.resetsAt ?? null };
|
|
394
394
|
}
|
|
395
395
|
function normalizeCodex(records, ref, invalid = 0) {
|
|
396
396
|
const timeline = [];
|
|
397
397
|
const fallbackPrompts = [];
|
|
398
398
|
let sawItemPrompt = false;
|
|
399
399
|
let model = null;
|
|
400
|
-
let
|
|
400
|
+
let usage2 = null;
|
|
401
401
|
let halt = null;
|
|
402
402
|
let unknown = invalid;
|
|
403
403
|
for (const rec of records) {
|
|
@@ -440,16 +440,16 @@ function normalizeCodex(records, ref, invalid = 0) {
|
|
|
440
440
|
const text = typeof p.message === "string" ? cleanPrompt(p.message) : null;
|
|
441
441
|
if (text) fallbackPrompts.push(text);
|
|
442
442
|
} else if (p.type === "token_count") {
|
|
443
|
-
|
|
444
|
-
halt = reachedHalt(p,
|
|
443
|
+
usage2 = usageOf(p) ?? usage2;
|
|
444
|
+
halt = reachedHalt(p, usage2) ?? halt;
|
|
445
445
|
} else if (p.type === "task_complete") {
|
|
446
|
-
halt = taskHalt(p,
|
|
446
|
+
halt = taskHalt(p, usage2);
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
if (!sawItemPrompt && fallbackPrompts.length) {
|
|
450
450
|
timeline.unshift(...fallbackPrompts.map((text) => ({ kind: "user", text })));
|
|
451
451
|
}
|
|
452
|
-
return { ref, model, timeline, todos: null, halt, usage, unknownRecords: unknown };
|
|
452
|
+
return { ref, model, timeline, todos: null, halt, usage: usage2, unknownRecords: unknown };
|
|
453
453
|
}
|
|
454
454
|
function parseSessionMeta(line) {
|
|
455
455
|
let r;
|
|
@@ -1146,15 +1146,15 @@ async function allocateHandoffId(handoffsDir, baseId) {
|
|
|
1146
1146
|
async function ensureGitExclude(workdir) {
|
|
1147
1147
|
const p = (await git(workdir, ["rev-parse", "--git-path", "info/exclude"])).trim();
|
|
1148
1148
|
const file = (0, import_node_path6.isAbsolute)(p) ? p : (0, import_node_path6.join)(workdir, p);
|
|
1149
|
-
let
|
|
1149
|
+
let current2 = "";
|
|
1150
1150
|
try {
|
|
1151
|
-
|
|
1151
|
+
current2 = await (0, import_promises5.readFile)(file, "utf8");
|
|
1152
1152
|
} catch {
|
|
1153
|
-
|
|
1153
|
+
current2 = "";
|
|
1154
1154
|
}
|
|
1155
|
-
if (
|
|
1155
|
+
if (current2.split("\n").some((l) => l.trim() === ".harlay/" || l.trim() === ".harlay")) return;
|
|
1156
1156
|
await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
1157
|
-
await (0, import_promises5.appendFile)(file, `${
|
|
1157
|
+
await (0, import_promises5.appendFile)(file, `${current2 && !current2.endsWith("\n") ? "\n" : ""}.harlay/
|
|
1158
1158
|
`);
|
|
1159
1159
|
}
|
|
1160
1160
|
async function readHandoffState(workdir) {
|
|
@@ -1591,12 +1591,535 @@ var Watcher = class extends import_node_events.EventEmitter {
|
|
|
1591
1591
|
}
|
|
1592
1592
|
};
|
|
1593
1593
|
|
|
1594
|
-
// src/
|
|
1595
|
-
var
|
|
1594
|
+
// ../../packages/core/src/usage/types.ts
|
|
1595
|
+
var WINDOW_MINUTES = { five_hour: 300, weekly: 10080 };
|
|
1596
|
+
function windowKind(minutes) {
|
|
1597
|
+
if (minutes === 300) return "five_hour";
|
|
1598
|
+
if (minutes === 10080) return "weekly";
|
|
1599
|
+
return "other";
|
|
1600
|
+
}
|
|
1601
|
+
function windowLabel(kind, minutes) {
|
|
1602
|
+
if (kind === "five_hour") return "5h";
|
|
1603
|
+
if (kind === "weekly") return "Week";
|
|
1604
|
+
if (!minutes) return "Limit";
|
|
1605
|
+
return minutes % 1440 === 0 ? `${minutes / 1440}d` : `${Math.round(minutes / 60)}h`;
|
|
1606
|
+
}
|
|
1607
|
+
function fullestWindow(s) {
|
|
1608
|
+
let best = null;
|
|
1609
|
+
for (const w of s.windows) if (!best || w.usedPercent > best.usedPercent) best = w;
|
|
1610
|
+
return best;
|
|
1611
|
+
}
|
|
1596
1612
|
|
|
1597
|
-
// src/
|
|
1613
|
+
// ../../packages/core/src/usage/codex.ts
|
|
1614
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1598
1615
|
var import_promises10 = require("node:fs/promises");
|
|
1599
1616
|
var import_node_path10 = require("node:path");
|
|
1617
|
+
var PACE_MS = 60 * 6e4;
|
|
1618
|
+
var TAIL_BYTES = 1024 * 1024;
|
|
1619
|
+
function num(x) {
|
|
1620
|
+
return typeof x === "number" && Number.isFinite(x) ? x : null;
|
|
1621
|
+
}
|
|
1622
|
+
function codexWindows(rl) {
|
|
1623
|
+
const out = [];
|
|
1624
|
+
for (const key of ["primary", "secondary"]) {
|
|
1625
|
+
const w = rl[key];
|
|
1626
|
+
if (!isObj(w)) continue;
|
|
1627
|
+
const pct = num(w.used_percent) ?? num(w.usedPercent);
|
|
1628
|
+
if (pct === null) continue;
|
|
1629
|
+
const minutes = num(w.window_minutes) ?? num(w.windowDurationMins);
|
|
1630
|
+
const resets = num(w.resets_at) ?? num(w.resetsAt);
|
|
1631
|
+
const kind = windowKind(minutes);
|
|
1632
|
+
out.push({ kind, label: windowLabel(kind, minutes), usedPercent: pct, resetsAt: resets === null ? null : new Date(resets * 1e3), exact: true });
|
|
1633
|
+
}
|
|
1634
|
+
return out;
|
|
1635
|
+
}
|
|
1636
|
+
function planOf(rl) {
|
|
1637
|
+
const p = rl.plan_type ?? rl.planType;
|
|
1638
|
+
return typeof p === "string" ? p : null;
|
|
1639
|
+
}
|
|
1640
|
+
async function readTail(file, bytes) {
|
|
1641
|
+
const fh = await (0, import_promises10.open)(file, "r");
|
|
1642
|
+
try {
|
|
1643
|
+
const { size } = await fh.stat();
|
|
1644
|
+
const start = Math.max(0, size - bytes);
|
|
1645
|
+
const buf = Buffer.alloc(size - start);
|
|
1646
|
+
await fh.read(buf, 0, buf.length, start);
|
|
1647
|
+
const text = buf.toString("utf8");
|
|
1648
|
+
return start > 0 ? text.slice(text.indexOf("\n") + 1) : text;
|
|
1649
|
+
} finally {
|
|
1650
|
+
await fh.close();
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
async function readCodexReadings(home2, files = 8) {
|
|
1654
|
+
const logs = (await listSessionFiles((0, import_node_path10.join)(home2, ".codex", "sessions"), (f) => f.endsWith(".jsonl"))).slice(0, files);
|
|
1655
|
+
const out = [];
|
|
1656
|
+
for (const { path } of logs) {
|
|
1657
|
+
let text;
|
|
1658
|
+
try {
|
|
1659
|
+
text = await readTail(path, TAIL_BYTES);
|
|
1660
|
+
} catch {
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
for (const line of text.split("\n")) {
|
|
1664
|
+
if (!line.includes('"rate_limits":{')) continue;
|
|
1665
|
+
try {
|
|
1666
|
+
const rec = JSON.parse(line);
|
|
1667
|
+
const p = isObj(rec.payload) ? rec.payload : null;
|
|
1668
|
+
const rl = p && isObj(p.rate_limits) ? p.rate_limits : null;
|
|
1669
|
+
const at = typeof rec.timestamp === "string" ? new Date(rec.timestamp) : null;
|
|
1670
|
+
if (!rl || !at || Number.isNaN(at.getTime())) continue;
|
|
1671
|
+
const windows = codexWindows(rl);
|
|
1672
|
+
if (windows.length) out.push({ at, windows, plan: planOf(rl), resetCredits: null });
|
|
1673
|
+
} catch {
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return out.sort((a, b) => a.at.getTime() - b.at.getTime());
|
|
1678
|
+
}
|
|
1679
|
+
function minutesToFull(points, now) {
|
|
1680
|
+
const recent = points.filter((p) => now.getTime() - p.at.getTime() <= PACE_MS);
|
|
1681
|
+
if (recent.length < 2) return null;
|
|
1682
|
+
const first = recent[0];
|
|
1683
|
+
const last = recent[recent.length - 1];
|
|
1684
|
+
const minutes = (last.at.getTime() - first.at.getTime()) / 6e4;
|
|
1685
|
+
const rise = last.pct - first.pct;
|
|
1686
|
+
if (minutes < 5 || rise <= 0) return null;
|
|
1687
|
+
return Math.max(0, (100 - last.pct) / (rise / minutes));
|
|
1688
|
+
}
|
|
1689
|
+
function current(w, now) {
|
|
1690
|
+
return w.resetsAt && w.resetsAt.getTime() <= now.getTime() ? { ...w, usedPercent: 0, resetsAt: null } : w;
|
|
1691
|
+
}
|
|
1692
|
+
function codexSnapshot(readings, now, query = null) {
|
|
1693
|
+
const all = query ? [...readings, query].sort((a, b) => a.at.getTime() - b.at.getTime()) : readings;
|
|
1694
|
+
const latest = all[all.length - 1];
|
|
1695
|
+
if (!latest) return null;
|
|
1696
|
+
const windows = latest.windows.map((w) => current(w, now));
|
|
1697
|
+
const fullest = windows.reduce((b, w) => !b || w.usedPercent > b.usedPercent ? w : b, null);
|
|
1698
|
+
let minutesToLimit = null;
|
|
1699
|
+
if (fullest && fullest.usedPercent > 0) {
|
|
1700
|
+
const series = all.flatMap((r) => r.windows.filter((w) => w.kind === fullest.kind).map((w) => ({ at: r.at, pct: w.usedPercent })));
|
|
1701
|
+
minutesToLimit = minutesToFull(series, now);
|
|
1702
|
+
if (minutesToLimit !== null && fullest.resetsAt && now.getTime() + minutesToLimit * 6e4 >= fullest.resetsAt.getTime()) minutesToLimit = null;
|
|
1703
|
+
}
|
|
1704
|
+
return {
|
|
1705
|
+
tool: "codex",
|
|
1706
|
+
windows,
|
|
1707
|
+
observedAt: latest.at,
|
|
1708
|
+
minutesToLimit,
|
|
1709
|
+
plan: latest.plan ?? query?.plan ?? null,
|
|
1710
|
+
resetCredits: query?.resetCredits ?? null
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function queryCodexUsage(codexBin, timeoutMs = 15e3) {
|
|
1714
|
+
return new Promise((resolve) => {
|
|
1715
|
+
let done = false;
|
|
1716
|
+
let buf = "";
|
|
1717
|
+
const child = (0, import_node_child_process3.spawn)(codexBin, ["app-server"], { stdio: ["pipe", "pipe", "ignore"] });
|
|
1718
|
+
const finish = (r) => {
|
|
1719
|
+
if (done) return;
|
|
1720
|
+
done = true;
|
|
1721
|
+
clearTimeout(timer);
|
|
1722
|
+
child.kill();
|
|
1723
|
+
resolve(r);
|
|
1724
|
+
};
|
|
1725
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1726
|
+
child.on("error", () => finish(null));
|
|
1727
|
+
child.stdin.on("error", () => finish(null));
|
|
1728
|
+
child.on("exit", () => finish(null));
|
|
1729
|
+
child.stdout.setEncoding("utf8");
|
|
1730
|
+
child.stdout.on("data", (chunk) => {
|
|
1731
|
+
buf += chunk;
|
|
1732
|
+
let nl;
|
|
1733
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
1734
|
+
const line = buf.slice(0, nl);
|
|
1735
|
+
buf = buf.slice(nl + 1);
|
|
1736
|
+
let msg;
|
|
1737
|
+
try {
|
|
1738
|
+
msg = JSON.parse(line);
|
|
1739
|
+
} catch {
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (msg.id !== 2) continue;
|
|
1743
|
+
const res = isObj(msg.result) ? msg.result : null;
|
|
1744
|
+
const rl = res && isObj(res.rateLimits) ? res.rateLimits : null;
|
|
1745
|
+
if (!res || !rl) return finish(null);
|
|
1746
|
+
const credits = isObj(res.rateLimitResetCredits) ? num(res.rateLimitResetCredits.availableCount) : null;
|
|
1747
|
+
finish({ at: /* @__PURE__ */ new Date(), windows: codexWindows(rl), plan: planOf(rl), resetCredits: credits });
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
const send = (o) => child.stdin.write(JSON.stringify(o) + "\n");
|
|
1751
|
+
send({ method: "initialize", id: 1, params: { clientInfo: { name: "harlay", version: "1" } } });
|
|
1752
|
+
send({ method: "initialized" });
|
|
1753
|
+
send({ method: "account/rateLimits/read", id: 2 });
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// ../../packages/core/src/usage/claude.ts
|
|
1758
|
+
var import_promises11 = require("node:fs/promises");
|
|
1759
|
+
var import_node_path11 = require("node:path");
|
|
1760
|
+
var HOUR = 36e5;
|
|
1761
|
+
var SAMPLE_MAX_AGE_MS = 30 * 24 * HOUR;
|
|
1762
|
+
var DISPROVE_FACTOR = 1.15;
|
|
1763
|
+
var MIN_CALIBRATION_PCT = 5;
|
|
1764
|
+
var PACE_MS2 = 30 * 6e4;
|
|
1765
|
+
function replyUnits(usage2, model) {
|
|
1766
|
+
const n = (k) => typeof usage2[k] === "number" ? usage2[k] : 0;
|
|
1767
|
+
const tokens = n("input_tokens") + 1.25 * n("cache_creation_input_tokens") + 0.1 * n("cache_read_input_tokens") + 5 * n("output_tokens");
|
|
1768
|
+
const tier = /opus/i.test(model) ? 5 : /haiku/i.test(model) ? 1 : 3;
|
|
1769
|
+
return tokens * tier;
|
|
1770
|
+
}
|
|
1771
|
+
function limitKind(t) {
|
|
1772
|
+
if (t === "five_hour") return "five_hour";
|
|
1773
|
+
if (t === "seven_day" || t === "weekly") return "weekly";
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
var ClaudeUsageLog = class {
|
|
1777
|
+
constructor(home2) {
|
|
1778
|
+
this.home = home2;
|
|
1779
|
+
}
|
|
1780
|
+
home;
|
|
1781
|
+
files = /* @__PURE__ */ new Map();
|
|
1782
|
+
replies = /* @__PURE__ */ new Map();
|
|
1783
|
+
hits = /* @__PURE__ */ new Map();
|
|
1784
|
+
async refresh(now = /* @__PURE__ */ new Date(), maxAgeMs = 8 * 24 * HOUR) {
|
|
1785
|
+
const root = (0, import_node_path11.join)(this.home, ".claude", "projects");
|
|
1786
|
+
let names;
|
|
1787
|
+
try {
|
|
1788
|
+
names = await (0, import_promises11.readdir)(root, { recursive: true });
|
|
1789
|
+
} catch {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
for (const name of names) {
|
|
1793
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
1794
|
+
const path = (0, import_node_path11.join)(root, name);
|
|
1795
|
+
try {
|
|
1796
|
+
const s = await (0, import_promises11.stat)(path);
|
|
1797
|
+
if (now.getTime() - s.mtimeMs > maxAgeMs) continue;
|
|
1798
|
+
await this.readFrom(path, s.size);
|
|
1799
|
+
} catch {
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
const cutoff = now.getTime() - maxAgeMs;
|
|
1803
|
+
for (const [k, e] of this.replies) if (e.at < cutoff) this.replies.delete(k);
|
|
1804
|
+
for (const [k, h] of this.hits) if (h.at < cutoff - SAMPLE_MAX_AGE_MS) this.hits.delete(k);
|
|
1805
|
+
}
|
|
1806
|
+
async readFrom(path, size) {
|
|
1807
|
+
const st = this.files.get(path) ?? { offset: 0, partial: "" };
|
|
1808
|
+
if (size < st.offset) Object.assign(st, { offset: 0, partial: "" });
|
|
1809
|
+
if (size === st.offset) return;
|
|
1810
|
+
const fh = await (0, import_promises11.open)(path, "r");
|
|
1811
|
+
try {
|
|
1812
|
+
const buf = Buffer.alloc(size - st.offset);
|
|
1813
|
+
await fh.read(buf, 0, buf.length, st.offset);
|
|
1814
|
+
st.offset = size;
|
|
1815
|
+
const text = st.partial + buf.toString("utf8");
|
|
1816
|
+
const lines = text.split("\n");
|
|
1817
|
+
st.partial = lines.pop() ?? "";
|
|
1818
|
+
for (const line of lines) this.ingest(line);
|
|
1819
|
+
} finally {
|
|
1820
|
+
await fh.close();
|
|
1821
|
+
this.files.set(path, st);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
ingest(line) {
|
|
1825
|
+
if (!line.includes('"assistant"')) return;
|
|
1826
|
+
let r;
|
|
1827
|
+
try {
|
|
1828
|
+
r = JSON.parse(line);
|
|
1829
|
+
} catch {
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (r.type !== "assistant" || typeof r.timestamp !== "string") return;
|
|
1833
|
+
const at = Date.parse(r.timestamp);
|
|
1834
|
+
if (Number.isNaN(at)) return;
|
|
1835
|
+
if (r.isApiErrorMessage === true) {
|
|
1836
|
+
const q = isObj(r.quotaLimits) ? r.quotaLimits : null;
|
|
1837
|
+
const kind = q ? limitKind(q.rateLimitType) : null;
|
|
1838
|
+
if (q && kind && q.status === "rejected" && typeof q.resetsAt === "number") {
|
|
1839
|
+
this.hits.set(`${kind}|${q.resetsAt}`, { at, kind, resetsAt: q.resetsAt * 1e3 });
|
|
1840
|
+
}
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const m = isObj(r.message) ? r.message : null;
|
|
1844
|
+
const usage2 = m && isObj(m.usage) ? m.usage : null;
|
|
1845
|
+
const model = m && typeof m.model === "string" ? m.model : "";
|
|
1846
|
+
if (!m || !usage2 || model === "<synthetic>") return;
|
|
1847
|
+
const key = `${String(m.id)}|${String(r.requestId)}`;
|
|
1848
|
+
this.replies.set(key, { at, units: replyUnits(usage2, model) });
|
|
1849
|
+
}
|
|
1850
|
+
entries() {
|
|
1851
|
+
return [...this.replies.values()].sort((a, b) => a.at - b.at);
|
|
1852
|
+
}
|
|
1853
|
+
limitHits() {
|
|
1854
|
+
return [...this.hits.values()].sort((a, b) => a.at - b.at);
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
function parseClaudeStatusline(json, at) {
|
|
1858
|
+
if (!isObj(json) || !isObj(json.rate_limits)) return null;
|
|
1859
|
+
const rl = json.rate_limits;
|
|
1860
|
+
const windows = [];
|
|
1861
|
+
for (const [key, kind] of [["five_hour", "five_hour"], ["seven_day", "weekly"]]) {
|
|
1862
|
+
const w = rl[key];
|
|
1863
|
+
if (!isObj(w) || typeof w.used_percentage !== "number" || typeof w.resets_at !== "number") continue;
|
|
1864
|
+
windows.push({ kind, usedPercent: w.used_percentage, resetsAt: w.resets_at * 1e3 });
|
|
1865
|
+
}
|
|
1866
|
+
return windows.length ? { at, windows } : null;
|
|
1867
|
+
}
|
|
1868
|
+
var prefixCache = /* @__PURE__ */ new WeakMap();
|
|
1869
|
+
function prefixOf(entries) {
|
|
1870
|
+
let p = prefixCache.get(entries);
|
|
1871
|
+
if (!p) {
|
|
1872
|
+
const at = [];
|
|
1873
|
+
const sum = [0];
|
|
1874
|
+
for (const e of entries) {
|
|
1875
|
+
at.push(e.at);
|
|
1876
|
+
sum.push(sum[sum.length - 1] + e.units);
|
|
1877
|
+
}
|
|
1878
|
+
p = { at, sum };
|
|
1879
|
+
prefixCache.set(entries, p);
|
|
1880
|
+
}
|
|
1881
|
+
return p;
|
|
1882
|
+
}
|
|
1883
|
+
function bound(at, t, after) {
|
|
1884
|
+
let lo = 0;
|
|
1885
|
+
let hi = at.length;
|
|
1886
|
+
while (lo < hi) {
|
|
1887
|
+
const mid = lo + hi >> 1;
|
|
1888
|
+
if (after ? at[mid] <= t : at[mid] < t) lo = mid + 1;
|
|
1889
|
+
else hi = mid;
|
|
1890
|
+
}
|
|
1891
|
+
return lo;
|
|
1892
|
+
}
|
|
1893
|
+
function sumUnits(entries, from, to) {
|
|
1894
|
+
if (to < from) return 0;
|
|
1895
|
+
const p = prefixOf(entries);
|
|
1896
|
+
return p.sum[bound(p.at, to, true)] - p.sum[bound(p.at, from, false)];
|
|
1897
|
+
}
|
|
1898
|
+
function windowMs(kind) {
|
|
1899
|
+
return kind === "weekly" ? WINDOW_MINUTES.weekly * 6e4 : WINDOW_MINUTES.five_hour * 6e4;
|
|
1900
|
+
}
|
|
1901
|
+
function capacitySamples(entries, hits, exact) {
|
|
1902
|
+
const out = [];
|
|
1903
|
+
for (const h of hits) {
|
|
1904
|
+
const units = sumUnits(entries, h.resetsAt - windowMs(h.kind), h.at);
|
|
1905
|
+
if (units > 0) out.push({ kind: h.kind, capacity: units, at: h.at, source: "limit" });
|
|
1906
|
+
}
|
|
1907
|
+
for (const w of exact?.windows ?? []) {
|
|
1908
|
+
if (w.usedPercent < MIN_CALIBRATION_PCT || !exact) continue;
|
|
1909
|
+
const units = sumUnits(entries, w.resetsAt - windowMs(w.kind), exact.at);
|
|
1910
|
+
if (units > 0) out.push({ kind: w.kind, capacity: units / (w.usedPercent / 100), at: exact.at, source: "exact" });
|
|
1911
|
+
}
|
|
1912
|
+
return out;
|
|
1913
|
+
}
|
|
1914
|
+
function mergeSamples(stored, fresh, now) {
|
|
1915
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const s of [...stored, ...fresh]) byKey.set(`${s.kind}|${s.source}|${Math.round(s.at / 6e4)}`, s);
|
|
1917
|
+
const kept = [...byKey.values()].filter((s) => now.getTime() - s.at <= SAMPLE_MAX_AGE_MS).sort((a, b) => b.at - a.at);
|
|
1918
|
+
const perKind = /* @__PURE__ */ new Map();
|
|
1919
|
+
return kept.filter((s) => {
|
|
1920
|
+
const n = (perKind.get(s.kind) ?? 0) + 1;
|
|
1921
|
+
perKind.set(s.kind, n);
|
|
1922
|
+
return n <= 20;
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function pickCapacity(kind, samples, entries, hits, now) {
|
|
1926
|
+
const len = windowMs(kind);
|
|
1927
|
+
for (const s of samples.filter((x) => x.kind === kind).sort((a, b) => b.at - a.at)) {
|
|
1928
|
+
let disproved = false;
|
|
1929
|
+
for (let start = s.at; start < now.getTime() && !disproved; start += len / 5) {
|
|
1930
|
+
const end = Math.min(start + len, now.getTime());
|
|
1931
|
+
const hitInside = hits.some((h) => h.kind === kind && h.at > start && h.at <= end);
|
|
1932
|
+
if (!hitInside && sumUnits(entries, start, end) > s.capacity * DISPROVE_FACTOR) disproved = true;
|
|
1933
|
+
}
|
|
1934
|
+
if (!disproved) return s.capacity;
|
|
1935
|
+
}
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
function currentFiveHourStart(entries, now) {
|
|
1939
|
+
const len = windowMs("five_hour");
|
|
1940
|
+
let start = null;
|
|
1941
|
+
for (const e of entries) {
|
|
1942
|
+
if (e.at > now.getTime()) break;
|
|
1943
|
+
if (start === null || e.at >= start + len) start = Math.floor(e.at / HOUR) * HOUR;
|
|
1944
|
+
}
|
|
1945
|
+
return start !== null && now.getTime() < start + len ? start : null;
|
|
1946
|
+
}
|
|
1947
|
+
function claudeSnapshot({ entries, hits, exact, samples, now }) {
|
|
1948
|
+
const t = now.getTime();
|
|
1949
|
+
const windows = [];
|
|
1950
|
+
let minutesToLimit = null;
|
|
1951
|
+
let observedAt = 0;
|
|
1952
|
+
for (const kind of ["five_hour", "weekly"]) {
|
|
1953
|
+
const len = windowMs(kind);
|
|
1954
|
+
const cap = pickCapacity(kind, samples, entries, hits, now);
|
|
1955
|
+
const ex = exact?.windows.find((w) => w.kind === kind && w.resetsAt > t);
|
|
1956
|
+
const hit = [...hits].reverse().find((h) => h.kind === kind && h.resetsAt > t);
|
|
1957
|
+
let start;
|
|
1958
|
+
let resetsAt;
|
|
1959
|
+
let pct = null;
|
|
1960
|
+
let isExact = false;
|
|
1961
|
+
if (ex && exact) {
|
|
1962
|
+
start = ex.resetsAt - len;
|
|
1963
|
+
resetsAt = ex.resetsAt;
|
|
1964
|
+
const since = sumUnits(entries, exact.at + 1, t);
|
|
1965
|
+
if (since === 0) {
|
|
1966
|
+
pct = ex.usedPercent;
|
|
1967
|
+
isExact = true;
|
|
1968
|
+
} else if (cap) {
|
|
1969
|
+
pct = ex.usedPercent + since / cap * 100;
|
|
1970
|
+
} else {
|
|
1971
|
+
pct = ex.usedPercent;
|
|
1972
|
+
}
|
|
1973
|
+
observedAt = Math.max(observedAt, exact.at);
|
|
1974
|
+
} else if (hit) {
|
|
1975
|
+
start = hit.resetsAt - len;
|
|
1976
|
+
resetsAt = hit.resetsAt;
|
|
1977
|
+
pct = 100;
|
|
1978
|
+
isExact = true;
|
|
1979
|
+
observedAt = Math.max(observedAt, hit.at);
|
|
1980
|
+
} else if (kind === "five_hour") {
|
|
1981
|
+
start = currentFiveHourStart(entries, now);
|
|
1982
|
+
resetsAt = start === null ? null : start + len;
|
|
1983
|
+
if (start === null) {
|
|
1984
|
+
if (cap) {
|
|
1985
|
+
windows.push({ kind, label: "5h", usedPercent: 0, resetsAt: null, exact: false });
|
|
1986
|
+
}
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (cap) pct = sumUnits(entries, start, t) / cap * 100;
|
|
1990
|
+
} else {
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
if (pct === null) continue;
|
|
1994
|
+
pct = Math.min(100, Math.max(0, pct));
|
|
1995
|
+
windows.push({ kind, label: kind === "five_hour" ? "5h" : "Week", usedPercent: pct, resetsAt: resetsAt === null ? null : new Date(resetsAt), exact: isExact });
|
|
1996
|
+
if (cap && pct < 100) {
|
|
1997
|
+
const recent = sumUnits(entries, t - PACE_MS2, t);
|
|
1998
|
+
if (recent > 0) {
|
|
1999
|
+
const perMinute = recent / (PACE_MS2 / 6e4);
|
|
2000
|
+
const minutes = cap * (1 - pct / 100) / perMinute;
|
|
2001
|
+
const fitsBeforeReset = resetsAt === null || t + minutes * 6e4 < resetsAt;
|
|
2002
|
+
if (fitsBeforeReset && (minutesToLimit === null || minutes < minutesToLimit)) minutesToLimit = minutes;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const last = entries[entries.length - 1];
|
|
2007
|
+
if (last) observedAt = Math.max(observedAt, last.at);
|
|
2008
|
+
if (!windows.length) return null;
|
|
2009
|
+
return { tool: "claude-code", windows, observedAt: new Date(observedAt || t), minutesToLimit, plan: null, resetCredits: null };
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// ../../packages/core/src/usage/bridge.ts
|
|
2013
|
+
var import_promises12 = require("node:fs/promises");
|
|
2014
|
+
var import_node_path12 = require("node:path");
|
|
2015
|
+
var BRIDGE_MARKER = "# harlay-usage";
|
|
2016
|
+
function claudeSettingsPath(home2) {
|
|
2017
|
+
return (0, import_node_path12.join)(home2, ".claude", "settings.json");
|
|
2018
|
+
}
|
|
2019
|
+
function claudeStatusFile(dataDir2) {
|
|
2020
|
+
return (0, import_node_path12.join)(dataDir2, "claude-status.json");
|
|
2021
|
+
}
|
|
2022
|
+
var scriptPath = (dataDir2) => (0, import_node_path12.join)(dataDir2, "claude-statusline.sh");
|
|
2023
|
+
var previousJson = (dataDir2) => (0, import_node_path12.join)(dataDir2, "claude-statusline.previous.json");
|
|
2024
|
+
var previousCommand = (dataDir2) => (0, import_node_path12.join)(dataDir2, "claude-statusline.previous");
|
|
2025
|
+
var BRIDGE_SCRIPT = `#!/bin/sh
|
|
2026
|
+
# Harlay's Claude Code status line. Saves Claude Code's status line input (it includes your exact usage)
|
|
2027
|
+
# for Harlay, then shows your previous status line, or a short usage summary if you had none.
|
|
2028
|
+
# Turn it off in Harlay: Settings > Usage > Exact Claude usage.
|
|
2029
|
+
dir=$(dirname "$0")
|
|
2030
|
+
input=$(cat)
|
|
2031
|
+
printf '%s' "$input" > "$dir/claude-status.json.tmp" && mv -f "$dir/claude-status.json.tmp" "$dir/claude-status.json"
|
|
2032
|
+
if [ -s "$dir/claude-statusline.previous" ]; then
|
|
2033
|
+
printf '%s' "$input" | "\${SHELL:-/bin/sh}" -c "$(cat "$dir/claude-statusline.previous")"
|
|
2034
|
+
exit $?
|
|
2035
|
+
fi
|
|
2036
|
+
pct() {
|
|
2037
|
+
printf '%s' "$input" | tr -d '\\n' | sed -n "s/.*\\"$1\\"[^}]*\\"used_percentage\\"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p"
|
|
2038
|
+
}
|
|
2039
|
+
five=$(pct five_hour)
|
|
2040
|
+
week=$(pct seven_day)
|
|
2041
|
+
out=""
|
|
2042
|
+
[ -n "$five" ] && out="5h $five%"
|
|
2043
|
+
[ -n "$week" ] && out="\${out:+$out \xB7 }wk $week%"
|
|
2044
|
+
printf '%s' "$out"
|
|
2045
|
+
`;
|
|
2046
|
+
function bridgeCommand(dataDir2) {
|
|
2047
|
+
const q = `'${scriptPath(dataDir2).replace(/'/g, `'\\''`)}'`;
|
|
2048
|
+
return `${q} ${BRIDGE_MARKER}`;
|
|
2049
|
+
}
|
|
2050
|
+
function isObj2(x) {
|
|
2051
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2052
|
+
}
|
|
2053
|
+
function isOurs(statusLine) {
|
|
2054
|
+
return isObj2(statusLine) && typeof statusLine.command === "string" && statusLine.command.includes(BRIDGE_MARKER);
|
|
2055
|
+
}
|
|
2056
|
+
async function readSettings(path) {
|
|
2057
|
+
let text;
|
|
2058
|
+
try {
|
|
2059
|
+
text = await (0, import_promises12.readFile)(path, "utf8");
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
if (e.code === "ENOENT") return {};
|
|
2062
|
+
throw e;
|
|
2063
|
+
}
|
|
2064
|
+
const parsed = JSON.parse(text);
|
|
2065
|
+
if (!isObj2(parsed)) throw new Error(`${path} is not a JSON object`);
|
|
2066
|
+
return parsed;
|
|
2067
|
+
}
|
|
2068
|
+
async function writeAtomic(path, text) {
|
|
2069
|
+
const tmp = `${path}.harlay-tmp`;
|
|
2070
|
+
await (0, import_promises12.writeFile)(tmp, text);
|
|
2071
|
+
await (0, import_promises12.rename)(tmp, path);
|
|
2072
|
+
}
|
|
2073
|
+
async function bridgeState(home2) {
|
|
2074
|
+
try {
|
|
2075
|
+
return isOurs((await readSettings(claudeSettingsPath(home2))).statusLine) ? "on" : "off";
|
|
2076
|
+
} catch {
|
|
2077
|
+
return "unavailable";
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async function installBridge(home2, dataDir2) {
|
|
2081
|
+
const path = claudeSettingsPath(home2);
|
|
2082
|
+
const s = await readSettings(path);
|
|
2083
|
+
await (0, import_promises12.mkdir)(dataDir2, { recursive: true });
|
|
2084
|
+
await (0, import_promises12.writeFile)(scriptPath(dataDir2), BRIDGE_SCRIPT);
|
|
2085
|
+
await (0, import_promises12.chmod)(scriptPath(dataDir2), 493);
|
|
2086
|
+
if (isOurs(s.statusLine)) return;
|
|
2087
|
+
const prev = s.statusLine;
|
|
2088
|
+
if (prev !== void 0) {
|
|
2089
|
+
await writeAtomic(previousJson(dataDir2), JSON.stringify(prev, null, 2) + "\n");
|
|
2090
|
+
const cmd = isObj2(prev) && typeof prev.command === "string" ? prev.command : "";
|
|
2091
|
+
await writeAtomic(previousCommand(dataDir2), cmd);
|
|
2092
|
+
} else {
|
|
2093
|
+
await (0, import_promises12.rm)(previousJson(dataDir2), { force: true });
|
|
2094
|
+
await (0, import_promises12.rm)(previousCommand(dataDir2), { force: true });
|
|
2095
|
+
}
|
|
2096
|
+
s.statusLine = { ...isObj2(prev) ? prev : {}, type: "command", command: bridgeCommand(dataDir2) };
|
|
2097
|
+
await (0, import_promises12.mkdir)((0, import_node_path12.join)(home2, ".claude"), { recursive: true });
|
|
2098
|
+
await writeAtomic(path, JSON.stringify(s, null, 2) + "\n");
|
|
2099
|
+
}
|
|
2100
|
+
async function removeBridge(home2, dataDir2) {
|
|
2101
|
+
const path = claudeSettingsPath(home2);
|
|
2102
|
+
const s = await readSettings(path);
|
|
2103
|
+
if (!isOurs(s.statusLine)) return;
|
|
2104
|
+
let prev;
|
|
2105
|
+
try {
|
|
2106
|
+
prev = JSON.parse(await (0, import_promises12.readFile)(previousJson(dataDir2), "utf8"));
|
|
2107
|
+
} catch {
|
|
2108
|
+
prev = void 0;
|
|
2109
|
+
}
|
|
2110
|
+
if (prev === void 0) delete s.statusLine;
|
|
2111
|
+
else s.statusLine = prev;
|
|
2112
|
+
await writeAtomic(path, JSON.stringify(s, null, 2) + "\n");
|
|
2113
|
+
await (0, import_promises12.rm)(previousJson(dataDir2), { force: true });
|
|
2114
|
+
await (0, import_promises12.rm)(previousCommand(dataDir2), { force: true });
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/menu.ts
|
|
2118
|
+
var import_node_path14 = require("node:path");
|
|
2119
|
+
|
|
2120
|
+
// src/settings.ts
|
|
2121
|
+
var import_promises13 = require("node:fs/promises");
|
|
2122
|
+
var import_node_path13 = require("node:path");
|
|
1600
2123
|
var THEMES = ["system", "light", "dark"];
|
|
1601
2124
|
var isTheme = (x) => THEMES.includes(x);
|
|
1602
2125
|
var DEFAULT_SETTINGS = {
|
|
@@ -1606,12 +2129,13 @@ var DEFAULT_SETTINGS = {
|
|
|
1606
2129
|
usageThreshold: 90,
|
|
1607
2130
|
terminalApp: "Terminal",
|
|
1608
2131
|
openAtLogin: true,
|
|
1609
|
-
theme: "system"
|
|
2132
|
+
theme: "system",
|
|
2133
|
+
claudeBridgeAsked: false
|
|
1610
2134
|
};
|
|
1611
2135
|
async function loadSettings(file) {
|
|
1612
2136
|
let raw = {};
|
|
1613
2137
|
try {
|
|
1614
|
-
raw = JSON.parse(await (0,
|
|
2138
|
+
raw = JSON.parse(await (0, import_promises13.readFile)(file, "utf8"));
|
|
1615
2139
|
} catch {
|
|
1616
2140
|
raw = {};
|
|
1617
2141
|
}
|
|
@@ -1621,6 +2145,7 @@ async function loadSettings(file) {
|
|
|
1621
2145
|
if (typeof r.autoContinue === "boolean") s.autoContinue = r.autoContinue;
|
|
1622
2146
|
if (typeof r.openAtLogin === "boolean") s.openAtLogin = r.openAtLogin;
|
|
1623
2147
|
if (isTheme(r.theme)) s.theme = r.theme;
|
|
2148
|
+
if (typeof r.claudeBridgeAsked === "boolean") s.claudeBridgeAsked = r.claudeBridgeAsked;
|
|
1624
2149
|
if (typeof r.terminalApp === "string" && r.terminalApp.trim()) s.terminalApp = r.terminalApp.trim();
|
|
1625
2150
|
if (typeof r.usageThreshold === "number" && r.usageThreshold > 0 && r.usageThreshold <= 100) s.usageThreshold = r.usageThreshold;
|
|
1626
2151
|
if (typeof r.modes === "object" && r.modes !== null) {
|
|
@@ -1632,12 +2157,12 @@ async function loadSettings(file) {
|
|
|
1632
2157
|
return s;
|
|
1633
2158
|
}
|
|
1634
2159
|
async function saveSettings(file, s) {
|
|
1635
|
-
await (0,
|
|
1636
|
-
await (0,
|
|
2160
|
+
await (0, import_promises13.mkdir)((0, import_node_path13.dirname)(file), { recursive: true });
|
|
2161
|
+
await (0, import_promises13.writeFile)(file, JSON.stringify(s, null, 2) + "\n");
|
|
1637
2162
|
}
|
|
1638
2163
|
|
|
1639
2164
|
// src/state.ts
|
|
1640
|
-
var
|
|
2165
|
+
var import_promises14 = require("node:fs/promises");
|
|
1641
2166
|
var DEFAULT_LIMIT_MS = 60 * 6e4;
|
|
1642
2167
|
var sessionKey = (ref) => `${ref.tool}:${ref.id}`;
|
|
1643
2168
|
function createState() {
|
|
@@ -1702,7 +2227,7 @@ function latestSession(state2) {
|
|
|
1702
2227
|
async function existingBrief(t) {
|
|
1703
2228
|
if (!t?.briefPath) return null;
|
|
1704
2229
|
try {
|
|
1705
|
-
await (0,
|
|
2230
|
+
await (0, import_promises14.access)(t.briefPath);
|
|
1706
2231
|
return t.briefPath;
|
|
1707
2232
|
} catch {
|
|
1708
2233
|
return null;
|
|
@@ -1715,7 +2240,7 @@ var DOT = { limit: "\u25CF", warning: "\u25D0", idle: "\u25CB", handed_off: "\u2
|
|
|
1715
2240
|
var MODES = ["app", "terminal"];
|
|
1716
2241
|
var THEME_LABEL = { system: "System", light: "Light", dark: "Dark" };
|
|
1717
2242
|
function sessionTitle(t) {
|
|
1718
|
-
return t.ref.title ?? (t.ref.cwd ? (0,
|
|
2243
|
+
return t.ref.title ?? (t.ref.cwd ? (0, import_node_path14.basename)(t.ref.cwd) : t.ref.id.slice(0, 8));
|
|
1719
2244
|
}
|
|
1720
2245
|
function buildMenuModel(sessions, settings2, available2) {
|
|
1721
2246
|
const nodes = [
|
|
@@ -1775,14 +2300,14 @@ function buildMenuModel(sessions, settings2, available2) {
|
|
|
1775
2300
|
}
|
|
1776
2301
|
|
|
1777
2302
|
// src/notifications.ts
|
|
1778
|
-
var
|
|
2303
|
+
var import_node_path15 = require("node:path");
|
|
1779
2304
|
function haltNotificationBody(t, first) {
|
|
1780
|
-
const what = t.ref.cwd ? `${t.detail} \xB7 ${(0,
|
|
2305
|
+
const what = t.ref.cwd ? `${t.detail} \xB7 ${(0, import_node_path15.basename)(t.ref.cwd)}` : t.detail;
|
|
1781
2306
|
return first ? `${what}. Click to continue in ${TOOL_LABEL[first]}.` : `${what}. No other agent is available.`;
|
|
1782
2307
|
}
|
|
1783
2308
|
|
|
1784
2309
|
// src/popover.ts
|
|
1785
|
-
var
|
|
2310
|
+
var import_node_path16 = require("node:path");
|
|
1786
2311
|
var POPOVER_WIDTH = 392;
|
|
1787
2312
|
function timeAgo(d, now) {
|
|
1788
2313
|
const s = Math.max(0, Math.floor((now.getTime() - d.getTime()) / 1e3));
|
|
@@ -1793,7 +2318,44 @@ function timeAgo(d, now) {
|
|
|
1793
2318
|
if (h < 24) return `${h}h ago`;
|
|
1794
2319
|
return `${Math.floor(h / 24)}d ago`;
|
|
1795
2320
|
}
|
|
1796
|
-
function
|
|
2321
|
+
function resetsText(d, now) {
|
|
2322
|
+
if (!d) return null;
|
|
2323
|
+
const ms = d.getTime() - now.getTime();
|
|
2324
|
+
if (ms <= 0) return null;
|
|
2325
|
+
if (ms < 36e5) return `resets in ${Math.max(1, Math.round(ms / 6e4))}m`;
|
|
2326
|
+
const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
|
|
2327
|
+
return ms < 20 * 36e5 ? `resets ${time}` : `resets ${d.toLocaleDateString("en-US", { weekday: "short" })} ${time}`;
|
|
2328
|
+
}
|
|
2329
|
+
function paceText(minutes) {
|
|
2330
|
+
if (minutes === null) return null;
|
|
2331
|
+
const m = Math.max(1, Math.round(minutes));
|
|
2332
|
+
return m < 90 ? `\u2248 ${m} min to limit at this pace` : `\u2248 ${Math.round(m / 60)}h to limit at this pace`;
|
|
2333
|
+
}
|
|
2334
|
+
function buildUsageRows(snapshots, available2, bridge, now) {
|
|
2335
|
+
const rows = [];
|
|
2336
|
+
for (const tool of TOOLS) {
|
|
2337
|
+
const snap = snapshots[tool];
|
|
2338
|
+
const installed = available2[tool].app || available2[tool].terminal;
|
|
2339
|
+
if (!snap && !installed) continue;
|
|
2340
|
+
const row = { tool, label: TOOL_LABEL[tool], windows: [], pace: null, note: null, usagePage: false };
|
|
2341
|
+
if (snap) {
|
|
2342
|
+
row.windows = snap.windows.map((w) => ({ label: w.label, pct: Math.round(w.usedPercent), exact: w.exact, resets: resetsText(w.resetsAt, now) }));
|
|
2343
|
+
row.pace = paceText(snap.minutesToLimit);
|
|
2344
|
+
if (snap.resetCredits) row.note = `${snap.resetCredits} free limit reset${snap.resetCredits === 1 ? "" : "s"} available`;
|
|
2345
|
+
else if (tool === "claude-code" && snap.windows.some((w) => !w.exact)) row.note = "\u2248 estimated from your session logs";
|
|
2346
|
+
} else if (tool === "claude-code") {
|
|
2347
|
+
row.note = bridge === "on" ? "Updates after your next Claude Code reply in Terminal" : "Not measured yet. Turn on Exact Claude usage in Settings, or Harlay learns it at the next Claude limit.";
|
|
2348
|
+
} else if (tool === "codex") {
|
|
2349
|
+
row.note = "Updates after your next Codex turn";
|
|
2350
|
+
} else {
|
|
2351
|
+
row.note = "Cursor keeps no usage on this Mac";
|
|
2352
|
+
row.usagePage = true;
|
|
2353
|
+
}
|
|
2354
|
+
rows.push(row);
|
|
2355
|
+
}
|
|
2356
|
+
return rows;
|
|
2357
|
+
}
|
|
2358
|
+
function buildPopoverModel(sessions, settings2, available2, busy, now, usage2 = [], claudeBridge2 = "off", askClaudeBridge = false) {
|
|
1797
2359
|
const order = [settings2.defaultTarget, ...TOOLS.filter((t) => t !== settings2.defaultTarget)];
|
|
1798
2360
|
return {
|
|
1799
2361
|
sessions: sessions.map((t) => {
|
|
@@ -1805,13 +2367,15 @@ function buildPopoverModel(sessions, settings2, available2, busy, now) {
|
|
|
1805
2367
|
status: t.status,
|
|
1806
2368
|
detail: t.detail,
|
|
1807
2369
|
cwd: t.ref.cwd ?? null,
|
|
1808
|
-
repo: t.ref.cwd ? (0,
|
|
2370
|
+
repo: t.ref.cwd ? (0, import_node_path16.basename)(t.ref.cwd) : null,
|
|
1809
2371
|
ago: timeAgo(t.updatedAt, now),
|
|
1810
2372
|
busy: busy.has(key),
|
|
1811
2373
|
targets: order.filter((x) => x !== t.ref.tool).map((x) => ({ tool: x, label: TOOL_LABEL[x], enabled: available2[x][settings2.modes[x]], isDefault: x === settings2.defaultTarget }))
|
|
1812
2374
|
};
|
|
1813
2375
|
}),
|
|
1814
|
-
|
|
2376
|
+
usage: usage2,
|
|
2377
|
+
askClaudeBridge,
|
|
2378
|
+
settings: { defaultTarget: settings2.defaultTarget, autoContinue: settings2.autoContinue, openAtLogin: settings2.openAtLogin, theme: settings2.theme, claudeBridge: claudeBridge2 },
|
|
1815
2379
|
tools: TOOLS.map((x) => ({ tool: x, label: TOOL_LABEL[x], mode: settings2.modes[x], ...available2[x] })),
|
|
1816
2380
|
hotkey: HOTKEY.label
|
|
1817
2381
|
};
|
|
@@ -1834,6 +2398,10 @@ function isMenuAction(x) {
|
|
|
1834
2398
|
return isTool(a.target) && (a.mode === "app" || a.mode === "terminal");
|
|
1835
2399
|
case "toggleAuto":
|
|
1836
2400
|
case "toggleLogin":
|
|
2401
|
+
case "answerClaudeBridge":
|
|
2402
|
+
return typeof a.allow === "boolean";
|
|
2403
|
+
case "toggleClaudeBridge":
|
|
2404
|
+
case "openCursorUsage":
|
|
1837
2405
|
case "quit":
|
|
1838
2406
|
return true;
|
|
1839
2407
|
default:
|
|
@@ -1847,6 +2415,163 @@ function pickTargets(source, defaultTarget, available2, modes, limitedUntil = {}
|
|
|
1847
2415
|
return order.filter((t) => t !== source && available2[t][modes[t]] && !((limitedUntil[t]?.getTime() ?? 0) > now.getTime()));
|
|
1848
2416
|
}
|
|
1849
2417
|
|
|
2418
|
+
// src/usage.ts
|
|
2419
|
+
var import_node_events2 = require("node:events");
|
|
2420
|
+
var import_node_fs3 = require("node:fs");
|
|
2421
|
+
var import_promises15 = require("node:fs/promises");
|
|
2422
|
+
var import_node_path17 = require("node:path");
|
|
2423
|
+
var CODEX_STALE_MS = 30 * 6e4;
|
|
2424
|
+
var CODEX_QUERY_GAP_MS = 30 * 6e4;
|
|
2425
|
+
var DEBOUNCE_MS = 2e3;
|
|
2426
|
+
var ETA_WARN_PCT = 50;
|
|
2427
|
+
var ETA_WARN_MIN = 20;
|
|
2428
|
+
function clock2(d, now) {
|
|
2429
|
+
const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
2430
|
+
return d.getTime() - now.getTime() < 20 * 36e5 ? time : `${d.toLocaleDateString([], { weekday: "short" })} ${time}`;
|
|
2431
|
+
}
|
|
2432
|
+
var NAME = { "claude-code": "Claude", codex: "Codex", cursor: "Cursor" };
|
|
2433
|
+
var UsageMonitor = class extends import_node_events2.EventEmitter {
|
|
2434
|
+
constructor(opts) {
|
|
2435
|
+
super();
|
|
2436
|
+
this.opts = opts;
|
|
2437
|
+
this.claudeLog = new ClaudeUsageLog(opts.home);
|
|
2438
|
+
}
|
|
2439
|
+
opts;
|
|
2440
|
+
claudeLog;
|
|
2441
|
+
watchers = [];
|
|
2442
|
+
timers = /* @__PURE__ */ new Map();
|
|
2443
|
+
alerted = /* @__PURE__ */ new Set();
|
|
2444
|
+
snapshots = {};
|
|
2445
|
+
samples = [];
|
|
2446
|
+
codexQuery = null;
|
|
2447
|
+
lastCodexQueryAt = 0;
|
|
2448
|
+
stopped = false;
|
|
2449
|
+
get statusFile() {
|
|
2450
|
+
return claudeStatusFile(this.opts.dataDir);
|
|
2451
|
+
}
|
|
2452
|
+
get samplesFile() {
|
|
2453
|
+
return (0, import_node_path17.join)(this.opts.dataDir, "usage-calibration.json");
|
|
2454
|
+
}
|
|
2455
|
+
current() {
|
|
2456
|
+
return this.snapshots;
|
|
2457
|
+
}
|
|
2458
|
+
async start() {
|
|
2459
|
+
await (0, import_promises15.mkdir)(this.opts.dataDir, { recursive: true });
|
|
2460
|
+
try {
|
|
2461
|
+
const raw = JSON.parse(await (0, import_promises15.readFile)(this.samplesFile, "utf8"));
|
|
2462
|
+
if (Array.isArray(raw)) this.samples = raw.filter((s) => typeof s?.capacity === "number" && typeof s?.at === "number" && typeof s?.kind === "string");
|
|
2463
|
+
} catch {
|
|
2464
|
+
this.samples = [];
|
|
2465
|
+
}
|
|
2466
|
+
const watchDir = (dir, tool, recursive) => {
|
|
2467
|
+
try {
|
|
2468
|
+
const w = (0, import_node_fs3.watch)(dir, { recursive }, (_e, name) => {
|
|
2469
|
+
if (!name || String(name).endsWith(".jsonl") || String(name).startsWith("claude-status")) this.schedule(tool);
|
|
2470
|
+
});
|
|
2471
|
+
w.on("error", () => void 0);
|
|
2472
|
+
this.watchers.push(w);
|
|
2473
|
+
} catch {
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
watchDir((0, import_node_path17.join)(this.opts.home, ".codex", "sessions"), "codex", true);
|
|
2477
|
+
watchDir((0, import_node_path17.join)(this.opts.home, ".claude", "projects"), "claude-code", true);
|
|
2478
|
+
watchDir(this.opts.dataDir, "claude-code", false);
|
|
2479
|
+
await Promise.all([this.update("codex"), this.update("claude-code")]);
|
|
2480
|
+
void this.topUpCodex();
|
|
2481
|
+
}
|
|
2482
|
+
stop() {
|
|
2483
|
+
this.stopped = true;
|
|
2484
|
+
for (const w of this.watchers) w.close();
|
|
2485
|
+
for (const t of this.timers.values()) clearTimeout(t);
|
|
2486
|
+
}
|
|
2487
|
+
/** Re-derives readings (e.g. reset times passing) from what is already loaded; cheap. */
|
|
2488
|
+
async refresh() {
|
|
2489
|
+
await Promise.all([this.update("codex"), this.update("claude-code")]);
|
|
2490
|
+
}
|
|
2491
|
+
/** Asks Codex directly if its logs have nothing recent; called when the popover opens. */
|
|
2492
|
+
async topUpCodex() {
|
|
2493
|
+
const now = Date.now();
|
|
2494
|
+
const snap = this.snapshots.codex;
|
|
2495
|
+
if (snap && now - snap.observedAt.getTime() < CODEX_STALE_MS) return;
|
|
2496
|
+
if (now - this.lastCodexQueryAt < CODEX_QUERY_GAP_MS) return;
|
|
2497
|
+
const bin = await this.opts.codexBin();
|
|
2498
|
+
if (!bin) return;
|
|
2499
|
+
this.lastCodexQueryAt = now;
|
|
2500
|
+
const reading = await queryCodexUsage(bin);
|
|
2501
|
+
if (reading) {
|
|
2502
|
+
this.codexQuery = reading;
|
|
2503
|
+
await this.update("codex");
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
schedule(tool) {
|
|
2507
|
+
if (this.stopped) return;
|
|
2508
|
+
clearTimeout(this.timers.get(tool));
|
|
2509
|
+
this.timers.set(tool, setTimeout(() => void this.update(tool), DEBOUNCE_MS));
|
|
2510
|
+
}
|
|
2511
|
+
async update(tool) {
|
|
2512
|
+
const now = /* @__PURE__ */ new Date();
|
|
2513
|
+
let snap = null;
|
|
2514
|
+
try {
|
|
2515
|
+
if (tool === "codex") {
|
|
2516
|
+
snap = codexSnapshot(await readCodexReadings(this.opts.home), now, this.codexQuery);
|
|
2517
|
+
if (snap && snap.resetCredits === null && this.codexQuery) snap.resetCredits = this.codexQuery.resetCredits;
|
|
2518
|
+
} else if (tool === "claude-code") {
|
|
2519
|
+
snap = await this.claude(now);
|
|
2520
|
+
}
|
|
2521
|
+
} catch (e) {
|
|
2522
|
+
console.error("[harlay] usage update failed", e);
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
if (snap) this.snapshots[tool] = snap;
|
|
2526
|
+
else delete this.snapshots[tool];
|
|
2527
|
+
this.emit("update", this.snapshots);
|
|
2528
|
+
if (snap) this.checkAlerts(snap, now);
|
|
2529
|
+
}
|
|
2530
|
+
async claude(now) {
|
|
2531
|
+
await this.claudeLog.refresh(now);
|
|
2532
|
+
let exact = null;
|
|
2533
|
+
try {
|
|
2534
|
+
const s = await (0, import_promises15.stat)(this.statusFile);
|
|
2535
|
+
exact = parseClaudeStatusline(JSON.parse(await (0, import_promises15.readFile)(this.statusFile, "utf8")), s.mtimeMs);
|
|
2536
|
+
} catch {
|
|
2537
|
+
exact = null;
|
|
2538
|
+
}
|
|
2539
|
+
const entries = this.claudeLog.entries();
|
|
2540
|
+
const hits = this.claudeLog.limitHits();
|
|
2541
|
+
const merged = mergeSamples(this.samples, capacitySamples(entries, hits, exact), now);
|
|
2542
|
+
if (JSON.stringify(merged) !== JSON.stringify(this.samples)) {
|
|
2543
|
+
this.samples = merged;
|
|
2544
|
+
await (0, import_promises15.writeFile)(this.samplesFile, JSON.stringify(merged)).catch(() => void 0);
|
|
2545
|
+
}
|
|
2546
|
+
return claudeSnapshot({ entries, hits, exact, samples: this.samples, now });
|
|
2547
|
+
}
|
|
2548
|
+
checkAlerts(s, now) {
|
|
2549
|
+
const threshold = this.opts.threshold();
|
|
2550
|
+
const fire = (key, alert) => {
|
|
2551
|
+
if (this.alerted.has(key)) return;
|
|
2552
|
+
this.alerted.add(key);
|
|
2553
|
+
this.emit("alert", alert);
|
|
2554
|
+
};
|
|
2555
|
+
const resetKey = (w) => w.resetsAt ? Math.round(w.resetsAt.getTime() / 6e5) : "none";
|
|
2556
|
+
for (const w of s.windows) {
|
|
2557
|
+
if (w.usedPercent >= 100 || w.usedPercent < threshold) continue;
|
|
2558
|
+
fire(`${s.tool}|${w.kind}|${resetKey(w)}|level`, {
|
|
2559
|
+
tool: s.tool,
|
|
2560
|
+
title: `${NAME[s.tool]} is at ${w.exact ? "" : "\u2248"}${Math.round(w.usedPercent)}% of its ${w.label === "5h" ? "5-hour" : "weekly"} limit`,
|
|
2561
|
+
body: w.resetsAt ? `Resets ${clock2(w.resetsAt, now)}. Click to hand off the latest ${NAME[s.tool]} session.` : `Click to hand off the latest ${NAME[s.tool]} session.`
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
const full = fullestWindow(s);
|
|
2565
|
+
if (full && s.minutesToLimit !== null && s.minutesToLimit <= ETA_WARN_MIN && full.usedPercent >= ETA_WARN_PCT && full.usedPercent < threshold) {
|
|
2566
|
+
fire(`${s.tool}|${full.kind}|${resetKey(full)}|pace`, {
|
|
2567
|
+
tool: s.tool,
|
|
2568
|
+
title: `${NAME[s.tool]} may hit its limit in about ${Math.max(1, Math.round(s.minutesToLimit))} min`,
|
|
2569
|
+
body: `At the current pace. It's at ${full.exact ? "" : "\u2248"}${Math.round(full.usedPercent)}% of its ${full.label === "5h" ? "5-hour" : "weekly"} limit.`
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
|
|
1850
2575
|
// src/main.ts
|
|
1851
2576
|
var MAX_LIVE_NOTIFICATIONS = 50;
|
|
1852
2577
|
var AUTO_HANDOFF_COOLDOWN_MS = 2 * 6e4;
|
|
@@ -1862,6 +2587,8 @@ var tray = null;
|
|
|
1862
2587
|
var popover = null;
|
|
1863
2588
|
var popoverHiddenAt = 0;
|
|
1864
2589
|
var watcher = null;
|
|
2590
|
+
var usage = null;
|
|
2591
|
+
var claudeBridge = "off";
|
|
1865
2592
|
var lastAutoHandoffAt = 0;
|
|
1866
2593
|
var previewedBriefs = /* @__PURE__ */ new Map();
|
|
1867
2594
|
var errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
@@ -1881,7 +2608,11 @@ function toElectron(nodes) {
|
|
|
1881
2608
|
}
|
|
1882
2609
|
function refreshUi() {
|
|
1883
2610
|
if (!popover || popover.isDestroyed()) return;
|
|
1884
|
-
|
|
2611
|
+
const now = /* @__PURE__ */ new Date();
|
|
2612
|
+
const rows = buildUsageRows(usage?.current() ?? {}, available, claudeBridge, now);
|
|
2613
|
+
const claudeInstalled = available["claude-code"].app || available["claude-code"].terminal;
|
|
2614
|
+
const ask = !settings.claudeBridgeAsked && claudeBridge === "off" && claudeInstalled;
|
|
2615
|
+
popover.webContents.send("harlay:model", buildPopoverModel(recentSessions(state, 8), settings, available, handoffsInFlight, now, rows, claudeBridge, ask));
|
|
1885
2616
|
}
|
|
1886
2617
|
function nativeMenu() {
|
|
1887
2618
|
return import_electron.Menu.buildFromTemplate(toElectron(buildMenuModel(recentSessions(state, 8), settings, available)));
|
|
@@ -1946,14 +2677,14 @@ async function previewBrief(ref) {
|
|
|
1946
2677
|
path = res.written.latestPath;
|
|
1947
2678
|
}
|
|
1948
2679
|
previewedBriefs.set(key, path);
|
|
1949
|
-
send({ path, text: await (0,
|
|
2680
|
+
send({ path, text: await (0, import_promises16.readFile)(path, "utf8") });
|
|
1950
2681
|
} catch (e) {
|
|
1951
2682
|
send({ error: errorMessage(e) });
|
|
1952
2683
|
}
|
|
1953
2684
|
}
|
|
1954
2685
|
async function copyBrief(key) {
|
|
1955
2686
|
const path = previewedBriefs.get(key);
|
|
1956
|
-
if (path) import_electron.clipboard.writeText(await (0,
|
|
2687
|
+
if (path) import_electron.clipboard.writeText(await (0, import_promises16.readFile)(path, "utf8"));
|
|
1957
2688
|
}
|
|
1958
2689
|
async function persist() {
|
|
1959
2690
|
await saveSettings(settingsFile, settings);
|
|
@@ -2002,6 +2733,24 @@ async function onAction(a) {
|
|
|
2002
2733
|
if (import_electron.app.isPackaged) import_electron.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
|
|
2003
2734
|
await persist();
|
|
2004
2735
|
break;
|
|
2736
|
+
case "toggleClaudeBridge":
|
|
2737
|
+
case "answerClaudeBridge": {
|
|
2738
|
+
const on = a.type === "answerClaudeBridge" ? a.allow : claudeBridge !== "on";
|
|
2739
|
+
settings.claudeBridgeAsked = true;
|
|
2740
|
+
await saveSettings(settingsFile, settings);
|
|
2741
|
+
try {
|
|
2742
|
+
if (on) await installBridge(home, dataDir());
|
|
2743
|
+
else if (a.type === "toggleClaudeBridge") await removeBridge(home, dataDir());
|
|
2744
|
+
} catch (e) {
|
|
2745
|
+
notify("Harlay could not change Claude Code settings", errorMessage(e));
|
|
2746
|
+
}
|
|
2747
|
+
claudeBridge = await bridgeState(home);
|
|
2748
|
+
refreshUi();
|
|
2749
|
+
break;
|
|
2750
|
+
}
|
|
2751
|
+
case "openCursorUsage":
|
|
2752
|
+
await import_electron.shell.openExternal("https://cursor.com/dashboard?tab=usage");
|
|
2753
|
+
break;
|
|
2005
2754
|
case "quit":
|
|
2006
2755
|
import_electron.app.quit();
|
|
2007
2756
|
break;
|
|
@@ -2024,33 +2773,36 @@ function onWatchEvent(ev) {
|
|
|
2024
2773
|
void handoff(ev.ref, first);
|
|
2025
2774
|
return;
|
|
2026
2775
|
}
|
|
2027
|
-
const
|
|
2776
|
+
const n = new import_electron.Notification({
|
|
2028
2777
|
title: `${TOOL_LABEL[ev.ref.tool]} stopped: ${name}`,
|
|
2029
2778
|
body: haltNotificationBody(tracked, first),
|
|
2030
2779
|
actions: targets.map((t) => ({ type: "button", text: `Continue in ${TOOL_LABEL[t]}` }))
|
|
2031
2780
|
});
|
|
2032
|
-
keep(
|
|
2033
|
-
|
|
2781
|
+
keep(n);
|
|
2782
|
+
n.on("action", (_e, index) => {
|
|
2034
2783
|
const t = targets[index];
|
|
2035
2784
|
if (t) void handoff(ev.ref, t);
|
|
2036
2785
|
});
|
|
2037
|
-
|
|
2786
|
+
n.on("click", () => {
|
|
2038
2787
|
if (first) void handoff(ev.ref, first);
|
|
2039
2788
|
});
|
|
2040
|
-
|
|
2789
|
+
n.show();
|
|
2041
2790
|
return;
|
|
2042
2791
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
});
|
|
2792
|
+
}
|
|
2793
|
+
function onUsageAlert(a) {
|
|
2794
|
+
const latest = recentSessions(state, 50).find((t) => t.ref.tool === a.tool && t.status !== "handed_off");
|
|
2795
|
+
const [target] = latest ? pickTargets(a.tool, settings.defaultTarget, available, settings.modes, state.limitedUntil, /* @__PURE__ */ new Date()) : [];
|
|
2796
|
+
const n = new import_electron.Notification({ title: a.title, body: latest && target ? a.body : a.body.replace(/ ?Click to hand off.*$/, "") });
|
|
2048
2797
|
keep(n);
|
|
2049
2798
|
n.on("click", () => {
|
|
2050
|
-
if (
|
|
2799
|
+
if (latest && target) void handoff(latest.ref, target);
|
|
2051
2800
|
});
|
|
2052
2801
|
n.show();
|
|
2053
2802
|
}
|
|
2803
|
+
function dataDir() {
|
|
2804
|
+
return import_electron.app.getPath("userData");
|
|
2805
|
+
}
|
|
2054
2806
|
function onHotkey() {
|
|
2055
2807
|
const latest = latestSession(state);
|
|
2056
2808
|
if (!latest) {
|
|
@@ -2088,13 +2840,13 @@ function createPopover() {
|
|
|
2088
2840
|
fullscreenable: false,
|
|
2089
2841
|
skipTaskbar: true,
|
|
2090
2842
|
alwaysOnTop: true,
|
|
2091
|
-
webPreferences: { preload: (0,
|
|
2843
|
+
webPreferences: { preload: (0, import_node_path18.join)(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, sandbox: true }
|
|
2092
2844
|
});
|
|
2093
2845
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
2094
2846
|
win.on("blur", hidePopover);
|
|
2095
2847
|
win.webContents.on("will-navigate", (e) => e.preventDefault());
|
|
2096
2848
|
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
2097
|
-
void win.loadFile((0,
|
|
2849
|
+
void win.loadFile((0, import_node_path18.join)(__dirname, "..", "renderer", "popover.html"));
|
|
2098
2850
|
return win;
|
|
2099
2851
|
}
|
|
2100
2852
|
function showPopover() {
|
|
@@ -2104,6 +2856,7 @@ function showPopover() {
|
|
|
2104
2856
|
const x = Math.min(Math.max(t.x + t.width / 2 - POPOVER_WIDTH / 2, area.x), area.x + area.width - POPOVER_WIDTH);
|
|
2105
2857
|
popover.setPosition(Math.round(x), Math.round(t.y + t.height));
|
|
2106
2858
|
refreshUi();
|
|
2859
|
+
void usage?.topUpCodex();
|
|
2107
2860
|
popover.show();
|
|
2108
2861
|
popover.focus();
|
|
2109
2862
|
popover.webContents.send("harlay:shown");
|
|
@@ -2142,7 +2895,7 @@ function listenToPopover() {
|
|
|
2142
2895
|
});
|
|
2143
2896
|
import_electron.ipcMain.on("harlay:action", (e, action) => {
|
|
2144
2897
|
if (!fromPopover(e) || !isMenuAction(action)) return;
|
|
2145
|
-
if (action.type === "handoff" || action.type === "openBrief") hidePopover();
|
|
2898
|
+
if (action.type === "handoff" || action.type === "openBrief" || action.type === "openCursorUsage") hidePopover();
|
|
2146
2899
|
void onAction(action);
|
|
2147
2900
|
});
|
|
2148
2901
|
}
|
|
@@ -2155,12 +2908,12 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
2155
2908
|
import_electron.app.on("second-instance", revealPopover);
|
|
2156
2909
|
import_electron.app.whenReady().then(async () => {
|
|
2157
2910
|
import_electron.app.dock?.hide();
|
|
2158
|
-
settingsFile = (0,
|
|
2911
|
+
settingsFile = (0, import_node_path18.join)(import_electron.app.getPath("userData"), "settings.json");
|
|
2159
2912
|
settings = await loadSettings(settingsFile);
|
|
2160
2913
|
import_electron.nativeTheme.themeSource = settings.theme;
|
|
2161
2914
|
if (import_electron.app.isPackaged) import_electron.app.setLoginItemSettings({ openAtLogin: settings.openAtLogin });
|
|
2162
2915
|
available = await availableTargets(defaultResolveOptions());
|
|
2163
|
-
const iconPath = (0,
|
|
2916
|
+
const iconPath = (0, import_node_path18.join)(__dirname, "..", "assets", "trayTemplate.png");
|
|
2164
2917
|
const icon = import_electron.nativeImage.createFromPath(iconPath);
|
|
2165
2918
|
icon.setTemplateImage(true);
|
|
2166
2919
|
tray = new import_electron.Tray(icon);
|
|
@@ -2175,11 +2928,25 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
2175
2928
|
listenToPopover();
|
|
2176
2929
|
popover = createPopover();
|
|
2177
2930
|
await refreshSessions();
|
|
2178
|
-
setInterval(() =>
|
|
2931
|
+
setInterval(() => {
|
|
2932
|
+
void refreshSessions();
|
|
2933
|
+
void usage?.refresh();
|
|
2934
|
+
}, 6e4);
|
|
2179
2935
|
watcher = new Watcher(readers, { usageThreshold: settings.usageThreshold });
|
|
2180
2936
|
watcher.on("event", onWatchEvent);
|
|
2181
2937
|
watcher.on("error", (e) => console.error("[harlay] watcher error", e));
|
|
2182
2938
|
await watcher.start();
|
|
2939
|
+
usage = new UsageMonitor({
|
|
2940
|
+
home,
|
|
2941
|
+
dataDir: dataDir(),
|
|
2942
|
+
codexBin: () => resolveBinary("codex", defaultResolveOptions()),
|
|
2943
|
+
threshold: () => settings.usageThreshold
|
|
2944
|
+
});
|
|
2945
|
+
usage.on("update", refreshUi);
|
|
2946
|
+
usage.on("alert", onUsageAlert);
|
|
2947
|
+
claudeBridge = await bridgeState(home);
|
|
2948
|
+
if (claudeBridge === "on") await installBridge(home, dataDir()).catch((e) => console.error("[harlay] status line refresh failed", e));
|
|
2949
|
+
await usage.start().catch((e) => console.error("[harlay] usage monitor failed to start", e));
|
|
2183
2950
|
if (!import_electron.globalShortcut.register(HOTKEY.accelerator, onHotkey)) console.error(`[harlay] could not register ${HOTKEY.accelerator}`);
|
|
2184
2951
|
if (!import_electron.app.getLoginItemSettings().wasOpenedAtLogin) revealPopover();
|
|
2185
2952
|
}).catch((e) => {
|
|
@@ -2190,5 +2957,6 @@ if (!import_electron.app.requestSingleInstanceLock()) {
|
|
|
2190
2957
|
import_electron.app.on("will-quit", () => {
|
|
2191
2958
|
import_electron.globalShortcut.unregisterAll();
|
|
2192
2959
|
watcher?.stop();
|
|
2960
|
+
usage?.stop();
|
|
2193
2961
|
});
|
|
2194
2962
|
}
|