tryharness 0.7.1 → 0.7.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/cli.mjs +23 -2
- package/lib/desktop.mjs +100 -0
- 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(); }
|
package/lib/desktop.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
const opened = execute("open", [target]);
|
|
84
|
+
if (!opened.ok) throw new Error(opened.stderr.trim() || opened.error || "open failed");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const child = spawn(target, [], { detached: true, stdio: "ignore" });
|
|
88
|
+
child.on("error", () => {});
|
|
89
|
+
child.unref();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Disconnecting stops the app and takes it out of login items; the app itself stays. */
|
|
93
|
+
export function retireDesktop({ platform = process.platform, execute = run, homeDir = os.homedir() }) {
|
|
94
|
+
const installed = readJson(marker(), {});
|
|
95
|
+
if (installed.path) quitDesktop({ target: installed.path, platform, execute });
|
|
96
|
+
// Where the app's own login item lives (tauri-plugin-autostart, named after the product).
|
|
97
|
+
if (platform === "darwin") fs.rmSync(path.join(homeDir, "Library", "LaunchAgents", "Harness.plist"), { force: true });
|
|
98
|
+
else if (platform === "linux") fs.rmSync(path.join(homeDir, ".config", "autostart", "Harness.desktop"), { force: true });
|
|
99
|
+
else if (platform === "win32") execute("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "Harness", "/f"]);
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryharness",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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.2",
|
|
39
|
+
"tryharness-desktop-darwin-x64": "0.7.2",
|
|
40
|
+
"tryharness-desktop-linux-x64": "0.7.2",
|
|
41
|
+
"tryharness-desktop-win32-x64": "0.7.2"
|
|
36
42
|
}
|
|
37
43
|
}
|