tryharness 0.7.18 → 0.7.23
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/README.md +57 -14
- package/assets/opencode-plugin.js +47 -66
- package/assets/pi-extension.ts +91 -90
- package/cli.mjs +115 -9
- package/lib/adapters.mjs +19 -11
- package/lib/agentnode.mjs +154 -0
- package/lib/atlas.mjs +304 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +16 -4
- package/lib/context.mjs +290 -0
- package/lib/coordinator.mjs +27 -9
- package/lib/daemon.mjs +790 -93
- package/lib/digest.mjs +40 -0
- package/lib/environment.mjs +98 -0
- package/lib/firewall.mjs +154 -0
- package/lib/folders.mjs +161 -0
- package/lib/frames.mjs +159 -0
- package/lib/inference.mjs +326 -0
- package/lib/learn.mjs +120 -0
- package/lib/live.mjs +30 -5
- package/lib/localmodels.mjs +230 -0
- package/lib/mcp-stdio.mjs +107 -7
- package/lib/media.mjs +118 -0
- package/lib/members.json +1 -1
- package/lib/models.mjs +233 -0
- package/lib/network.mjs +118 -0
- package/lib/nudge.mjs +13 -5
- package/lib/ocr.mjs +217 -0
- package/lib/personal.mjs +39 -0
- package/lib/phones.mjs +31 -2
- package/lib/places.mjs +61 -0
- package/lib/receipts.mjs +42 -0
- package/lib/redact.mjs +334 -0
- package/lib/runner.mjs +37 -6
- package/lib/runtime-install.mjs +6 -5
- package/lib/screens.mjs +96 -0
- package/lib/sealed.mjs +3 -0
- package/lib/service.mjs +4 -2
- package/lib/snippets.mjs +64 -0
- package/lib/speech.mjs +480 -0
- package/lib/store.mjs +64 -5
- package/lib/swarm.mjs +89 -26
- package/lib/t3.mjs +7 -4
- package/lib/trails.mjs +177 -0
- package/lib/watches.mjs +163 -0
- package/lib/worktrees.mjs +21 -5
- package/package.json +6 -4
- package/lib/embedder.mjs +0 -156
package/lib/worktrees.mjs
CHANGED
|
@@ -142,17 +142,33 @@ export function createWorktrees({ store, root }) {
|
|
|
142
142
|
return { ok: true };
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The thread's folder changed: its copy of the old repository goes when nothing in it is the person's yet, and the
|
|
147
|
+
* next turn makes whatever the new folder needs. Changes not yet merged or discarded keep it where it is.
|
|
148
|
+
*/
|
|
149
|
+
const moved = (threadId) => {
|
|
150
|
+
const tree = get(threadId);
|
|
151
|
+
if (tree?.state === "open" && fs.existsSync(tree.path)) {
|
|
152
|
+
if (stat(tree.path, `${tree.base}..HEAD`).length) return { ok: false, error: `this thread changed files on ${tree.branch}; merge or discard them first` };
|
|
153
|
+
retire(tree);
|
|
154
|
+
git(tree.repo, ["branch", "-D", tree.branch]);
|
|
155
|
+
}
|
|
156
|
+
if (tree) save(threadId, null);
|
|
157
|
+
return { ok: true };
|
|
158
|
+
};
|
|
159
|
+
|
|
145
160
|
/** A thread that carries on a session from a terminal works where that session did: agents find sessions by folder. */
|
|
146
161
|
const inPlace = (threadId) => save(threadId, { state: "in_place" });
|
|
147
162
|
|
|
148
163
|
/**
|
|
149
164
|
* Where T3 Code runs a thread's agents (t3.mjs runInT3): the checkout T3 made for the thread's first agent, shared by
|
|
150
|
-
* the rest as its own worktree is, or the thread's own copy when it worked here before;
|
|
151
|
-
*
|
|
152
|
-
* no repository.
|
|
165
|
+
* the rest as its own worktree is, or the thread's own copy when it worked here before; a task's room that works in
|
|
166
|
+
* place, T3's project checkout itself; else a new one on its own branch from what the person has checked out (as
|
|
167
|
+
* folderFor's), or T3's project folder when the thread's folder is in no repository.
|
|
153
168
|
*/
|
|
154
169
|
const t3Workspace = (thread) => {
|
|
155
170
|
const current = get(thread.id);
|
|
171
|
+
if (current?.state === "in_place" || current?.inPlace) return { type: "root" };
|
|
156
172
|
if ((current?.state === "t3" || current?.state === "open") && current.path) return { type: "existing_worktree", worktreePath: current.path, ...(current.branch ? { branch: current.branch } : {}) };
|
|
157
173
|
const repo = repositoryOf(thread.goal.workspace);
|
|
158
174
|
if (!repo) return { type: "root" };
|
|
@@ -163,8 +179,8 @@ export function createWorktrees({ store, root }) {
|
|
|
163
179
|
/** The thread works in T3 Code from now on; `where` and `branch` once T3 has made its checkout. */
|
|
164
180
|
const inT3 = (threadId, where = null, branch = null) => {
|
|
165
181
|
const current = get(threadId);
|
|
166
|
-
if (current?.state !== "t3" || (where && where !== current.path)) save(threadId, { ...current, state: "t3", path: where ?? current?.path ?? null, branch: branch ?? current?.branch ?? null });
|
|
182
|
+
if (current?.state !== "t3" || (where && where !== current.path)) save(threadId, { ...current, ...(current?.state === "in_place" ? { inPlace: true } : {}), state: "t3", path: where ?? current?.path ?? null, branch: branch ?? current?.branch ?? null });
|
|
167
183
|
};
|
|
168
184
|
|
|
169
|
-
return { folderFor, commitTurn, diff, summary, merge, discard, inPlace, t3Workspace, inT3, get };
|
|
185
|
+
return { folderFor, commitTurn, diff, summary, merge, discard, moved, inPlace, t3Workspace, inT3, get };
|
|
170
186
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryharness",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
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"
|
|
@@ -32,15 +32,17 @@
|
|
|
32
32
|
"@clack/core": "0.5.0",
|
|
33
33
|
"@clack/prompts": "0.11.0",
|
|
34
34
|
"@huggingface/tokenizers": "^0.1.3",
|
|
35
|
+
"jpeg-js": "0.4.4",
|
|
35
36
|
"node-sqlite3-wasm": "0.8.60",
|
|
36
|
-
"onnxruntime-
|
|
37
|
+
"onnxruntime-node": "1.24.3",
|
|
37
38
|
"picocolors": "^1.1.1",
|
|
39
|
+
"pngjs": "7.0.0",
|
|
38
40
|
"qr": "0.4.2",
|
|
39
41
|
"ws": "^8.18.3"
|
|
40
42
|
},
|
|
41
43
|
"optionalDependencies": {
|
|
42
|
-
"tryharness-desktop-darwin-arm64": "0.7.
|
|
43
|
-
"tryharness-desktop-darwin-x64": "0.7.
|
|
44
|
+
"tryharness-desktop-darwin-arm64": "0.7.23",
|
|
45
|
+
"tryharness-desktop-darwin-x64": "0.7.23",
|
|
44
46
|
"tryharness-desktop-linux-x64": "0.7.4",
|
|
45
47
|
"tryharness-desktop-win32-x64": "0.7.4"
|
|
46
48
|
}
|
package/lib/embedder.mjs
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { ensureDir } from "./files.mjs";
|
|
4
|
-
|
|
5
|
-
// The daemon's own embedder for the context space: the same model, quantization, pooling
|
|
6
|
-
// and normalization as the browser (src/shared/memory/embeddingService.ts), so the two
|
|
7
|
-
// sides describe the same space under the same version key. It runs on onnxruntime-web's
|
|
8
|
-
// Node build (wasm, no native addon) with the same tokenizer library transformers.js uses,
|
|
9
|
-
// and mirrors transformers' feature-extraction pipeline step by step: tokenize with special
|
|
10
|
-
// tokens, truncate to the model's 512, pad to the batch, take the [CLS] vector, L2-normalize.
|
|
11
|
-
// Weights are downloaded into the Harness home during setup, never fetched from a browser.
|
|
12
|
-
|
|
13
|
-
export const EMBEDDING_MODEL = "Xenova/bge-base-en-v1.5";
|
|
14
|
-
export const EMBEDDING_DIMENSIONS = 768;
|
|
15
|
-
export const EMBEDDING_VERSION = "bge-base-en-v1.5:q8:cls:l2:asymmetric:v1";
|
|
16
|
-
const QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
|
|
17
|
-
const MODEL_FILES = ["config.json", "tokenizer.json", "tokenizer_config.json", "onnx/model_quantized.onnx"];
|
|
18
|
-
const REMOTE = `https://huggingface.co/${EMBEDDING_MODEL}/resolve/main/`;
|
|
19
|
-
// One text per run: a q8 model quantizes per batch tensor, so the same text in different company
|
|
20
|
-
// differs in the last decimals (transformers.js shows the same, cosine 0.992 alone versus batched).
|
|
21
|
-
// Entries arrive one at a time and a vector must not depend on its neighbours, so no batching.
|
|
22
|
-
const BATCH = 1;
|
|
23
|
-
|
|
24
|
-
export function modelDirectory(modelsDir) {
|
|
25
|
-
return path.join(modelsDir, ...EMBEDDING_MODEL.split("/"));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** True when every model file is present and non-empty. */
|
|
29
|
-
export function modelPresent(modelsDir) {
|
|
30
|
-
const dir = modelDirectory(modelsDir);
|
|
31
|
-
return MODEL_FILES.every((file) => {
|
|
32
|
-
try { return fs.statSync(path.join(dir, file)).size > 0; } catch { return false; }
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function download(url, target, { fetchImpl, log }) {
|
|
37
|
-
const partial = `${target}.partial`;
|
|
38
|
-
ensureDir(path.dirname(target));
|
|
39
|
-
let have = 0;
|
|
40
|
-
try { have = fs.statSync(partial).size; } catch {}
|
|
41
|
-
const headers = have > 0 ? { range: `bytes=${have}-` } : {};
|
|
42
|
-
const response = await fetchImpl(url, { headers, redirect: "follow" });
|
|
43
|
-
if (response.status === 416) { fs.renameSync(partial, target); return; }
|
|
44
|
-
if (!response.ok) throw new Error(`download of ${path.basename(target)} failed: HTTP ${response.status}`);
|
|
45
|
-
const resumed = response.status === 206;
|
|
46
|
-
const total = Number(response.headers.get("content-length") || 0) + (resumed ? have : 0);
|
|
47
|
-
const out = fs.createWriteStream(partial, { flags: resumed ? "a" : "w" });
|
|
48
|
-
let done = resumed ? have : 0;
|
|
49
|
-
let shown = -1;
|
|
50
|
-
const name = path.basename(target);
|
|
51
|
-
for await (const chunk of response.body) {
|
|
52
|
-
if (!out.write(chunk)) await new Promise((resolve) => out.once("drain", resolve));
|
|
53
|
-
done += chunk.length;
|
|
54
|
-
const percent = total ? Math.floor((done / total) * 10) * 10 : -1;
|
|
55
|
-
if (percent !== shown && percent >= 0) { shown = percent; log?.(`embedder: ${name} ${percent}%`); }
|
|
56
|
-
}
|
|
57
|
-
await new Promise((resolve, reject) => { out.end(); out.on("finish", resolve); out.on("error", reject); });
|
|
58
|
-
if (total && done !== total) throw new Error(`download of ${name} stopped at ${done} of ${total} bytes; run setup again to resume`);
|
|
59
|
-
fs.renameSync(partial, target);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Downloads any missing model file into modelsDir. Idempotent; a partial file resumes. */
|
|
63
|
-
export async function ensureModel({ modelsDir, log = () => {}, fetchImpl = fetch } = {}) {
|
|
64
|
-
const dir = modelDirectory(modelsDir);
|
|
65
|
-
const missing = MODEL_FILES.filter((file) => { try { return fs.statSync(path.join(dir, file)).size === 0; } catch { return true; } });
|
|
66
|
-
if (!missing.length) return { downloaded: [], present: true };
|
|
67
|
-
log(`embedder: downloading ${EMBEDDING_MODEL} (${missing.length} file${missing.length === 1 ? "" : "s"}, about 110 MB) into ${modelsDir}`);
|
|
68
|
-
for (const file of missing) await download(REMOTE + file, path.join(dir, file), { fetchImpl, log });
|
|
69
|
-
log("embedder: ready");
|
|
70
|
-
return { downloaded: missing, present: true };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function truncate(encoded, maxLength) {
|
|
74
|
-
for (const key of Object.keys(encoded)) encoded[key] = encoded[key].slice(0, maxLength);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function pad(encoded, maxLength, padId) {
|
|
78
|
-
for (const key of Object.keys(encoded)) {
|
|
79
|
-
const fill = key === "input_ids" ? padId : 0;
|
|
80
|
-
while (encoded[key].length < maxLength) encoded[key].push(fill);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* A lazily loaded embedder over local files only. Loading the session takes about half a
|
|
86
|
-
* second and inference runs on the calling thread (onnxruntime-web's Node build has no
|
|
87
|
-
* worker), so each short text holds the event loop for a few tens of milliseconds;
|
|
88
|
-
* callers embed in the background, never inside a request the person is waiting on.
|
|
89
|
-
*/
|
|
90
|
-
export function createEmbedder({ modelsDir }) {
|
|
91
|
-
const dir = modelDirectory(modelsDir);
|
|
92
|
-
let loading = null;
|
|
93
|
-
const load = () => {
|
|
94
|
-
if (loading) return loading;
|
|
95
|
-
loading = (async () => {
|
|
96
|
-
if (!modelPresent(modelsDir)) throw new Error(`the embedding model is not present in ${modelsDir}; run npx tryharness to download it`);
|
|
97
|
-
const [{ Tokenizer }, ort] = await Promise.all([import("@huggingface/tokenizers"), import("onnxruntime-web")]);
|
|
98
|
-
const tokenizerJson = JSON.parse(fs.readFileSync(path.join(dir, "tokenizer.json"), "utf8"));
|
|
99
|
-
const tokenizerConfig = JSON.parse(fs.readFileSync(path.join(dir, "tokenizer_config.json"), "utf8"));
|
|
100
|
-
const modelConfig = JSON.parse(fs.readFileSync(path.join(dir, "config.json"), "utf8"));
|
|
101
|
-
const tokenizer = new Tokenizer(tokenizerJson, tokenizerConfig);
|
|
102
|
-
ort.env.wasm.numThreads = 1;
|
|
103
|
-
const session = await ort.InferenceSession.create(path.join(dir, "onnx", "model_quantized.onnx"), { executionProviders: ["wasm"] });
|
|
104
|
-
return {
|
|
105
|
-
ort, tokenizer, session,
|
|
106
|
-
maxLength: Number(tokenizerConfig.model_max_length) || 512,
|
|
107
|
-
padId: Number(modelConfig.pad_token_id ?? 0),
|
|
108
|
-
wantsTypeIds: session.inputNames.includes("token_type_ids"),
|
|
109
|
-
};
|
|
110
|
-
})();
|
|
111
|
-
loading.catch(() => { loading = null; });
|
|
112
|
-
return loading;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const embedBatch = async (texts) => {
|
|
116
|
-
const { ort, tokenizer, session, maxLength, padId, wantsTypeIds } = await load();
|
|
117
|
-
const encoded = texts.map((text) => {
|
|
118
|
-
const { ids, attention_mask, token_type_ids } = tokenizer.encode(text, { add_special_tokens: true, return_token_type_ids: true });
|
|
119
|
-
return { input_ids: [...ids], attention_mask: [...attention_mask], ...(wantsTypeIds ? { token_type_ids: [...(token_type_ids || ids.map(() => 0))] } : {}) };
|
|
120
|
-
});
|
|
121
|
-
const width = Math.min(Math.max(...encoded.map((e) => e.input_ids.length)), maxLength);
|
|
122
|
-
for (const e of encoded) {
|
|
123
|
-
if (e.input_ids.length > width) truncate(e, width);
|
|
124
|
-
else if (e.input_ids.length < width) pad(e, width, padId);
|
|
125
|
-
}
|
|
126
|
-
const tensor = (key) => new ort.Tensor("int64", BigInt64Array.from(encoded.flatMap((e) => e[key].map((v) => BigInt(v)))), [texts.length, width]);
|
|
127
|
-
const feeds = { input_ids: tensor("input_ids"), attention_mask: tensor("attention_mask"), ...(wantsTypeIds ? { token_type_ids: tensor("token_type_ids") } : {}) };
|
|
128
|
-
const outputs = await session.run(feeds);
|
|
129
|
-
const hidden = outputs.last_hidden_state;
|
|
130
|
-
const [, seq, dims] = hidden.dims;
|
|
131
|
-
const data = hidden.data;
|
|
132
|
-
return texts.map((_, row) => {
|
|
133
|
-
const start = row * seq * dims;
|
|
134
|
-
const vector = new Float32Array(dims);
|
|
135
|
-
let norm = 0;
|
|
136
|
-
for (let i = 0; i < dims; i += 1) { vector[i] = data[start + i]; norm += vector[i] * vector[i]; }
|
|
137
|
-
norm = Math.sqrt(norm) || 1;
|
|
138
|
-
for (let i = 0; i < dims; i += 1) vector[i] /= norm;
|
|
139
|
-
return vector;
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
version: EMBEDDING_VERSION,
|
|
145
|
-
dimensions: EMBEDDING_DIMENSIONS,
|
|
146
|
-
ready: () => load().then(() => true),
|
|
147
|
-
async embedDocuments(texts) {
|
|
148
|
-
const out = [];
|
|
149
|
-
for (let i = 0; i < texts.length; i += BATCH) out.push(...(await embedBatch(texts.slice(i, i + BATCH))));
|
|
150
|
-
return out;
|
|
151
|
-
},
|
|
152
|
-
async embedQuery(text) {
|
|
153
|
-
return (await embedBatch([QUERY_PREFIX + text]))[0];
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|