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.
@@ -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; }
@@ -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 homedir3 } from "node:os";
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 mkdir3, mkdtemp, rm, writeFile as writeFile3 } from "node:fs/promises";
1617
- import { tmpdir } from "node:os";
1618
- import { dirname as dirname3, join as join9, resolve } from "node:path";
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 [join9(pkg, "app"), resolve(pkg, "..", "..", "apps", "desktop")]) {
1628
- if (existsSync(join9(dir, "dist", "main.cjs")) && existsSync(join9(dir, "renderer", "popover.html")) && existsSync(join9(dir, "assets", "icon.png"))) return dir;
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 = join9(work, "icon.iconset");
1634
- await mkdir3(set);
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", join9(set, `icon_${size}x${size}.png`)]);
1637
- await run("sips", ["-z", String(size * 2), String(size * 2), png, "--out", join9(set, `icon_${size}x${size}@2x.png`)]);
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 = join9(work, "icon.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(join9(tmpdir(), "harlay-app-"));
1741
+ const work = await mkdtemp(join10(tmpdir(), "harlay-app-"));
1662
1742
  try {
1663
- const appSrc = join9(work, "src");
1664
- for (const f of APP_FILES) await cp(join9(source, f), join9(appSrc, f), { recursive: true });
1665
- await writeFile3(
1666
- join9(appSrc, "package.json"),
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(join9(source, "assets", "icon.png"), work);
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: join9(work, "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 = join9(outDir, "Harlay.app");
1770
+ const built = join10(outDir, "Harlay.app");
1691
1771
  await run("codesign", ["--force", "--deep", "--sign", "-", built]);
1692
- const dest = join9(opts.dir, "Harlay.app");
1772
+ const dest = join10(opts.dir, "Harlay.app");
1693
1773
  await quitRunningApp();
1694
- await mkdir3(opts.dir, { recursive: true });
1695
- await rm(dest, { recursive: true, force: true });
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 rm(work, { recursive: true, force: true });
1782
+ await rm2(work, { recursive: true, force: true });
1703
1783
  }
1704
1784
  }
1705
1785
 
1706
1786
  // src/main.ts
1707
- var VERSION = "0.1.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 ?? homedir3());
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 !== "install") throw new UsageError(sub ? `Unknown app command: ${sub}` : "Usage: harlay app install [--dir <folder>] [--no-open]");
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 ?? homedir3(), slugRoot: ctx.env.HARLAY_SLUG_ROOT });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harlay",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Hand off a stalled AI coding session (Claude Code, Codex, Cursor) to another agent. CLI and macOS menu bar app.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",