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/renderer/popover.css
CHANGED
|
@@ -87,6 +87,29 @@ button { font: inherit; color: inherit; background: none; border: 0; text-align:
|
|
|
87
87
|
.label { display: flex; align-items: center; gap: 7px; padding: 8px 16px 6px; font: 500 10.5px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--fg3); }
|
|
88
88
|
.label::before { content: ""; width: 5px; height: 5px; background: var(--accent); }
|
|
89
89
|
|
|
90
|
+
/* usage */
|
|
91
|
+
.usage { padding: 0 8px 4px; }
|
|
92
|
+
.usage .label { padding: 2px 8px 6px; }
|
|
93
|
+
.u-row { display: flex; gap: 10px; padding: 6px 8px; }
|
|
94
|
+
.u-name { flex: none; width: 86px; font-size: 12px; font-weight: 600; padding-top: 1px; }
|
|
95
|
+
.u-body { flex: 1; min-width: 0; }
|
|
96
|
+
.u-win { display: flex; align-items: center; gap: 8px; height: 18px; }
|
|
97
|
+
.u-lbl { flex: none; width: 34px; font: 10.5px var(--mono); color: var(--fg3); }
|
|
98
|
+
.u-bar { flex: 1; height: 5px; border-radius: 999px; background: rgba(var(--ink-rgb), .08); overflow: hidden; }
|
|
99
|
+
.u-bar i { display: block; height: 100%; border-radius: inherit; background: var(--accent); }
|
|
100
|
+
.u-bar i.warm { background: var(--warn); }
|
|
101
|
+
.u-bar i.hot { background: var(--err); }
|
|
102
|
+
.u-pct { flex: none; width: 38px; text-align: right; font: 500 11px var(--mono); color: var(--fg); }
|
|
103
|
+
.u-reset { flex: none; width: 92px; font: 10.5px var(--mono); color: var(--fg3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
104
|
+
.u-meta { margin-top: 2px; font-size: 11px; color: var(--fg3); }
|
|
105
|
+
.link.inline { margin: 0 0 0 6px; font-size: 11px; }
|
|
106
|
+
.switch:disabled { opacity: .4; }
|
|
107
|
+
|
|
108
|
+
.ask { margin: 0 8px 8px; padding: 11px 12px; border-radius: 10px; background: var(--card); border: 1px solid rgba(var(--accent-rgb), .35); }
|
|
109
|
+
.ask-title { font-size: 12.5px; font-weight: 600; }
|
|
110
|
+
.ask-body { margin-top: 3px; font-size: 11.5px; color: var(--fg2); }
|
|
111
|
+
.ask-btns { display: flex; gap: 6px; margin-top: 9px; }
|
|
112
|
+
|
|
90
113
|
/* sessions */
|
|
91
114
|
.label.group { padding: 10px 8px 4px; }
|
|
92
115
|
.label.group:first-child { padding-top: 2px; }
|
package/app/renderer/popover.js
CHANGED
|
@@ -122,15 +122,56 @@ function groupSessions(sessions) {
|
|
|
122
122
|
return { attention, projects: [...projects.values()] };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** A bar's fill. The page's CSP forbids style attributes, so the width is set through the CSSOM. */
|
|
126
|
+
function fill(pct, cls) {
|
|
127
|
+
const el = h('i', { class: cls });
|
|
128
|
+
el.style.width = `${Math.max(2, Math.min(100, pct))}%`;
|
|
129
|
+
return el;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Per-agent usage: a bar per window (≈ marks Harlay's estimate), then reset time, pace and notes. */
|
|
133
|
+
function renderUsage() {
|
|
134
|
+
const rows = model.usage ?? [];
|
|
135
|
+
if (!rows.length) return null;
|
|
136
|
+
const ask = model.askClaudeBridge
|
|
137
|
+
? h('div', { class: 'ask' },
|
|
138
|
+
h('p', { class: 'ask-title' }, 'Show Claude’s exact usage?'),
|
|
139
|
+
h('p', { class: 'ask-body' }, 'Claude Code in Terminal can pass its real usage to Harlay through its status line. Harlay adds one line to ~/.claude/settings.json; a status line you already have keeps working.'),
|
|
140
|
+
h('div', { class: 'ask-btns' },
|
|
141
|
+
h('button', { class: 'btn primary', onclick: () => api.act({ type: 'answerClaudeBridge', allow: true }) }, 'Allow'),
|
|
142
|
+
h('button', { class: 'btn', onclick: () => api.act({ type: 'answerClaudeBridge', allow: false }) }, 'Not now')))
|
|
143
|
+
: null;
|
|
144
|
+
return h('div', { class: 'usage' },
|
|
145
|
+
h('div', { class: 'label' }, 'Usage'),
|
|
146
|
+
ask,
|
|
147
|
+
rows.map((r) => {
|
|
148
|
+
const level = (pct) => (pct >= 90 ? ' hot' : pct >= 70 ? ' warm' : '');
|
|
149
|
+
const meta = [r.windows.length === 1 ? r.windows[0].resets : null, r.pace, r.note].filter(Boolean);
|
|
150
|
+
return h('div', { class: 'u-row' },
|
|
151
|
+
h('div', { class: 'u-name' }, r.label),
|
|
152
|
+
h('div', { class: 'u-body' },
|
|
153
|
+
r.windows.map((w) => h('div', { class: 'u-win', title: w.exact ? 'Reported by the agent' : 'Harlay’s estimate from your session logs' },
|
|
154
|
+
h('span', { class: 'u-lbl' }, w.label),
|
|
155
|
+
h('span', { class: 'u-bar' }, fill(w.pct, level(w.pct))),
|
|
156
|
+
h('span', { class: 'u-pct' }, `${w.exact ? '' : '≈'}${w.pct}%`),
|
|
157
|
+
r.windows.length > 1 && w.resets ? h('span', { class: 'u-reset' }, w.resets.replace('resets ', '')) : null)),
|
|
158
|
+
meta.length || r.usagePage
|
|
159
|
+
? h('div', { class: 'u-meta' }, meta.join(' · '),
|
|
160
|
+
r.usagePage ? h('button', { class: 'link inline', onclick: () => api.act({ type: 'openCursorUsage' }) }, 'Open usage page') : null)
|
|
161
|
+
: null));
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
125
165
|
function renderHome() {
|
|
126
166
|
const { sessions } = model;
|
|
127
167
|
if (!sessions.length) {
|
|
128
|
-
$('#home').replaceChildren(renderHero(), h('p', { class: 'empty' }, 'Harlay reads session logs from ~/.claude, ~/.codex and ~/.cursor on this Mac.'));
|
|
168
|
+
$('#home').replaceChildren(renderHero(), renderUsage(), h('p', { class: 'empty' }, 'Harlay reads session logs from ~/.claude, ~/.codex and ~/.cursor on this Mac.'));
|
|
129
169
|
return;
|
|
130
170
|
}
|
|
131
171
|
const { attention, projects } = groupSessions(sessions);
|
|
132
172
|
$('#home').replaceChildren(
|
|
133
173
|
renderHero(),
|
|
174
|
+
renderUsage(),
|
|
134
175
|
h('div', { class: 'scroll' },
|
|
135
176
|
attention.length ? [h('div', { class: 'label group attn' }, 'Needs attention'), attention.map((s) => renderSession(s, true))] : null,
|
|
136
177
|
projects.map((p) => [
|
|
@@ -144,10 +185,10 @@ function seg(options, current, onPick) {
|
|
|
144
185
|
h('button', { 'aria-pressed': String(o.value === current), disabled: o.disabled, onclick: () => onPick(o.value) }, o.label)));
|
|
145
186
|
}
|
|
146
187
|
|
|
147
|
-
function toggleRow(label, hint, checked, action) {
|
|
188
|
+
function toggleRow(label, hint, checked, action, disabled = false) {
|
|
148
189
|
return h('div', { class: 'set-row' },
|
|
149
190
|
h('span', { class: 'k' }, label, hint ? h('span', { class: 'hint' }, hint) : null),
|
|
150
|
-
h('button', { class: 'switch', role: 'switch', 'aria-checked': String(checked), 'aria-label': label, onclick: () => api.act(action) }));
|
|
191
|
+
h('button', { class: 'switch', role: 'switch', disabled, 'aria-checked': String(checked), 'aria-label': label, onclick: () => api.act(action) }));
|
|
151
192
|
}
|
|
152
193
|
|
|
153
194
|
function renderSettings() {
|
|
@@ -172,7 +213,16 @@ function renderSettings() {
|
|
|
172
213
|
], settings.theme, (v) => api.act({ type: 'setTheme', theme: v })),
|
|
173
214
|
h('div', { class: 'label' }, 'Behavior'),
|
|
174
215
|
toggleRow('Auto-continue', 'Hand off without asking when an agent stops', settings.autoContinue, { type: 'toggleAuto' }),
|
|
175
|
-
toggleRow('Open at login', null, settings.openAtLogin, { type: 'toggleLogin' })
|
|
216
|
+
toggleRow('Open at login', null, settings.openAtLogin, { type: 'toggleLogin' }),
|
|
217
|
+
h('div', { class: 'label' }, 'Usage'),
|
|
218
|
+
toggleRow(
|
|
219
|
+
'Exact Claude usage',
|
|
220
|
+
settings.claudeBridge === 'unavailable'
|
|
221
|
+
? 'Claude Code’s ~/.claude/settings.json isn’t valid JSON, so Harlay leaves it alone.'
|
|
222
|
+
: 'Claude Code in Terminal passes its real usage to Harlay through its status line. Your own status line keeps working; turning this off restores it.',
|
|
223
|
+
settings.claudeBridge === 'on',
|
|
224
|
+
{ type: 'toggleClaudeBridge' },
|
|
225
|
+
settings.claudeBridge === 'unavailable')));
|
|
176
226
|
}
|
|
177
227
|
|
|
178
228
|
/** Inline markdown: `code` and **bold**; everything else stays plain text. */
|
|
@@ -353,7 +403,12 @@ function demoApi() {
|
|
|
353
403
|
{ tool: 'codex', label: 'Codex', mode: 'app', app: true, terminal: true },
|
|
354
404
|
{ tool: 'cursor', label: 'Cursor', mode: 'app', app: true, terminal: false },
|
|
355
405
|
];
|
|
356
|
-
const settings = { defaultTarget: 'codex', autoContinue: false, openAtLogin: true, theme: 'system' };
|
|
406
|
+
const settings = { defaultTarget: 'codex', autoContinue: false, openAtLogin: true, theme: 'system', claudeBridge: 'off' };
|
|
407
|
+
const usage = [
|
|
408
|
+
{ tool: 'claude-code', label: 'Claude Code', windows: [{ label: '5h', pct: 72, exact: false, resets: 'resets 4:10 PM' }], pace: '≈ 38 min to limit at this pace', note: '≈ estimated from your session logs', usagePage: false },
|
|
409
|
+
{ tool: 'codex', label: 'Codex', windows: [{ label: '5h', pct: 18, exact: true, resets: 'resets in 52m' }, { label: 'Week', pct: 41, exact: true, resets: 'resets Fri 7:12 PM' }], pace: null, note: '1 free limit reset available', usagePage: false },
|
|
410
|
+
{ tool: 'cursor', label: 'Cursor', windows: [], pace: null, note: 'Cursor keeps no usage on this Mac', usagePage: true },
|
|
411
|
+
];
|
|
357
412
|
const targets = (from) => {
|
|
358
413
|
const order = [settings.defaultTarget, ...tools.map((t) => t.tool).filter((t) => t !== settings.defaultTarget)];
|
|
359
414
|
return order.filter((t) => t !== from).map((t) => {
|
|
@@ -373,6 +428,8 @@ function demoApi() {
|
|
|
373
428
|
const sampleBrief = (title) => `---\nharlay: 1\n---\n\n# Continue: ${title}\n\n> You are continuing a coding task that another AI agent started.\n\n## 1. Goal (user's words)\n> Move auth from session cookies to JWT across \`api/\`.\n\n## 2. Next step\nMigrate the last 3 route handlers in \`api/routes/\`.\n\n## 4. Progress\n- [x] Added \`src/lib/jwt.ts\`\n- [ ] Migrate \`billing.ts\`\n\n\`\`\`text\nnpm test -- auth\n\`\`\`\n`;
|
|
374
429
|
const emit = () => onModel({
|
|
375
430
|
sessions: sessions.map((s) => ({ ...s, targets: targets(s.from) })),
|
|
431
|
+
usage,
|
|
432
|
+
askClaudeBridge: !settings.asked,
|
|
376
433
|
settings: { ...settings },
|
|
377
434
|
tools: tools.map((t) => ({ ...t })),
|
|
378
435
|
hotkey: '⌥H',
|
|
@@ -391,6 +448,11 @@ function demoApi() {
|
|
|
391
448
|
if (a.type === 'toggleAuto') settings.autoContinue = !settings.autoContinue;
|
|
392
449
|
if (a.type === 'toggleLogin') settings.openAtLogin = !settings.openAtLogin;
|
|
393
450
|
if (a.type === 'setTheme') settings.theme = a.theme;
|
|
451
|
+
if (a.type === 'answerClaudeBridge') {
|
|
452
|
+
settings.asked = true;
|
|
453
|
+
settings.claudeBridge = a.allow ? 'on' : 'off';
|
|
454
|
+
}
|
|
455
|
+
if (a.type === 'toggleClaudeBridge') settings.claudeBridge = settings.claudeBridge === 'on' ? 'off' : 'on';
|
|
394
456
|
if (a.type === 'previewBrief') {
|
|
395
457
|
const s = sessions.find((x) => x.key === a.key);
|
|
396
458
|
setTimeout(() => onBrief({ key: s.key, title: s.title, path: `${s.cwd}/.harlay/HANDOFF.md`, text: sampleBrief(s.title) }), 600);
|
package/dist/harlay.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { homedir as
|
|
4
|
+
import { homedir as homedir4 } from "node:os";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
|
|
7
7
|
// ../core/src/types.ts
|
|
@@ -1610,12 +1610,70 @@ var Watcher = class extends EventEmitter {
|
|
|
1610
1610
|
}
|
|
1611
1611
|
};
|
|
1612
1612
|
|
|
1613
|
+
// ../core/src/usage/codex.ts
|
|
1614
|
+
var PACE_MS = 60 * 6e4;
|
|
1615
|
+
var TAIL_BYTES = 1024 * 1024;
|
|
1616
|
+
|
|
1617
|
+
// ../core/src/usage/claude.ts
|
|
1618
|
+
var HOUR = 36e5;
|
|
1619
|
+
var SAMPLE_MAX_AGE_MS = 30 * 24 * HOUR;
|
|
1620
|
+
var PACE_MS2 = 30 * 6e4;
|
|
1621
|
+
|
|
1622
|
+
// ../core/src/usage/bridge.ts
|
|
1623
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile5, rename, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
1624
|
+
import { join as join9 } from "node:path";
|
|
1625
|
+
var BRIDGE_MARKER = "# harlay-usage";
|
|
1626
|
+
function claudeSettingsPath(home) {
|
|
1627
|
+
return join9(home, ".claude", "settings.json");
|
|
1628
|
+
}
|
|
1629
|
+
var previousJson = (dataDir) => join9(dataDir, "claude-statusline.previous.json");
|
|
1630
|
+
var previousCommand = (dataDir) => join9(dataDir, "claude-statusline.previous");
|
|
1631
|
+
function isObj2(x) {
|
|
1632
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
1633
|
+
}
|
|
1634
|
+
function isOurs(statusLine) {
|
|
1635
|
+
return isObj2(statusLine) && typeof statusLine.command === "string" && statusLine.command.includes(BRIDGE_MARKER);
|
|
1636
|
+
}
|
|
1637
|
+
async function readSettings(path) {
|
|
1638
|
+
let text;
|
|
1639
|
+
try {
|
|
1640
|
+
text = await readFile5(path, "utf8");
|
|
1641
|
+
} catch (e) {
|
|
1642
|
+
if (e.code === "ENOENT") return {};
|
|
1643
|
+
throw e;
|
|
1644
|
+
}
|
|
1645
|
+
const parsed = JSON.parse(text);
|
|
1646
|
+
if (!isObj2(parsed)) throw new Error(`${path} is not a JSON object`);
|
|
1647
|
+
return parsed;
|
|
1648
|
+
}
|
|
1649
|
+
async function writeAtomic(path, text) {
|
|
1650
|
+
const tmp = `${path}.harlay-tmp`;
|
|
1651
|
+
await writeFile3(tmp, text);
|
|
1652
|
+
await rename(tmp, path);
|
|
1653
|
+
}
|
|
1654
|
+
async function removeBridge(home, dataDir) {
|
|
1655
|
+
const path = claudeSettingsPath(home);
|
|
1656
|
+
const s = await readSettings(path);
|
|
1657
|
+
if (!isOurs(s.statusLine)) return;
|
|
1658
|
+
let prev;
|
|
1659
|
+
try {
|
|
1660
|
+
prev = JSON.parse(await readFile5(previousJson(dataDir), "utf8"));
|
|
1661
|
+
} catch {
|
|
1662
|
+
prev = void 0;
|
|
1663
|
+
}
|
|
1664
|
+
if (prev === void 0) delete s.statusLine;
|
|
1665
|
+
else s.statusLine = prev;
|
|
1666
|
+
await writeAtomic(path, JSON.stringify(s, null, 2) + "\n");
|
|
1667
|
+
await rm(previousJson(dataDir), { force: true });
|
|
1668
|
+
await rm(previousCommand(dataDir), { force: true });
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1613
1671
|
// src/app.ts
|
|
1614
1672
|
import { execFile as execFile3 } from "node:child_process";
|
|
1615
1673
|
import { existsSync } from "node:fs";
|
|
1616
|
-
import { cp, mkdir as
|
|
1617
|
-
import { tmpdir } from "node:os";
|
|
1618
|
-
import { dirname as dirname3, join as
|
|
1674
|
+
import { cp, mkdir as mkdir4, mkdtemp, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
1675
|
+
import { homedir as homedir3, tmpdir } from "node:os";
|
|
1676
|
+
import { dirname as dirname3, join as join10, resolve } from "node:path";
|
|
1619
1677
|
import { fileURLToPath } from "node:url";
|
|
1620
1678
|
import { promisify as promisify3 } from "node:util";
|
|
1621
1679
|
var run = promisify3(execFile3);
|
|
@@ -1624,19 +1682,19 @@ var APP_BUNDLE_ID = "com.corelabs.harlay";
|
|
|
1624
1682
|
var APP_FILES = ["dist", "renderer", "assets"];
|
|
1625
1683
|
function findAppSource(cliFile = fileURLToPath(import.meta.url)) {
|
|
1626
1684
|
const pkg = resolve(dirname3(cliFile), "..");
|
|
1627
|
-
for (const dir of [
|
|
1628
|
-
if (existsSync(
|
|
1685
|
+
for (const dir of [join10(pkg, "app"), resolve(pkg, "..", "..", "apps", "desktop")]) {
|
|
1686
|
+
if (existsSync(join10(dir, "dist", "main.cjs")) && existsSync(join10(dir, "renderer", "popover.html")) && existsSync(join10(dir, "assets", "icon.png"))) return dir;
|
|
1629
1687
|
}
|
|
1630
1688
|
return null;
|
|
1631
1689
|
}
|
|
1632
1690
|
async function makeIcns(png, work) {
|
|
1633
|
-
const set =
|
|
1634
|
-
await
|
|
1691
|
+
const set = join10(work, "icon.iconset");
|
|
1692
|
+
await mkdir4(set);
|
|
1635
1693
|
for (const size of [16, 32, 128, 256, 512]) {
|
|
1636
|
-
await run("sips", ["-z", String(size), String(size), png, "--out",
|
|
1637
|
-
await run("sips", ["-z", String(size * 2), String(size * 2), png, "--out",
|
|
1694
|
+
await run("sips", ["-z", String(size), String(size), png, "--out", join10(set, `icon_${size}x${size}.png`)]);
|
|
1695
|
+
await run("sips", ["-z", String(size * 2), String(size * 2), png, "--out", join10(set, `icon_${size}x${size}@2x.png`)]);
|
|
1638
1696
|
}
|
|
1639
|
-
const icns =
|
|
1697
|
+
const icns = join10(work, "icon.icns");
|
|
1640
1698
|
await run("iconutil", ["-c", "icns", set, "-o", icns]);
|
|
1641
1699
|
return icns;
|
|
1642
1700
|
}
|
|
@@ -1654,25 +1712,47 @@ async function quitRunningApp() {
|
|
|
1654
1712
|
await new Promise((r) => setTimeout(r, 250));
|
|
1655
1713
|
}
|
|
1656
1714
|
}
|
|
1715
|
+
function harlayDataDir(home = homedir3()) {
|
|
1716
|
+
return join10(home, "Library", "Application Support", "Harlay");
|
|
1717
|
+
}
|
|
1718
|
+
async function uninstallApp(opts) {
|
|
1719
|
+
await quitRunningApp();
|
|
1720
|
+
try {
|
|
1721
|
+
await removeBridge(opts.home, harlayDataDir(opts.home));
|
|
1722
|
+
opts.log("Restored your Claude Code status line settings.\n");
|
|
1723
|
+
} catch (e) {
|
|
1724
|
+
opts.log(`Left ~/.claude/settings.json unchanged: ${e instanceof Error ? e.message : String(e)}
|
|
1725
|
+
`);
|
|
1726
|
+
}
|
|
1727
|
+
const app = join10(opts.dir, "Harlay.app");
|
|
1728
|
+
if (!existsSync(app)) {
|
|
1729
|
+
opts.log(`No Harlay.app in ${opts.dir}.
|
|
1730
|
+
`);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
await run("osascript", ["-e", "on run argv", "-e", 'tell application "Finder" to delete (POSIX file (item 1 of argv) as alias)', "-e", "end run", app]);
|
|
1734
|
+
opts.log(`Moved ${app} to the Trash.
|
|
1735
|
+
`);
|
|
1736
|
+
}
|
|
1657
1737
|
async function installApp(opts) {
|
|
1658
1738
|
if (process.platform !== "darwin") throw new Error("The Harlay app is macOS only; the CLI works without it.");
|
|
1659
1739
|
const source = findAppSource();
|
|
1660
1740
|
if (!source) throw new Error("App files are missing from this install. Reinstall with `npm install -g harlay`.");
|
|
1661
|
-
const work = await mkdtemp(
|
|
1741
|
+
const work = await mkdtemp(join10(tmpdir(), "harlay-app-"));
|
|
1662
1742
|
try {
|
|
1663
|
-
const appSrc =
|
|
1664
|
-
for (const f of APP_FILES) await cp(
|
|
1665
|
-
await
|
|
1666
|
-
|
|
1743
|
+
const appSrc = join10(work, "src");
|
|
1744
|
+
for (const f of APP_FILES) await cp(join10(source, f), join10(appSrc, f), { recursive: true });
|
|
1745
|
+
await writeFile4(
|
|
1746
|
+
join10(appSrc, "package.json"),
|
|
1667
1747
|
JSON.stringify({ name: "harlay-desktop", productName: "Harlay", version: opts.version, main: "dist/main.cjs" }, null, 2)
|
|
1668
1748
|
);
|
|
1669
|
-
const icon = await makeIcns(
|
|
1749
|
+
const icon = await makeIcns(join10(source, "assets", "icon.png"), work);
|
|
1670
1750
|
opts.log(`Building Harlay.app (Electron ${ELECTRON_VERSION}; the first install downloads it, about 100 MB)\u2026
|
|
1671
1751
|
`);
|
|
1672
1752
|
const { packager } = await import("@electron/packager");
|
|
1673
1753
|
const [outDir] = await packager({
|
|
1674
1754
|
dir: appSrc,
|
|
1675
|
-
out:
|
|
1755
|
+
out: join10(work, "out"),
|
|
1676
1756
|
name: "Harlay",
|
|
1677
1757
|
platform: "darwin",
|
|
1678
1758
|
arch: process.arch === "arm64" ? "arm64" : "x64",
|
|
@@ -1687,24 +1767,24 @@ async function installApp(opts) {
|
|
|
1687
1767
|
quiet: true,
|
|
1688
1768
|
extendInfo: { LSUIElement: true, NSUserNotificationAlertStyle: "alert" }
|
|
1689
1769
|
});
|
|
1690
|
-
const built =
|
|
1770
|
+
const built = join10(outDir, "Harlay.app");
|
|
1691
1771
|
await run("codesign", ["--force", "--deep", "--sign", "-", built]);
|
|
1692
|
-
const dest =
|
|
1772
|
+
const dest = join10(opts.dir, "Harlay.app");
|
|
1693
1773
|
await quitRunningApp();
|
|
1694
|
-
await
|
|
1695
|
-
await
|
|
1774
|
+
await mkdir4(opts.dir, { recursive: true });
|
|
1775
|
+
await rm2(dest, { recursive: true, force: true });
|
|
1696
1776
|
await run("ditto", [built, dest]);
|
|
1697
1777
|
opts.log(`Installed ${dest}
|
|
1698
1778
|
`);
|
|
1699
1779
|
if (opts.open) await run("open", [dest]);
|
|
1700
1780
|
return dest;
|
|
1701
1781
|
} finally {
|
|
1702
|
-
await
|
|
1782
|
+
await rm2(work, { recursive: true, force: true });
|
|
1703
1783
|
}
|
|
1704
1784
|
}
|
|
1705
1785
|
|
|
1706
1786
|
// src/main.ts
|
|
1707
|
-
var VERSION = "0.1.
|
|
1787
|
+
var VERSION = "0.1.2";
|
|
1708
1788
|
var HELP = `harlay ${VERSION} \u2014 hand off a stalled AI coding session to another agent
|
|
1709
1789
|
|
|
1710
1790
|
Usage:
|
|
@@ -1712,6 +1792,7 @@ Usage:
|
|
|
1712
1792
|
harlay handoff --to <claude|codex|cursor> [--session <tool>:<id>] [--mode app|terminal] [--no-launch] [--terminal <App>]
|
|
1713
1793
|
harlay watch [--auto <claude|codex|cursor>] [--mode app|terminal] [--threshold N]
|
|
1714
1794
|
harlay app install [--dir <folder>] [--no-open] install or update the macOS menu bar app (default folder: /Applications)
|
|
1795
|
+
harlay app uninstall [--dir <folder>] quit it, restore Claude Code's status line, move it to the Trash
|
|
1715
1796
|
harlay --version
|
|
1716
1797
|
`;
|
|
1717
1798
|
var UsageError = class extends Error {
|
|
@@ -1775,7 +1856,7 @@ async function cmdHandoff(args, readers, ctx) {
|
|
|
1775
1856
|
if (!values.to) throw new UsageError("--to is required (claude, codex or cursor)");
|
|
1776
1857
|
const target = parseTool(values.to);
|
|
1777
1858
|
const mode = parseMode(values.mode, target);
|
|
1778
|
-
const session = await pickSession(readers, values.session, ctx.cwd, ctx.env.HARLAY_HOME ??
|
|
1859
|
+
const session = await pickSession(readers, values.session, ctx.cwd, ctx.env.HARLAY_HOME ?? homedir4());
|
|
1779
1860
|
const res = await runHandoff({
|
|
1780
1861
|
session,
|
|
1781
1862
|
target,
|
|
@@ -1836,7 +1917,19 @@ async function cmdWatch(args, readers, ctx) {
|
|
|
1836
1917
|
}
|
|
1837
1918
|
async function cmdApp(args, ctx) {
|
|
1838
1919
|
const [sub, ...rest] = args;
|
|
1839
|
-
if (sub
|
|
1920
|
+
if (sub === "uninstall") {
|
|
1921
|
+
const { values: values2 } = parseArgs({ args: rest, options: { dir: { type: "string" } }, strict: true });
|
|
1922
|
+
try {
|
|
1923
|
+
await uninstallApp({ dir: values2.dir ?? "/Applications", home: ctx.env.HARLAY_HOME ?? homedir4(), log: ctx.out });
|
|
1924
|
+
} catch (e) {
|
|
1925
|
+
ctx.err(`harlay: could not uninstall the app: ${e instanceof Error ? e.message : String(e)}
|
|
1926
|
+
`);
|
|
1927
|
+
return 1;
|
|
1928
|
+
}
|
|
1929
|
+
ctx.out("Uninstall the CLI too with: npm rm -g harlay\n");
|
|
1930
|
+
return 0;
|
|
1931
|
+
}
|
|
1932
|
+
if (sub !== "install") throw new UsageError(sub ? `Unknown app command: ${sub}` : "Usage: harlay app install|uninstall [--dir <folder>]");
|
|
1840
1933
|
const { values } = parseArgs({ args: rest, options: { dir: { type: "string" }, "no-open": { type: "boolean" } }, strict: true });
|
|
1841
1934
|
try {
|
|
1842
1935
|
await installApp({ dir: values.dir ?? "/Applications", version: VERSION, open: !values["no-open"], log: ctx.out });
|
|
@@ -1849,7 +1942,7 @@ async function cmdApp(args, ctx) {
|
|
|
1849
1942
|
return 0;
|
|
1850
1943
|
}
|
|
1851
1944
|
async function main(argv, ctx = defaultContext()) {
|
|
1852
|
-
const readers = createReaders({ home: ctx.env.HARLAY_HOME ??
|
|
1945
|
+
const readers = createReaders({ home: ctx.env.HARLAY_HOME ?? homedir4(), slugRoot: ctx.env.HARLAY_SLUG_ROOT });
|
|
1853
1946
|
const [cmd, ...rest] = argv;
|
|
1854
1947
|
try {
|
|
1855
1948
|
switch (cmd) {
|
package/package.json
CHANGED