tryharness 0.7.28 → 0.7.29
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 +5 -4
- package/cli.mjs +32 -7
- package/lib/atlas.mjs +155 -31
- package/lib/context.mjs +44 -32
- package/lib/daemon.mjs +193 -34
- package/lib/desktop.mjs +24 -1
- package/lib/library.mjs +693 -0
- package/lib/mcp-stdio.mjs +12 -6
- package/lib/readers.mjs +459 -0
- package/lib/service.mjs +21 -1
- package/lib/snippets.mjs +114 -7
- package/lib/store.mjs +6 -0
- package/package.json +1 -1
package/lib/snippets.mjs
CHANGED
|
@@ -4,10 +4,12 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { nameOf } from "./members.mjs";
|
|
5
5
|
import { repositoryOf } from "./worktrees.mjs";
|
|
6
6
|
|
|
7
|
-
// Code for an agent that does not run on this computer (Grok Bot, Muse) leaves it only with the person's OK.
|
|
8
|
-
// says what it needs to see (request_snippet); the relay finds it
|
|
9
|
-
// the
|
|
10
|
-
// `tryharness board snippet <id> share|keep`); shared
|
|
7
|
+
// Code or a file for an agent that does not run on this computer (Grok Bot, Muse) leaves it only with the person's OK.
|
|
8
|
+
// The agent says what it needs to see (request_snippet); the relay finds it with its local search, in the thread's
|
|
9
|
+
// folder (code) or in the folders the person added to the library (files), masks any secret in it, and holds it. The
|
|
10
|
+
// person shares or keeps it (the buddy, or `tryharness board snippet <id> share|keep`); what is shared is posted in the
|
|
11
|
+
// thread for that agent, what is kept never leaves. A file's part goes as its words (a page, a passage), a picture with
|
|
12
|
+
// keys and personal details covered, or what a recording's minute says.
|
|
11
13
|
|
|
12
14
|
const BEFORE = 5;
|
|
13
15
|
const LINES = 40;
|
|
@@ -16,6 +18,7 @@ export function createSnippets({
|
|
|
16
18
|
store,
|
|
17
19
|
search,
|
|
18
20
|
scrub,
|
|
21
|
+
files = null,
|
|
19
22
|
wake = () => {},
|
|
20
23
|
notify = () => {},
|
|
21
24
|
log = () => {},
|
|
@@ -35,9 +38,12 @@ export function createSnippets({
|
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
/** A cloud agent's request: { ok, snippet } held for the person, or why there is nothing to hold. */
|
|
38
|
-
const request = async ({ threadId, agent, about }) => {
|
|
41
|
+
const request = async ({ threadId, agent, about, where = null }) => {
|
|
39
42
|
const thread = store.getTask(threadId);
|
|
40
43
|
const folder = thread?.kind === "goal" ? thread.goal.workspace : null;
|
|
44
|
+
// Without a say, a thread with a folder means its code, one without means the person's files.
|
|
45
|
+
if ((where ?? (folder ? "code" : "files")) === "files")
|
|
46
|
+
return requestFile({ threadId, agent, about });
|
|
41
47
|
if (!folder)
|
|
42
48
|
return {
|
|
43
49
|
ok: false,
|
|
@@ -100,6 +106,107 @@ export function createSnippets({
|
|
|
100
106
|
};
|
|
101
107
|
};
|
|
102
108
|
|
|
109
|
+
/** The part of a file the agent asked about, prepared to leave: { text } or { picture }, and what was hidden. */
|
|
110
|
+
const prepare = async (content) => {
|
|
111
|
+
if (content.modality === "text") return scrub(content.text);
|
|
112
|
+
if (content.modality === "audio") {
|
|
113
|
+
const heard = await files.transcribe(content);
|
|
114
|
+
return heard
|
|
115
|
+
? scrub(heard)
|
|
116
|
+
: { error: "nothing could be heard in that minute" };
|
|
117
|
+
}
|
|
118
|
+
const covered = await files.cover(content.data);
|
|
119
|
+
if (covered.refused) return { error: covered.refused };
|
|
120
|
+
return {
|
|
121
|
+
picture: files.keep(covered.bytes),
|
|
122
|
+
mime: covered.mime,
|
|
123
|
+
masked: covered.masked?.length ?? 0,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const requestFile = async ({ threadId, agent, about }) => {
|
|
128
|
+
if (!files)
|
|
129
|
+
return { ok: false, error: "the person's files are not searchable here" };
|
|
130
|
+
const words = String(about || "")
|
|
131
|
+
.trim()
|
|
132
|
+
.slice(0, 500);
|
|
133
|
+
if (!words) return { ok: false, error: "say what you need to see" };
|
|
134
|
+
const [hit] = await files.search(words);
|
|
135
|
+
if (!hit)
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
error: "nothing in the folders the person added matched that",
|
|
139
|
+
};
|
|
140
|
+
let ready = null;
|
|
141
|
+
try {
|
|
142
|
+
const content = await files.content(hit.file, hit.vec);
|
|
143
|
+
ready = content ? await prepare(content) : null;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
ready = { error: error.message };
|
|
146
|
+
}
|
|
147
|
+
if (!ready || ready.error)
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: ready?.error || `${hit.name} could not be read`,
|
|
151
|
+
};
|
|
152
|
+
const label = hit.place ? `${hit.place} of ${hit.name}` : hit.name;
|
|
153
|
+
const snippet = {
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
thread: threadId,
|
|
156
|
+
agent,
|
|
157
|
+
about: words,
|
|
158
|
+
source: "files",
|
|
159
|
+
label,
|
|
160
|
+
path: hit.file,
|
|
161
|
+
text:
|
|
162
|
+
ready.text ??
|
|
163
|
+
`A picture: ${label}, with keys and personal details covered.`,
|
|
164
|
+
picture: ready.picture ?? null,
|
|
165
|
+
mime: ready.mime ?? null,
|
|
166
|
+
masked: ready.masked,
|
|
167
|
+
state: "waiting",
|
|
168
|
+
at: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
save(snippet);
|
|
171
|
+
store.roomPost({
|
|
172
|
+
goalId: threadId,
|
|
173
|
+
author: "harness",
|
|
174
|
+
kind: "note",
|
|
175
|
+
text: `${nameOf(agent)} asks to see ${label} from your files ("${words.slice(0, 120)}"). It stays on this computer until you share it.`,
|
|
176
|
+
});
|
|
177
|
+
notify(
|
|
178
|
+
`${nameOf(agent)} asks to see ${label}. Share or keep it in Harness.`,
|
|
179
|
+
snippet
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
snippet: { id: snippet.id, label, state: "waiting" },
|
|
184
|
+
note: "The person decides whether to share it; you are told in the thread.",
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** What the thread is told when the person shares a waiting snippet. */
|
|
189
|
+
const sharedText = (snippet) => {
|
|
190
|
+
const hidden = snippet.masked
|
|
191
|
+
? ` (${snippet.masked} secret${snippet.masked === 1 ? "" : "s"} in it stay masked)`
|
|
192
|
+
: "";
|
|
193
|
+
if (snippet.source !== "files")
|
|
194
|
+
return `@${snippet.agent} the person shared lines ${snippet.from} to ${snippet.to} of ${snippet.path}${hidden}:\n\n\`\`\`\n${snippet.text}\n\`\`\``;
|
|
195
|
+
if (snippet.picture) {
|
|
196
|
+
const file = files.attach({
|
|
197
|
+
threadId: snippet.thread,
|
|
198
|
+
sha256: snippet.picture,
|
|
199
|
+
mime: snippet.mime,
|
|
200
|
+
name: `${snippet.label}.${snippet.mime === "image/jpeg" ? "jpg" : "png"}`,
|
|
201
|
+
});
|
|
202
|
+
return `@${snippet.agent} the person shared ${snippet.label}, with keys and personal details covered: file ${file.id} (get_file opens it).`;
|
|
203
|
+
}
|
|
204
|
+
return `@${snippet.agent} the person shared ${snippet.label}${hidden}:\n\n${snippet.text
|
|
205
|
+
.split("\n")
|
|
206
|
+
.map((line) => `> ${line}`)
|
|
207
|
+
.join("\n")}`;
|
|
208
|
+
};
|
|
209
|
+
|
|
103
210
|
/** The person shares or keeps a waiting snippet. */
|
|
104
211
|
const decide = (id, share) => {
|
|
105
212
|
const snippet = read(id);
|
|
@@ -118,7 +225,7 @@ export function createSnippets({
|
|
|
118
225
|
author: "harness",
|
|
119
226
|
kind: "post",
|
|
120
227
|
mentions: [snippet.agent],
|
|
121
|
-
text:
|
|
228
|
+
text: sharedText(snippet),
|
|
122
229
|
});
|
|
123
230
|
wake(snippet.thread, post, [snippet.agent]);
|
|
124
231
|
} else
|
|
@@ -126,7 +233,7 @@ export function createSnippets({
|
|
|
126
233
|
goalId: snippet.thread,
|
|
127
234
|
author: "harness",
|
|
128
235
|
kind: "note",
|
|
129
|
-
text: `The person kept lines ${snippet.from} to ${snippet.to} of ${snippet.path} on this computer.`,
|
|
236
|
+
text: `The person kept ${snippet.label ?? `lines ${snippet.from} to ${snippet.to} of ${snippet.path}`} on this computer.`,
|
|
130
237
|
});
|
|
131
238
|
log(`snippet ${id}: ${done.state}`);
|
|
132
239
|
return { ok: true, snippet: { id, state: done.state } };
|
package/lib/store.mjs
CHANGED
|
@@ -1670,6 +1670,12 @@ export function openStore(file) {
|
|
|
1670
1670
|
String(id)
|
|
1671
1671
|
);
|
|
1672
1672
|
},
|
|
1673
|
+
presentFiles() {
|
|
1674
|
+
return execute(
|
|
1675
|
+
"all",
|
|
1676
|
+
"select id from board_files where present = 1 order by created_at asc"
|
|
1677
|
+
).map((row) => this.file(row.id));
|
|
1678
|
+
},
|
|
1673
1679
|
taskFiles(taskId) {
|
|
1674
1680
|
return execute(
|
|
1675
1681
|
"all",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryharness",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.29",
|
|
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"
|