tryharness 0.7.9 → 0.7.13
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/assets/opencode-plugin.js +9 -2
- package/assets/pi-extension.ts +8 -3
- package/cli.mjs +67 -42
- package/lib/adapters.mjs +12 -0
- package/lib/cards.mjs +113 -0
- package/lib/coordinator.mjs +5 -6
- package/lib/daemon.mjs +172 -20
- package/lib/files.mjs +15 -0
- package/lib/mcp-stdio.mjs +36 -5
- package/lib/members.json +26 -13
- package/lib/nudge.mjs +29 -13
- package/lib/onboard.mjs +132 -82
- package/lib/paths.mjs +1 -0
- package/lib/phones.mjs +1 -1
- package/lib/runtime-install.mjs +1 -1
- package/lib/sealed.mjs +9 -0
- package/lib/store.mjs +65 -3
- package/lib/swarm.mjs +12 -7
- package/lib/ui.mjs +111 -0
- package/package.json +9 -6
package/lib/ui.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { TextPrompt } from "@clack/core";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
// Setup's terminal interface, in the style of @clack/prompts (the skills installer's): a rail of steps, arrow-key
|
|
6
|
+
// menus, spinners while waiting. createUi draws it; scriptedUi answers from a list and keeps what a person would have
|
|
7
|
+
// seen, for tests. Prompts return null when the person cancels (Ctrl-C), so setup can skip that part and go on.
|
|
8
|
+
|
|
9
|
+
const unicode = process.platform !== "win32" || Boolean(process.env.WT_SESSION) || process.env.TERM_PROGRAM === "vscode";
|
|
10
|
+
const glyph = (fancy, plain) => (unicode ? fancy : plain);
|
|
11
|
+
const BAR = glyph("│", "|"), END = glyph("└", "—"), ACTIVE = glyph("◆", "*"), DONE = glyph("◇", "o"), STOP = glyph("■", "x"), WARN = glyph("▲", "!");
|
|
12
|
+
|
|
13
|
+
const visible = (text) => text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
14
|
+
|
|
15
|
+
/** Lines of text wrapped to a width, each keeping its own indent. */
|
|
16
|
+
export function wrap(text, width) {
|
|
17
|
+
return text.split("\n").flatMap((line) => {
|
|
18
|
+
if (!line.trim()) return [""];
|
|
19
|
+
const indent = " ".repeat(line.length - line.trimStart().length), room = Math.max(20, width - indent.length), rows = [];
|
|
20
|
+
let row = "";
|
|
21
|
+
for (const word of line.trim().split(" ")) {
|
|
22
|
+
if (row && visible(row) + 1 + visible(word) > room) { rows.push(row); row = word; } else row = row ? `${row} ${word}` : word;
|
|
23
|
+
}
|
|
24
|
+
return [...rows, row].map((part) => `${indent}${part}`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const columns = () => process.stdout.columns || 100;
|
|
29
|
+
|
|
30
|
+
// A text prompt Harness can close: shown while something else may finish first (Grok registering itself). `until`
|
|
31
|
+
// resolves to { ok, line }; the prompt then closes with that line. Returns { value } when the person types and presses
|
|
32
|
+
// Enter, { done } when `until` won, or { cancelled: true }.
|
|
33
|
+
async function waitOrType({ message, placeholder, until }) {
|
|
34
|
+
const stop = new AbortController();
|
|
35
|
+
let done = null;
|
|
36
|
+
void until.then((result) => { done = result; stop.abort(); });
|
|
37
|
+
const value = await new TextPrompt({
|
|
38
|
+
placeholder,
|
|
39
|
+
signal: stop.signal,
|
|
40
|
+
render() {
|
|
41
|
+
const head = (symbol) => `${pc.gray(BAR)}\n${symbol} ${message}\n`;
|
|
42
|
+
if (this.state === "submit") return `${head(pc.green(DONE))}${pc.gray(BAR)} ${pc.dim(this.value || "keep waiting")}`;
|
|
43
|
+
if (this.state === "cancel" && done) return `${pc.gray(BAR)}\n${done.ok ? pc.green(DONE) : pc.yellow(WARN)} ${done.line}`;
|
|
44
|
+
if (this.state === "cancel") return `${head(pc.red(STOP))}${pc.gray(BAR)} ${pc.dim("stopped")}`;
|
|
45
|
+
const shown = this.value ? this.valueWithCursor : `${pc.inverse(placeholder[0])}${pc.dim(placeholder.slice(1))}`;
|
|
46
|
+
return `${head(pc.cyan(ACTIVE))}${pc.cyan(BAR)} ${shown}\n${pc.cyan(END)}\n`;
|
|
47
|
+
},
|
|
48
|
+
}).prompt();
|
|
49
|
+
if (done) return { done };
|
|
50
|
+
if (p.isCancel(value)) return { cancelled: true };
|
|
51
|
+
return { value: String(value ?? "").trim() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createUi({ interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) } = {}) {
|
|
55
|
+
const answer = (value) => (p.isCancel(value) ? null : value);
|
|
56
|
+
// clack does not wrap; a long line would run back to the left edge, off the rail.
|
|
57
|
+
const fit = (text) => wrap(text, columns() - 4).join("\n");
|
|
58
|
+
return {
|
|
59
|
+
interactive,
|
|
60
|
+
intro: (title) => p.intro(title),
|
|
61
|
+
outro: (text) => p.outro(text),
|
|
62
|
+
step: (text) => p.log.step(fit(text)),
|
|
63
|
+
success: (text) => p.log.success(fit(text)),
|
|
64
|
+
info: (text) => p.log.info(fit(text)),
|
|
65
|
+
warn: (text) => p.log.warn(fit(text)),
|
|
66
|
+
message: (text) => p.log.message(fit(text)),
|
|
67
|
+
note: (text, title) => p.note(text, title),
|
|
68
|
+
select: async (opts) => answer(await p.select(opts)),
|
|
69
|
+
text: async (opts) => answer(await p.text(opts)),
|
|
70
|
+
password: async (opts) => answer(await p.password(opts)),
|
|
71
|
+
confirm: async (opts) => answer(await p.confirm(opts)),
|
|
72
|
+
// clack's spinner stops on Ctrl-C but keeps the process alive; setup ends there instead. Without a terminal (piped
|
|
73
|
+
// or logged) it is two plain lines, not animation frames.
|
|
74
|
+
spinner: () => {
|
|
75
|
+
if (!process.stdout.isTTY) return { start: (text) => p.log.message(fit(text)), message: () => {}, stop: (text, code) => (code ? p.log.warn : p.log.step)(fit(text)) };
|
|
76
|
+
const spin = p.spinner(), quit = () => process.exit(130);
|
|
77
|
+
return {
|
|
78
|
+
start: (text) => { spin.start(text); process.once("SIGINT", quit); },
|
|
79
|
+
message: (text) => spin.message(text),
|
|
80
|
+
stop: (text, code) => { process.removeListener("SIGINT", quit); spin.stop(text, code); },
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
waitOrType,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The same interface answered from `answers` in order (undefined takes the default); `lines` is what was shown. */
|
|
88
|
+
export function scriptedUi(answers = [], lines = []) {
|
|
89
|
+
const say = (symbol) => (text) => { lines.push(...`${symbol} ${text}`.split("\n")); };
|
|
90
|
+
const next = (fallback) => { const value = answers.shift(); return value === undefined ? fallback : value; };
|
|
91
|
+
const ask = async (opts) => { lines.push(`? ${opts.message}`); const value = next(null); if (value !== null) lines.push(` ${value}`); return value; };
|
|
92
|
+
return {
|
|
93
|
+
interactive: true,
|
|
94
|
+
lines,
|
|
95
|
+
intro: say("┌"), outro: say("└"), step: say("◇"), success: say("◆"), info: say("●"), warn: say("▲"), message: say("│"),
|
|
96
|
+
note: (text, title) => { lines.push(`[${title}]`, ...text.split("\n")); },
|
|
97
|
+
select: async (opts) => { lines.push(`? ${opts.message}`, ...opts.options.map((option) => ` ${option.label}${option.hint ? ` (${option.hint})` : ""}`)); return next(opts.initialValue ?? opts.options[0].value); },
|
|
98
|
+
text: ask,
|
|
99
|
+
password: async (opts) => { lines.push(`? ${opts.message}`); return next(null); },
|
|
100
|
+
confirm: async (opts) => { lines.push(`? ${opts.message}`); return next(opts.initialValue ?? true); },
|
|
101
|
+
spinner: () => ({ start: say("◒"), message: () => {}, stop: (text, code = 0) => say(code ? "▲" : "◇")(text) }),
|
|
102
|
+
waitOrType: async ({ message, until }) => {
|
|
103
|
+
lines.push(`? ${message}`);
|
|
104
|
+
const value = next("");
|
|
105
|
+
if (value) return { value };
|
|
106
|
+
const done = await until;
|
|
107
|
+
lines.push(`${done.ok ? "◇" : "▲"} ${done.line}`);
|
|
108
|
+
return { done };
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryharness",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
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"
|
|
@@ -29,15 +29,18 @@
|
|
|
29
29
|
"homepage": "https://tryharness.ai",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@clack/core": "0.5.0",
|
|
33
|
+
"@clack/prompts": "0.11.0",
|
|
34
|
+
"@huggingface/tokenizers": "^0.1.3",
|
|
32
35
|
"node-sqlite3-wasm": "0.8.60",
|
|
36
|
+
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
|
|
37
|
+
"picocolors": "^1.1.1",
|
|
33
38
|
"qr": "0.4.2",
|
|
34
|
-
"ws": "^8.18.3"
|
|
35
|
-
"@huggingface/tokenizers": "^0.1.3",
|
|
36
|
-
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c"
|
|
39
|
+
"ws": "^8.18.3"
|
|
37
40
|
},
|
|
38
41
|
"optionalDependencies": {
|
|
39
|
-
"tryharness-desktop-darwin-arm64": "0.7.
|
|
40
|
-
"tryharness-desktop-darwin-x64": "0.7.
|
|
42
|
+
"tryharness-desktop-darwin-arm64": "0.7.10",
|
|
43
|
+
"tryharness-desktop-darwin-x64": "0.7.10",
|
|
41
44
|
"tryharness-desktop-linux-x64": "0.7.4",
|
|
42
45
|
"tryharness-desktop-win32-x64": "0.7.4"
|
|
43
46
|
}
|