tryharness 0.7.1 → 0.7.3

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.
Files changed (3) hide show
  1. package/cli.mjs +23 -2
  2. package/lib/desktop.mjs +105 -0
  3. package/package.json +7 -1
package/cli.mjs CHANGED
@@ -11,6 +11,7 @@ import { paths } from "./lib/paths.mjs";
11
11
  import { currentRuntime, installRuntime } from "./lib/runtime-install.mjs";
12
12
  import { ensureModel, modelPresent } from "./lib/embedder.mjs";
13
13
  import { installService, serviceInfo, uninstallService } from "./lib/service.mjs";
14
+ import { canShowDesktop, desktopSource, desktopTarget, installDesktop, openDesktop, retireDesktop } from "./lib/desktop.mjs";
14
15
 
15
16
  const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
16
17
  const PACKAGE = readJson(path.join(SOURCE_ROOT, "package.json"), { version: "0.4.3" });
@@ -158,6 +159,7 @@ async function connect() {
158
159
  const service = installService({ runtimeRoot });
159
160
  const ready = await waitForDaemon();
160
161
  console.log(`daemon: ready via ${service.manager} and connected to Harness`);
162
+ if (!has("--no-desktop")) desktop();
161
163
  // The context space's embedder lives with the daemon and comes down with setup, so the machine
162
164
  // can search a task by meaning before the first task exists. About 110 MB, once.
163
165
  try {
@@ -175,6 +177,23 @@ async function connect() {
175
177
  console.log("Done. You can close this terminal.");
176
178
  }
177
179
 
180
+ // The desktop app: installed from this machine's platform package and opened. Never fatal; the
181
+ // daemon works without it.
182
+ function desktop() {
183
+ if (!canShowDesktop({})) { console.log("desktop: skipped (no screen on this login)"); return false; }
184
+ const source = desktopSource({ sourceRoot: SOURCE_ROOT });
185
+ if (!source) { console.log(`desktop: no app for ${process.platform} ${process.arch} yet`); return false; }
186
+ try {
187
+ const { target, updated } = installDesktop({ source, target: desktopTarget({ source }) });
188
+ openDesktop({ target });
189
+ console.log(`desktop: ${updated ? "installed and opened" : "opened"}. The Harness mark is at the top right of your screen; it starts when you log in.`);
190
+ return true;
191
+ } catch (error) {
192
+ console.log(`desktop: not opened (${error.message})`);
193
+ return false;
194
+ }
195
+ }
196
+
178
197
  async function status() {
179
198
  const fallback = readJson(paths.status, null);
180
199
  let current;
@@ -522,6 +541,7 @@ function logs() {
522
541
 
523
542
  async function disconnect() {
524
543
  uninstallService();
544
+ retireDesktop({});
525
545
  try { await daemonRequest({ command: "shutdown" }, 2_000); } catch {}
526
546
  removeAgentConfigurations();
527
547
  try { fs.renameSync(paths.config, paths.config + `.disconnected-${Date.now()}`); } catch {}
@@ -530,11 +550,11 @@ async function disconnect() {
530
550
  }
531
551
 
532
552
  function help() {
533
- console.log(`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text>\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness logs [--follow]\n tryharness disconnect\n`);
553
+ console.log(`tryharness ${VERSION}\n\nCommands:\n tryharness set up everything and start the daemon\n tryharness desktop open the Harness desktop app (setup opens it too; --no-desktop skips it)\n tryharness status show daemon, bridge, agent, and index health\n tryharness doctor diagnose the complete connection\n tryharness sync [--agent] rescan local coding history in the background\n tryharness task <id> --follow\n follow a delegated agent run in this terminal\n tryharness board [--all] list open work on the machine's task board\n tryharness board post --for <agent> --objective <text> [--file <brief.md>]\n tryharness board show <id>\n tryharness board post --for codex --objective <text> --run --workspace <dir>\n the daemon runs it and reports back on the task\n tryharness board run <id> authorize a run an agent asked for\n tryharness board release <id>\n tryharness board agents when each agent last read the board\n tryharness board --watch follow the board in this terminal\n tryharness notify on|off desktop notifications for what waits on you\n tryharness board send <task> <message> send a held reply to another person's agent\n tryharness board auto <task> on|off auto-send replies on that thread\n tryharness board end <task> --outcome <text>\n tryharness board context <task> <query> search everything written about a task\n tryharness goal set --objective <text> [--team a,b] [--workspace <dir>] a goal your agents work together in one room\n tryharness goal [list | show <id> | stop <id> | resume <id>]\n tryharness room <goal> | room post <goal> <text>\n tryharness connect <handle/agent> --about <text> ask another person's agent to work an issue with yours\n tryharness mcp grok-bot print the MCP snippet that puts Grok Bot on the board\n tryharness logs [--follow]\n tryharness disconnect\n`);
534
554
  }
535
555
 
536
556
  try {
537
- if (command === "setup" || (command === "connect" && !argv[0])) await connect();
557
+ if (command === "setup" || (command === "connect" && (!argv[0] || argv[0].startsWith("-")))) await connect();
538
558
  else if (command === "status" || command === "bridge") await status();
539
559
  else if (command === "doctor") await doctor();
540
560
  else if (command === "sync" || command === "backfill" || command === "drain") await sync();
@@ -545,6 +565,7 @@ try {
545
565
  else if (command === "mcp") await mcpSnippet();
546
566
  else if (command === "notify") await notify();
547
567
  else if (command === "connect") await connectAgent();
568
+ else if (command === "desktop") { if (!desktop()) process.exitCode = 1; }
548
569
  else if (command === "logs") logs();
549
570
  else if (command === "disconnect") await disconnect();
550
571
  else if (command === "daemon") { const { startDaemon } = await import("./lib/daemon.mjs"); await startDaemon(); }
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ import { run } from "./commands.mjs";
7
+ import { ensureDir, readJson, writeJsonAtomic } from "./files.mjs";
8
+ import { paths } from "./paths.mjs";
9
+
10
+ // The desktop app ships as one optional npm package per platform (tryharness-desktop-<os>-<cpu>),
11
+ // so `npx tryharness` brings the build for this machine and nothing else. Setup copies it out of
12
+ // npm's temporary cache to a place it keeps, opens it, and the app registers itself to start at
13
+ // login from there.
14
+
15
+ const marker = () => path.join(paths.home, "desktop.json");
16
+
17
+ export function desktopPackageName(platform = process.platform, arch = process.arch) {
18
+ return `tryharness-desktop-${platform}-${arch}`;
19
+ }
20
+
21
+ /** The app inside this machine's platform package, or null when there is none for it. */
22
+ export function desktopSource({ sourceRoot, env = process.env, platform = process.platform, arch = process.arch }) {
23
+ if (env.HARNESS_DESKTOP_APP) return { app: path.resolve(env.HARNESS_DESKTOP_APP), version: env.HARNESS_DESKTOP_VERSION || "dev" };
24
+ let packageJson;
25
+ try { packageJson = createRequire(path.join(sourceRoot, "package.json")).resolve(`${desktopPackageName(platform, arch)}/package.json`); }
26
+ catch { return null; }
27
+ const meta = readJson(packageJson, {});
28
+ const app = path.join(path.dirname(packageJson), String(meta.harnessDesktop || ""));
29
+ return meta.harnessDesktop && fs.existsSync(app) ? { app, version: String(meta.version) } : null;
30
+ }
31
+
32
+ /** Where the app is kept. On a Mac that is ~/Applications, so Spotlight and Launchpad find it. */
33
+ export function desktopTarget({ source, env = process.env, platform = process.platform, homeDir = os.homedir() }) {
34
+ const file = path.basename(source.app);
35
+ if (platform === "darwin" && !env.HARNESS_HOME) return path.join(homeDir, "Applications", file);
36
+ return path.join(paths.home, "desktop", file);
37
+ }
38
+
39
+ function executables(target, platform) {
40
+ if (platform === "darwin") {
41
+ const dir = path.join(target, "Contents", "MacOS");
42
+ return fs.existsSync(dir) ? fs.readdirSync(dir).map((name) => path.join(dir, name)) : [];
43
+ }
44
+ return [target];
45
+ }
46
+
47
+ export function quitDesktop({ target, platform = process.platform, execute = run }) {
48
+ if (platform === "win32") { execute("taskkill", ["/F", "/IM", path.basename(target)]); return; }
49
+ const exes = executables(target, platform);
50
+ for (const exe of exes) execute("pkill", ["-f", exe]);
51
+ // Opening it again before the old one has gone fails on macOS (LaunchServices error -600).
52
+ for (let i = 0; i < 50 && exes.some((exe) => execute("pgrep", ["-f", exe]).ok); i++) {
53
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
54
+ }
55
+ }
56
+
57
+ /** Copies the app into place unless this version is already there. Returns where it is. */
58
+ export function installDesktop({ source, target, platform = process.platform, execute = run }) {
59
+ const installed = readJson(marker(), {});
60
+ if (installed.version === source.version && installed.path === target && fs.existsSync(target)) return { target, updated: false };
61
+ // Replacing a running app's files under it is how it crashes; it is quit first and reopened after.
62
+ if (fs.existsSync(target)) quitDesktop({ target, platform, execute });
63
+ ensureDir(path.dirname(target));
64
+ const staging = `${target}.staging-${process.pid}`;
65
+ fs.rmSync(staging, { recursive: true, force: true });
66
+ fs.cpSync(source.app, staging, { recursive: true, verbatimSymlinks: true });
67
+ // npm does not promise to keep a file's executable bit.
68
+ for (const exe of executables(staging, platform)) fs.chmodSync(exe, 0o755);
69
+ fs.rmSync(target, { recursive: true, force: true });
70
+ fs.renameSync(staging, target);
71
+ writeJsonAtomic(marker(), { version: source.version, path: target });
72
+ return { target, updated: true };
73
+ }
74
+
75
+ /** Whether this login can show a window at all: not a server reached over SSH. */
76
+ export function canShowDesktop({ env = process.env, platform = process.platform }) {
77
+ if (platform === "linux") return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
78
+ return platform === "darwin" || platform === "win32";
79
+ }
80
+
81
+ export function openDesktop({ target, platform = process.platform, execute = run }) {
82
+ if (platform === "darwin") {
83
+ // Just after the old copy quits, LaunchServices can still refuse (error -600) for a moment.
84
+ let opened = execute("open", [target]);
85
+ for (let i = 0; i < 20 && !opened.ok && /-600/.test(opened.stderr); i++) {
86
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
87
+ opened = execute("open", [target]);
88
+ }
89
+ if (!opened.ok) throw new Error(opened.stderr.trim() || opened.error || "open failed");
90
+ return;
91
+ }
92
+ const child = spawn(target, [], { detached: true, stdio: "ignore" });
93
+ child.on("error", () => {});
94
+ child.unref();
95
+ }
96
+
97
+ /** Disconnecting stops the app and takes it out of login items; the app itself stays. */
98
+ export function retireDesktop({ platform = process.platform, execute = run, homeDir = os.homedir() }) {
99
+ const installed = readJson(marker(), {});
100
+ if (installed.path) quitDesktop({ target: installed.path, platform, execute });
101
+ // Where the app's own login item lives (tauri-plugin-autostart, named after the product).
102
+ if (platform === "darwin") fs.rmSync(path.join(homeDir, "Library", "LaunchAgents", "Harness.plist"), { force: true });
103
+ else if (platform === "linux") fs.rmSync(path.join(homeDir, ".config", "autostart", "Harness.desktop"), { force: true });
104
+ else if (platform === "win32") execute("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "Harness", "/f"]);
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tryharness",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Harness Relay: the daemon that turns the agents on this machine into your personal swarm. Memory, history, and work handed between any of them.",
5
5
  "bin": {
6
6
  "tryharness": "cli.mjs"
@@ -33,5 +33,11 @@
33
33
  "ws": "^8.18.3",
34
34
  "@huggingface/tokenizers": "^0.1.3",
35
35
  "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c"
36
+ },
37
+ "optionalDependencies": {
38
+ "tryharness-desktop-darwin-arm64": "0.7.3",
39
+ "tryharness-desktop-darwin-x64": "0.7.3",
40
+ "tryharness-desktop-linux-x64": "0.7.3",
41
+ "tryharness-desktop-win32-x64": "0.7.3"
36
42
  }
37
43
  }