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/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 {
|
|
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()) {
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
69
|
+
const out = path.join(
|
|
70
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "harness-media-")),
|
|
71
|
+
"audio.wav"
|
|
72
|
+
);
|
|
51
73
|
try {
|
|
52
|
-
const converted =
|
|
53
|
-
|
|
54
|
-
|
|
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 {
|
|
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(
|
|
62
|
-
|
|
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({
|
|
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) => {
|
|
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({
|
|
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())
|
|
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 =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(() =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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) {
|
|
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(
|
|
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)
|
|
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 {
|
|
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
|
|
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(
|
|
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) =>
|
|
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(
|
|
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(
|
|
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) =>
|
|
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(
|
|
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) =>
|
|
45
|
-
|
|
46
|
-
|
|
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
|
}
|