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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5248 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +382 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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
|
|
16
|
+
for (const name of WINDOWS
|
|
17
|
+
? [`${command}.exe`, `${command}.cmd`, command]
|
|
18
|
+
: [command]) {
|
|
17
19
|
const file = path.join(dir, name);
|
|
18
|
-
try {
|
|
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) => {
|
|
25
|
-
|
|
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 {
|
|
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 {
|
|
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(
|
|
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)
|
|
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) => {
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 === "^")
|
|
78
|
-
|
|
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)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
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) =>
|
|
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
|
|
111
|
-
|
|
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(
|
|
114
|
-
|
|
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 {
|
|
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 {
|
|
129
|
-
|
|
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(
|
|
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
|
|
134
|
-
.
|
|
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))
|
|
148
|
-
|
|
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 {
|
|
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 {
|
|
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
|
}
|