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/media.mjs CHANGED
@@ -28,13 +28,32 @@ export function mediaFiles(folder, { max = MAX_FILES } = {}) {
28
28
  const walk = (dir, depth) => {
29
29
  if (depth > MAX_DEPTH || out.length >= max) return;
30
30
  let entries = [];
31
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
31
+ try {
32
+ entries = fs.readdirSync(dir, { withFileTypes: true });
33
+ } catch {
34
+ return;
35
+ }
32
36
  for (const entry of entries) {
33
37
  if (out.length >= max) return;
34
38
  const full = path.join(dir, entry.name);
35
- if (entry.isDirectory()) { if (!SKIP_DIR.test(entry.name)) walk(full, depth + 1); continue; }
36
- if (!entry.isFile() || !(AUDIO.test(entry.name) || IMAGE.test(entry.name))) continue;
37
- try { const stat = fs.statSync(full); out.push({ path: full, kind: AUDIO.test(entry.name) ? "audio" : "image", mtime: stat.mtimeMs, size: stat.size }); } catch {}
39
+ if (entry.isDirectory()) {
40
+ if (!SKIP_DIR.test(entry.name)) walk(full, depth + 1);
41
+ continue;
42
+ }
43
+ if (
44
+ !entry.isFile() ||
45
+ !(AUDIO.test(entry.name) || IMAGE.test(entry.name))
46
+ )
47
+ continue;
48
+ try {
49
+ const stat = fs.statSync(full);
50
+ out.push({
51
+ path: full,
52
+ kind: AUDIO.test(entry.name) ? "audio" : "image",
53
+ mtime: stat.mtimeMs,
54
+ size: stat.size,
55
+ });
56
+ } catch {}
38
57
  }
39
58
  };
40
59
  walk(folder, 0);
@@ -47,30 +66,79 @@ export function readRecording(file, { run = spawnSync } = {}) {
47
66
  const { sampleRate, samples } = decodeWav(fs.readFileSync(file));
48
67
  return resample(samples, sampleRate, RATE);
49
68
  }
50
- const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "harness-media-")), "audio.wav");
69
+ const out = path.join(
70
+ fs.mkdtempSync(path.join(os.tmpdir(), "harness-media-")),
71
+ "audio.wav"
72
+ );
51
73
  try {
52
- const converted = process.platform === "darwin" ? run("afconvert", ["-f", "WAVE", "-d", `LEI16@${RATE}`, "-c", "1", file, out], { encoding: "utf8" })
53
- : run("ffmpeg", ["-v", "error", "-y", "-i", file, "-ac", "1", "-ar", String(RATE), out], { encoding: "utf8" });
54
- if (converted.status !== 0 || !fs.existsSync(out)) throw new Error(converted.error?.code === "ENOENT" ? "no audio converter on this computer (ffmpeg)" : `could not read ${path.basename(file)}`);
74
+ const converted =
75
+ process.platform === "darwin"
76
+ ? run(
77
+ "afconvert",
78
+ ["-f", "WAVE", "-d", `LEI16@${RATE}`, "-c", "1", file, out],
79
+ { encoding: "utf8" }
80
+ )
81
+ : run(
82
+ "ffmpeg",
83
+ [
84
+ "-v",
85
+ "error",
86
+ "-y",
87
+ "-i",
88
+ file,
89
+ "-ac",
90
+ "1",
91
+ "-ar",
92
+ String(RATE),
93
+ out,
94
+ ],
95
+ { encoding: "utf8" }
96
+ );
97
+ if (converted.status !== 0 || !fs.existsSync(out))
98
+ throw new Error(
99
+ converted.error?.code === "ENOENT"
100
+ ? "no audio converter on this computer (ffmpeg)"
101
+ : `could not read ${path.basename(file)}`
102
+ );
55
103
  const { sampleRate, samples } = decodeWav(fs.readFileSync(out));
56
104
  return resample(samples, sampleRate, RATE);
57
- } finally { fs.rmSync(path.dirname(out), { recursive: true, force: true }); }
105
+ } finally {
106
+ fs.rmSync(path.dirname(out), { recursive: true, force: true });
107
+ }
58
108
  }
59
109
 
60
110
  /** A recording's windows: { startMs, endMs, samples }, 30 seconds each, a new one every 20. */
61
- export function windows(samples, { windowMs = WINDOW_MS, stepMs = STEP_MS, rate = RATE } = {}) {
62
- const size = Math.round((windowMs / 1000) * rate), step = Math.round((stepMs / 1000) * rate);
111
+ export function windows(
112
+ samples,
113
+ { windowMs = WINDOW_MS, stepMs = STEP_MS, rate = RATE } = {}
114
+ ) {
115
+ const size = Math.round((windowMs / 1000) * rate),
116
+ step = Math.round((stepMs / 1000) * rate);
63
117
  const out = [];
64
118
  for (let at = 0; at < samples.length; at += step) {
65
- out.push({ startMs: Math.round((at / rate) * 1000), endMs: Math.round((Math.min(samples.length, at + size) / rate) * 1000), samples: samples.subarray(at, at + size) });
119
+ out.push({
120
+ startMs: Math.round((at / rate) * 1000),
121
+ endMs: Math.round((Math.min(samples.length, at + size) / rate) * 1000),
122
+ samples: samples.subarray(at, at + size),
123
+ });
66
124
  if (at + size >= samples.length) break;
67
125
  }
68
126
  return out;
69
127
  }
70
128
 
71
- export const clock = (ms) => { const s = Math.floor(ms / 1000); const h = Math.floor(s / 3600); return `${h ? `${h}:` : ""}${String(Math.floor(s / 60) % 60).padStart(h ? 2 : 1, "0")}:${String(s % 60).padStart(2, "0")}`; };
129
+ export const clock = (ms) => {
130
+ const s = Math.floor(ms / 1000);
131
+ const h = Math.floor(s / 3600);
132
+ return `${h ? `${h}:` : ""}${String(Math.floor(s / 60) % 60).padStart(h ? 2 : 1, "0")}:${String(s % 60).padStart(2, "0")}`;
133
+ };
72
134
 
73
- export function createMediaIndex({ atlas, inference, folders = () => [], log = () => {}, version = ATLAS_VERSION }) {
135
+ export function createMediaIndex({
136
+ atlas,
137
+ inference,
138
+ folders = () => [],
139
+ log = () => {},
140
+ version = ATLAS_VERSION,
141
+ }) {
74
142
  let cache = null;
75
143
  const vectors = () => (cache ??= atlas.mediaVectors(version));
76
144
 
@@ -78,24 +146,51 @@ export function createMediaIndex({ atlas, inference, folders = () => [], log = (
78
146
  const refresh = async ({ lane = "background" } = {}) => {
79
147
  const files = folders().flatMap((folder) => mediaFiles(folder));
80
148
  const present = new Set(files.map((file) => file.path));
81
- for (const gone of atlas.mediaPaths()) if (!present.has(gone)) atlas.dropMedia(gone);
149
+ for (const gone of atlas.mediaPaths())
150
+ if (!present.has(gone)) atlas.dropMedia(gone);
82
151
  let added = 0;
83
152
  for (const file of files) {
84
153
  if (atlas.mediaSeen(file.path) === file.mtime) continue;
85
154
  try {
86
- const parts = file.kind === "image"
87
- ? (file.size <= MAX_IMAGE_BYTES ? [{ startMs: 0, endMs: 0, data: fs.readFileSync(file.path) }] : [])
88
- : windows(readRecording(file.path)).map((window) => ({ startMs: window.startMs, endMs: window.endMs, data: encodeWav(window.samples, RATE) }));
89
- const embedded = await inference.embedMedia(parts.map((part) => ({ title: path.basename(file.path), data: part.data })), { lane });
155
+ const parts =
156
+ file.kind === "image"
157
+ ? file.size <= MAX_IMAGE_BYTES
158
+ ? [{ startMs: 0, endMs: 0, data: fs.readFileSync(file.path) }]
159
+ : []
160
+ : windows(readRecording(file.path)).map((window) => ({
161
+ startMs: window.startMs,
162
+ endMs: window.endMs,
163
+ data: encodeWav(window.samples, RATE),
164
+ }));
165
+ const embedded = await inference.embedMedia(
166
+ parts.map((part) => ({
167
+ title: path.basename(file.path),
168
+ data: part.data,
169
+ })),
170
+ { lane }
171
+ );
90
172
  atlas.dropMedia(file.path);
91
- atlas.batch(() => parts.forEach((part, i) => {
92
- const hash = createHash("sha1").update(`${file.path}\0${file.mtime}\0${part.startMs}`).digest("hex");
93
- atlas.putMedia({ hash, path: file.path, kind: file.kind, startMs: part.startMs, endMs: part.endMs, mtime: file.mtime });
94
- atlas.putVector(hash, version, embedded[i]);
95
- }));
173
+ atlas.batch(() =>
174
+ parts.forEach((part, i) => {
175
+ const hash = createHash("sha1")
176
+ .update(`${file.path}\0${file.mtime}\0${part.startMs}`)
177
+ .digest("hex");
178
+ atlas.putMedia({
179
+ hash,
180
+ path: file.path,
181
+ kind: file.kind,
182
+ startMs: part.startMs,
183
+ endMs: part.endMs,
184
+ mtime: file.mtime,
185
+ });
186
+ atlas.putVector(hash, version, embedded[i]);
187
+ })
188
+ );
96
189
  added += 1;
97
190
  cache = null;
98
- } catch (error) { log(`media: ${path.basename(file.path)}: ${error.message}`); }
191
+ } catch (error) {
192
+ log(`media: ${path.basename(file.path)}: ${error.message}`);
193
+ }
99
194
  }
100
195
  return { files: files.length, added };
101
196
  };
@@ -104,15 +199,30 @@ export function createMediaIndex({ atlas, inference, folders = () => [], log = (
104
199
  const search = async (query, { limit = 5, lane = "interactive" } = {}) => {
105
200
  const rows = vectors();
106
201
  if (!rows.length) return [];
107
- const [q] = await inference.embed([{ kind: "query", text: String(query).slice(0, 2_000) }], { lane });
202
+ const [q] = await inference.embed(
203
+ [{ kind: "query", text: String(query).slice(0, 2_000) }],
204
+ { lane }
205
+ );
108
206
  const best = new Map();
109
207
  for (const row of rows) {
110
208
  let dot = 0;
111
209
  for (let i = 0; i < q.length; i += 1) dot += q[i] * row.vector[i];
112
- if ((best.get(row.path)?.score ?? -1) < dot) best.set(row.path, { path: row.path, kind: row.kind, startMs: row.startMs, endMs: row.endMs, at: row.kind === "audio" ? clock(row.startMs) : null, score: Math.round(dot * 1000) / 1000 });
210
+ if ((best.get(row.path)?.score ?? -1) < dot)
211
+ best.set(row.path, {
212
+ path: row.path,
213
+ kind: row.kind,
214
+ startMs: row.startMs,
215
+ endMs: row.endMs,
216
+ at: row.kind === "audio" ? clock(row.startMs) : null,
217
+ score: Math.round(dot * 1000) / 1000,
218
+ });
113
219
  }
114
220
  return [...best.values()].sort((a, b) => b.score - a.score).slice(0, limit);
115
221
  };
116
222
 
117
- return { refresh, search, status: () => ({ folders: folders(), files: atlas.mediaPaths().length }) };
223
+ return {
224
+ refresh,
225
+ search,
226
+ status: () => ({ folders: folders(), files: atlas.mediaPaths().length }),
227
+ };
118
228
  }
package/lib/members.json CHANGED
@@ -20,7 +20,7 @@
20
20
  "about": "research on X and the web",
21
21
  "where": "in its own cloud", "limits": "Cannot see files on this computer unless one is attached with share_with_cloud, and cannot run anything here." },
22
22
  { "id": "bankr", "name": "Bankr", "aliases": [], "road": "server", "home": "cloud", "runs": "server", "target": true, "wake": "run",
23
- "about": "the person's crypto wallet and trading, always under the person's own approval and limits",
23
+ "about": "holds the swarm's wallet, which is the person's own, and does anything onchain the work needs: buying, selling, swapping, sending or bridging tokens, or any other transaction, always under the person's own approval and limits",
24
24
  "where": "on Harness's server, through Bankr", "limits": "Cannot see files on this computer. Reading balances or a transaction needs no task: an agent on this computer reads them with the wallet tool in seconds." },
25
25
  { "id": "quotient", "name": "Quotient", "aliases": [], "road": "server", "home": "cloud", "runs": "server", "target": true, "wake": "run",
26
26
  "about": "prediction markets research: one search over the markets it tracks, with their odds and rules",
package/lib/members.mjs CHANGED
@@ -5,29 +5,41 @@ import { readFileSync } from "node:fs";
5
5
  // waiting (wake). The server reads the same file (src/shared/members.ts). A caller on this machine names itself
6
6
  // (--agent) and is taken at its word: anything running as the person could read a token kept for it, so none is
7
7
  // kept (owner, October 10). A call through Harness is never taken for a member on this machine (hostedCaller).
8
- const ROSTER = JSON.parse(readFileSync(new URL("./members.json", import.meta.url), "utf8"));
8
+ const ROSTER = JSON.parse(
9
+ readFileSync(new URL("./members.json", import.meta.url), "utf8")
10
+ );
9
11
 
10
12
  export const ROSTER_VERSION = ROSTER.version;
11
13
  export const MEMBERS = ROSTER.members;
12
14
  const byId = new Map(MEMBERS.map((entry) => [entry.id, entry]));
13
15
 
14
16
  export const member = (id) => byId.get(String(id)) ?? null;
15
- export const ids = (keep = () => true) => MEMBERS.filter(keep).map((entry) => entry.id);
17
+ export const ids = (keep = () => true) =>
18
+ MEMBERS.filter(keep).map((entry) => entry.id);
16
19
  /** Members others may hand work to, Harness included. */
17
20
  export const TARGETS = ids((entry) => entry.target);
18
21
  /** Coding agents the daemon runs on this machine. */
19
22
  export const MACHINE_RUNNERS = ids((entry) => entry.runs === "machine");
20
23
  /** Targets whose tasks live on Harness's server, not the machine (Harness itself excluded). */
21
- export const CLOUD_HOMED = ids((entry) => entry.target && entry.home === "cloud" && entry.id !== "harness");
24
+ export const CLOUD_HOMED = ids(
25
+ (entry) => entry.target && entry.home === "cloud" && entry.id !== "harness"
26
+ );
22
27
  /** Targets asked by a task rather than started here: they run themselves or on the server. */
23
- export const ASKED_BY_TASK = ids((entry) => entry.target && entry.runs !== "machine" && entry.id !== "harness");
28
+ export const ASKED_BY_TASK = ids(
29
+ (entry) => entry.target && entry.runs !== "machine" && entry.id !== "harness"
30
+ );
24
31
  export const nameOf = (id) => member(id)?.name ?? String(id);
25
32
  export const aboutOf = (id) => {
26
33
  const entry = member(id);
27
34
  return entry ? `${entry.name}: ${entry.about}` : String(id);
28
35
  };
29
36
 
30
- const plain = (value) => String(value || "").trim().toLowerCase().replace(/^@/, "").replace(/[\s_]+/g, " ");
37
+ const plain = (value) =>
38
+ String(value || "")
39
+ .trim()
40
+ .toLowerCase()
41
+ .replace(/^@/, "")
42
+ .replace(/[\s_]+/g, " ");
31
43
  /**
32
44
  * A name as people and agents write it, resolved to one member: an id, a display name or an alias, else the one
33
45
  * member whose name it starts. A start shared by several ("claude": Claude Code and the Claude app) comes back
@@ -37,11 +49,25 @@ const plain = (value) => String(value || "").trim().toLowerCase().replace(/^@/,
37
49
  export function resolveMember(name, { extra = [] } = {}) {
38
50
  const wanted = plain(name);
39
51
  if (!wanted) return { ok: false, candidates: [] };
40
- const exact = MEMBERS.find((entry) => entry.id === wanted || plain(entry.name) === wanted || entry.aliases.some((alias) => plain(alias) === wanted));
52
+ const exact = MEMBERS.find(
53
+ (entry) =>
54
+ entry.id === wanted ||
55
+ plain(entry.name) === wanted ||
56
+ entry.aliases.some((alias) => plain(alias) === wanted)
57
+ );
41
58
  if (exact) return { ok: true, id: exact.id };
42
59
  if (extra.map(String).includes(wanted)) return { ok: true, id: wanted };
43
60
  const words = wanted.split(" ");
44
- const candidates = MEMBERS.filter((entry) => [entry.id.replace(/-/g, " "), plain(entry.name), ...entry.aliases.map(plain)]
45
- .some((label) => words.every((word, index) => label.split(" ")[index]?.startsWith(word)))).map((entry) => entry.id);
46
- return candidates.length === 1 ? { ok: true, id: candidates[0] } : { ok: false, candidates };
61
+ const candidates = MEMBERS.filter((entry) =>
62
+ [
63
+ entry.id.replace(/-/g, " "),
64
+ plain(entry.name),
65
+ ...entry.aliases.map(plain),
66
+ ].some((label) =>
67
+ words.every((word, index) => label.split(" ")[index]?.startsWith(word))
68
+ )
69
+ ).map((entry) => entry.id);
70
+ return candidates.length === 1
71
+ ? { ok: true, id: candidates[0] }
72
+ : { ok: false, candidates };
47
73
  }