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/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. The agent
8
- // says what it needs to see (request_snippet); the relay finds it in the thread's folder with its local search, takes
9
- // the lines around it, masks any secret in them, and holds them. The person shares or keeps them (the buddy, or
10
- // `tryharness board snippet <id> share|keep`); shared lines are posted in the thread for that agent, kept ones never leave.
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: `@${snippet.agent} the person shared lines ${snippet.from} to ${snippet.to} of ${snippet.path}${snippet.masked ? ` (${snippet.masked} secret${snippet.masked === 1 ? "" : "s"} in them stay masked)` : ""}:\n\n\`\`\`\n${snippet.text}\n\`\`\``,
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.28",
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"