tryharness 0.7.23 → 0.7.25

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5248 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +382 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/agentnode.mjs CHANGED
@@ -13,21 +13,48 @@ const WINDOWS = process.platform === "win32";
13
13
  /** The executable a command name runs on this PATH. */
14
14
  export function onPath(command, envPath = process.env.PATH || "") {
15
15
  for (const dir of String(envPath).split(path.delimiter).filter(Boolean)) {
16
- for (const name of WINDOWS ? [`${command}.exe`, `${command}.cmd`, command] : [command]) {
16
+ for (const name of WINDOWS
17
+ ? [`${command}.exe`, `${command}.cmd`, command]
18
+ : [command]) {
17
19
  const file = path.join(dir, name);
18
- try { if (fs.statSync(file).isFile()) { fs.accessSync(file, fs.constants.X_OK); return file; } } catch {}
20
+ try {
21
+ if (fs.statSync(file).isFile()) {
22
+ fs.accessSync(file, fs.constants.X_OK);
23
+ return file;
24
+ }
25
+ } catch {}
19
26
  }
20
27
  }
21
28
  return null;
22
29
  }
23
30
 
24
- const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
25
- const real = (file) => { try { return fs.realpathSync(file); } catch { return file; } };
31
+ const readJson = (file) => {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(file, "utf8"));
34
+ } catch {
35
+ return null;
36
+ }
37
+ };
38
+ const real = (file) => {
39
+ try {
40
+ return fs.realpathSync(file);
41
+ } catch {
42
+ return file;
43
+ }
44
+ };
26
45
 
27
46
  /** Whether a file is a script Node runs (`#!/usr/bin/env node`), not a native program. */
28
47
  function nodeScript(file) {
29
48
  let head = "";
30
- try { const fd = fs.openSync(file, "r"); const buffer = Buffer.alloc(200); fs.readSync(fd, buffer, 0, 200, 0); fs.closeSync(fd); head = buffer.toString("utf8"); } catch { return false; }
49
+ try {
50
+ const fd = fs.openSync(file, "r");
51
+ const buffer = Buffer.alloc(200);
52
+ fs.readSync(fd, buffer, 0, 200, 0);
53
+ fs.closeSync(fd);
54
+ head = buffer.toString("utf8");
55
+ } catch {
56
+ return false;
57
+ }
31
58
  return head.startsWith("#!") && /\bnode\b/.test(head.split("\n")[0]);
32
59
  }
33
60
 
@@ -35,9 +62,23 @@ function nodeScript(file) {
35
62
  function managedEntry(launcher, command) {
36
63
  const dir = path.dirname(path.dirname(launcher));
37
64
  let version = "";
38
- try { version = fs.readFileSync(path.join(dir, "install", "current-version"), "utf8").trim(); } catch { return null; }
65
+ try {
66
+ version = fs
67
+ .readFileSync(path.join(dir, "install", "current-version"), "utf8")
68
+ .trim();
69
+ } catch {
70
+ return null;
71
+ }
39
72
  if (!/^[\w.+-]+$/.test(version)) return null;
40
- const entry = path.join(dir, "install", "releases", version, "node_modules", ".bin", command);
73
+ const entry = path.join(
74
+ dir,
75
+ "install",
76
+ "releases",
77
+ version,
78
+ "node_modules",
79
+ ".bin",
80
+ command
81
+ );
41
82
  return fs.existsSync(entry) ? real(entry) : null;
42
83
  }
43
84
 
@@ -49,33 +90,66 @@ export function nodeRange(command, envPath = process.env.PATH || "") {
49
90
  if (!nodeScript(entry)) return null;
50
91
  for (let dir = path.dirname(entry); ; dir = path.dirname(dir)) {
51
92
  const pkg = readJson(path.join(dir, "package.json"));
52
- if (pkg?.name) return typeof pkg.engines?.node === "string" && pkg.engines.node.trim() ? pkg.engines.node.trim() : null;
93
+ if (pkg?.name)
94
+ return typeof pkg.engines?.node === "string" && pkg.engines.node.trim()
95
+ ? pkg.engines.node.trim()
96
+ : null;
53
97
  if (path.dirname(dir) === dir) return null;
54
98
  }
55
99
  }
56
100
 
57
101
  // ── Versions and ranges (the forms package.json engines use) ─────────────────
58
102
 
59
- const parse = (text) => { const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(text).trim()); return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null; };
103
+ const parse = (text) => {
104
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(text).trim());
105
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
106
+ };
60
107
  const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
61
108
 
62
109
  /** One comparator (">=22.19.0", "^20", "~18.2", "22.x", "<23"); one it does not understand holds nothing back. */
63
110
  function holds(version, comparator) {
64
111
  if (["", "*", "x", "X"].includes(comparator)) return true;
65
- const m = /^(>=|<=|>|<|=|\^|~)?v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/.exec(comparator);
112
+ const m =
113
+ /^(>=|<=|>|<|=|\^|~)?v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/.exec(
114
+ comparator
115
+ );
66
116
  if (!m) return true;
67
117
  const [, op = "", major, minor, patch] = m;
68
- const part = (value) => (value === undefined || /^[xX*]$/.test(value) ? null : Number(value));
118
+ const part = (value) =>
119
+ value === undefined || /^[xX*]$/.test(value) ? null : Number(value);
69
120
  const low = [Number(major), part(minor) ?? 0, part(patch) ?? 0];
70
121
  // The first version past a partial one: "22" ends before 23.0.0, "22.4" before 22.5.0.
71
- const past = part(minor) === null ? [low[0] + 1, 0, 0] : part(patch) === null ? [low[0], low[1] + 1, 0] : [low[0], low[1], low[2] + 1];
122
+ const past =
123
+ part(minor) === null
124
+ ? [low[0] + 1, 0, 0]
125
+ : part(patch) === null
126
+ ? [low[0], low[1] + 1, 0]
127
+ : [low[0], low[1], low[2] + 1];
72
128
  const at = compare(version, low);
73
129
  if (op === ">=") return at >= 0;
74
130
  if (op === ">") return compare(version, past) >= 0;
75
131
  if (op === "<") return at < 0;
76
132
  if (op === "<=") return compare(version, past) < 0;
77
- if (op === "^") return at >= 0 && compare(version, low[0] > 0 ? [low[0] + 1, 0, 0] : low[1] > 0 ? [0, low[1] + 1, 0] : [0, 0, low[2] + 1]) < 0;
78
- if (op === "~") return at >= 0 && compare(version, part(minor) === null ? [low[0] + 1, 0, 0] : [low[0], low[1] + 1, 0]) < 0;
133
+ if (op === "^")
134
+ return (
135
+ at >= 0 &&
136
+ compare(
137
+ version,
138
+ low[0] > 0
139
+ ? [low[0] + 1, 0, 0]
140
+ : low[1] > 0
141
+ ? [0, low[1] + 1, 0]
142
+ : [0, 0, low[2] + 1]
143
+ ) < 0
144
+ );
145
+ if (op === "~")
146
+ return (
147
+ at >= 0 &&
148
+ compare(
149
+ version,
150
+ part(minor) === null ? [low[0] + 1, 0, 0] : [low[0], low[1] + 1, 0]
151
+ ) < 0
152
+ );
79
153
  return at >= 0 && compare(version, past) < 0;
80
154
  }
81
155
 
@@ -83,11 +157,15 @@ function holds(version, comparator) {
83
157
  export function satisfies(version, range) {
84
158
  const parsed = parse(version);
85
159
  if (!parsed) return false;
86
- return String(range).split("||").some((alternative) => {
87
- const hyphen = /^\s*(\S+)\s+-\s+(\S+)\s*$/.exec(alternative);
88
- const comparators = hyphen ? [`>=${hyphen[1]}`, `<=${hyphen[2]}`] : alternative.trim().split(/\s+/).filter(Boolean);
89
- return comparators.every((comparator) => holds(parsed, comparator));
90
- });
160
+ return String(range)
161
+ .split("||")
162
+ .some((alternative) => {
163
+ const hyphen = /^\s*(\S+)\s+-\s+(\S+)\s*$/.exec(alternative);
164
+ const comparators = hyphen
165
+ ? [`>=${hyphen[1]}`, `<=${hyphen[2]}`]
166
+ : alternative.trim().split(/\s+/).filter(Boolean);
167
+ return comparators.every((comparator) => holds(parsed, comparator));
168
+ });
91
169
  }
92
170
 
93
171
  // ── The Nodes on this computer ───────────────────────────────────────────────
@@ -97,7 +175,10 @@ const versions = new Map();
97
175
  function versionOf(node) {
98
176
  if (!versions.has(node)) {
99
177
  const said = spawnSync(node, ["-v"], { encoding: "utf8", timeout: 5_000 });
100
- versions.set(node, said.status === 0 && parse(said.stdout) ? said.stdout.trim() : null);
178
+ versions.set(
179
+ node,
180
+ said.status === 0 && parse(said.stdout) ? said.stdout.trim() : null
181
+ );
101
182
  }
102
183
  return versions.get(node);
103
184
  }
@@ -105,13 +186,33 @@ function versionOf(node) {
105
186
  /** Every Node installed where people put them: version managers, Homebrew, the system's, the one Harness runs on. */
106
187
  export function installedNodes(envPath = process.env.PATH || "") {
107
188
  const home = os.homedir();
108
- const bin = (dir) => (WINDOWS ? [path.join(dir, "node.exe")] : [path.join(dir, "bin", "node"), path.join(dir, "installation", "bin", "node")]);
189
+ const bin = (dir) =>
190
+ WINDOWS
191
+ ? [path.join(dir, "node.exe")]
192
+ : [
193
+ path.join(dir, "bin", "node"),
194
+ path.join(dir, "installation", "bin", "node"),
195
+ ];
109
196
  const managers = [
110
- process.env.NVM_DIR ? path.join(process.env.NVM_DIR, "versions", "node") : path.join(home, ".nvm", "versions", "node"),
111
- path.join(process.env.FNM_DIR || path.join(home, ".local", "share", "fnm"), "node-versions"),
197
+ process.env.NVM_DIR
198
+ ? path.join(process.env.NVM_DIR, "versions", "node")
199
+ : path.join(home, ".nvm", "versions", "node"),
200
+ path.join(
201
+ process.env.FNM_DIR || path.join(home, ".local", "share", "fnm"),
202
+ "node-versions"
203
+ ),
112
204
  path.join(home, "Library", "Application Support", "fnm", "node-versions"),
113
- path.join(process.env.VOLTA_HOME || path.join(home, ".volta"), "tools", "image", "node"),
114
- path.join(process.env.ASDF_DATA_DIR || path.join(home, ".asdf"), "installs", "nodejs"),
205
+ path.join(
206
+ process.env.VOLTA_HOME || path.join(home, ".volta"),
207
+ "tools",
208
+ "image",
209
+ "node"
210
+ ),
211
+ path.join(
212
+ process.env.ASDF_DATA_DIR || path.join(home, ".asdf"),
213
+ "installs",
214
+ "nodejs"
215
+ ),
115
216
  path.join(home, ".local", "share", "mise", "installs", "node"),
116
217
  path.join(process.env.N_PREFIX || "/usr/local", "n", "versions", "node"),
117
218
  ...(process.env.NVM_HOME ? [process.env.NVM_HOME] : []),
@@ -120,18 +221,45 @@ export function installedNodes(envPath = process.env.PATH || "") {
120
221
  const found = [process.execPath, onPath("node", envPath)];
121
222
  for (const dir of managers) {
122
223
  let entries = [];
123
- try { entries = fs.readdirSync(dir); } catch { continue; }
224
+ try {
225
+ entries = fs.readdirSync(dir);
226
+ } catch {
227
+ continue;
228
+ }
124
229
  for (const entry of entries) found.push(...bin(path.join(dir, entry)));
125
230
  }
126
231
  for (const brew of ["/opt/homebrew/opt", "/usr/local/opt"]) {
127
232
  let entries = [];
128
- try { entries = fs.readdirSync(brew).filter((name) => /^node(@\d+)?$/.test(name)); } catch {}
129
- for (const entry of entries) found.push(path.join(brew, entry, "bin", "node"));
233
+ try {
234
+ entries = fs
235
+ .readdirSync(brew)
236
+ .filter((name) => /^node(@\d+)?$/.test(name));
237
+ } catch {}
238
+ for (const entry of entries)
239
+ found.push(path.join(brew, entry, "bin", "node"));
130
240
  }
131
- found.push(...(WINDOWS ? [path.join(process.env.ProgramFiles || "C:\\Program Files", "nodejs", "node.exe")] : ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"]));
241
+ found.push(
242
+ ...(WINDOWS
243
+ ? [
244
+ path.join(
245
+ process.env.ProgramFiles || "C:\\Program Files",
246
+ "nodejs",
247
+ "node.exe"
248
+ ),
249
+ ]
250
+ : ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"])
251
+ );
132
252
  const seen = new Set();
133
- return found.filter((node) => node && fs.existsSync(node) && !seen.has(real(node)) && seen.add(real(node)))
134
- .map((node) => ({ node, version: versionOf(node) })).filter((entry) => entry.version);
253
+ return found
254
+ .filter(
255
+ (node) =>
256
+ node &&
257
+ fs.existsSync(node) &&
258
+ !seen.has(real(node)) &&
259
+ seen.add(real(node))
260
+ )
261
+ .map((node) => ({ node, version: versionOf(node) }))
262
+ .filter((entry) => entry.version);
135
263
  }
136
264
 
137
265
  /**
@@ -144,11 +272,23 @@ export function agentEnvironment(command, env = process.env) {
144
272
  if (!range) return { env };
145
273
  const current = onPath("node", env.PATH);
146
274
  const have = current ? versionOf(current) : null;
147
- if (have && satisfies(have, range)) return { env, bin: path.dirname(current) };
148
- const fits = installedNodes(env.PATH).filter((entry) => satisfies(entry.version, range))
275
+ if (have && satisfies(have, range))
276
+ return { env, bin: path.dirname(current) };
277
+ const fits = installedNodes(env.PATH)
278
+ .filter((entry) => satisfies(entry.version, range))
149
279
  .sort((a, b) => compare(parse(b.version), parse(a.version)))[0];
150
280
  if (!fits) {
151
- return { env, error: `${command} needs Node ${range}, and this computer has ${have ? `Node ${have.replace(/^v/, "")}` : "no Node"} and no other in that range. Install one (for example: nvm install 22), then ask again.` };
281
+ return {
282
+ env,
283
+ error: `${command} needs Node ${range}, and this computer has ${have ? `Node ${have.replace(/^v/, "")}` : "no Node"} and no other in that range. Install one (for example: nvm install 22), then ask again.`,
284
+ };
152
285
  }
153
- return { env: { ...env, PATH: [path.dirname(fits.node), env.PATH || ""].join(path.delimiter) }, bin: path.dirname(fits.node), node: fits.version };
286
+ return {
287
+ env: {
288
+ ...env,
289
+ PATH: [path.dirname(fits.node), env.PATH || ""].join(path.delimiter),
290
+ },
291
+ bin: path.dirname(fits.node),
292
+ node: fits.version,
293
+ };
154
294
  }