tryharness 0.7.22 → 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/README.md +48 -8
- package/assets/opencode-plugin.js +48 -10
- package/assets/pi-extension.ts +106 -26
- package/cli.mjs +1281 -289
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +604 -0
- package/lib/bip39-english.txt +2048 -0
- package/lib/cards.mjs +177 -39
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +628 -0
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +281 -56
- package/lib/daemon.mjs +5590 -838
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +81 -0
- package/lib/environment.mjs +180 -0
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +288 -0
- package/lib/folders.mjs +223 -57
- package/lib/frames.mjs +333 -0
- package/lib/inference.mjs +663 -0
- package/lib/learn.mjs +182 -0
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +571 -0
- package/lib/mcp-stdio.mjs +912 -103
- package/lib/media.mjs +228 -0
- 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 +190 -32
- package/lib/ocr.mjs +380 -0
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +77 -0
- package/lib/phones.mjs +398 -51
- package/lib/places.mjs +566 -102
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +82 -0
- package/lib/redact.mjs +730 -0
- package/lib/runner.mjs +347 -55
- package/lib/runtime-install.mjs +59 -17
- package/lib/screens.mjs +212 -0
- package/lib/sealed.mjs +160 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +173 -35
- package/lib/snippets.mjs +140 -0
- package/lib/speech.mjs +993 -0
- package/lib/store.mjs +1678 -301
- package/lib/swarm.mjs +598 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +401 -0
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +362 -0
- package/lib/worktrees.mjs +186 -38
- package/package.json +6 -4
- package/lib/embedder.mjs +0 -156
package/lib/service.mjs
CHANGED
|
@@ -9,14 +9,27 @@ import { paths } from "./paths.mjs";
|
|
|
9
9
|
const LABEL = "ai.tryharness.daemon";
|
|
10
10
|
|
|
11
11
|
function xml(value) {
|
|
12
|
-
return String(value)
|
|
12
|
+
return String(value)
|
|
13
|
+
.replaceAll("&", "&")
|
|
14
|
+
.replaceAll("<", "<")
|
|
15
|
+
.replaceAll(">", ">")
|
|
16
|
+
.replaceAll('"', """)
|
|
17
|
+
.replaceAll("'", "'");
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
const WINDOWS_TASK = "tryharness-daemon";
|
|
16
21
|
|
|
17
22
|
function servicePath(platform = process.platform, homeDir = os.homedir()) {
|
|
18
|
-
if (platform === "darwin")
|
|
19
|
-
|
|
23
|
+
if (platform === "darwin")
|
|
24
|
+
return path.join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
25
|
+
if (platform === "linux")
|
|
26
|
+
return path.join(
|
|
27
|
+
homeDir,
|
|
28
|
+
".config",
|
|
29
|
+
"systemd",
|
|
30
|
+
"user",
|
|
31
|
+
"tryharness-daemon.service"
|
|
32
|
+
);
|
|
20
33
|
if (platform === "win32") return path.join(paths.home, `${WINDOWS_TASK}.xml`);
|
|
21
34
|
return null;
|
|
22
35
|
}
|
|
@@ -59,35 +72,76 @@ export function windowsLauncher({ nodePath, entry }) {
|
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
function windowsUser(env = process.env) {
|
|
62
|
-
return env.USERDOMAIN && env.USERNAME
|
|
75
|
+
return env.USERDOMAIN && env.USERNAME
|
|
76
|
+
? `${env.USERDOMAIN}\\${env.USERNAME}`
|
|
77
|
+
: env.USERNAME || os.userInfo().username;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
export function installWindowsTask({
|
|
80
|
+
export function installWindowsTask({
|
|
81
|
+
runtimeRoot,
|
|
82
|
+
nodePath = process.execPath,
|
|
83
|
+
execute = run,
|
|
84
|
+
env = process.env,
|
|
85
|
+
}) {
|
|
66
86
|
ensureDir(paths.home);
|
|
67
87
|
const entry = path.join(runtimeRoot, "cli.mjs");
|
|
68
88
|
const launcher = path.join(paths.home, `${WINDOWS_TASK}.vbs`);
|
|
69
89
|
const definition = path.join(paths.home, `${WINDOWS_TASK}.xml`);
|
|
70
90
|
fs.writeFileSync(launcher, windowsLauncher({ nodePath, entry }));
|
|
71
91
|
// schtasks reads task XML as UTF-16 with a byte order mark.
|
|
72
|
-
fs.writeFileSync(
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
definition,
|
|
94
|
+
"\ufeff" +
|
|
95
|
+
windowsTaskDefinition({
|
|
96
|
+
nodePath,
|
|
97
|
+
entry,
|
|
98
|
+
user: windowsUser(env),
|
|
99
|
+
launcher,
|
|
100
|
+
}),
|
|
101
|
+
"utf16le"
|
|
102
|
+
);
|
|
103
|
+
const created = execute("schtasks", [
|
|
104
|
+
"/Create",
|
|
105
|
+
"/TN",
|
|
106
|
+
WINDOWS_TASK,
|
|
107
|
+
"/XML",
|
|
108
|
+
definition,
|
|
109
|
+
"/F",
|
|
110
|
+
]);
|
|
111
|
+
if (!created.ok)
|
|
112
|
+
throw new Error(
|
|
113
|
+
`could not register the Harness daemon with Task Scheduler: ${created.stderr || created.stdout || created.error}`
|
|
114
|
+
);
|
|
75
115
|
execute("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
76
116
|
const started = execute("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
77
|
-
if (!started.ok)
|
|
117
|
+
if (!started.ok)
|
|
118
|
+
throw new Error(
|
|
119
|
+
`the Harness daemon task was registered but did not start: ${started.stderr || started.stdout || started.error}`
|
|
120
|
+
);
|
|
78
121
|
return { manager: "task-scheduler", file: definition };
|
|
79
122
|
}
|
|
80
123
|
|
|
81
124
|
export function uninstallWindowsTask({ execute = run } = {}) {
|
|
82
125
|
execute("schtasks", ["/End", "/TN", WINDOWS_TASK]);
|
|
83
126
|
execute("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
|
|
84
|
-
for (const file of [
|
|
85
|
-
|
|
127
|
+
for (const file of [
|
|
128
|
+
path.join(paths.home, `${WINDOWS_TASK}.xml`),
|
|
129
|
+
path.join(paths.home, `${WINDOWS_TASK}.vbs`),
|
|
130
|
+
]) {
|
|
131
|
+
try {
|
|
132
|
+
fs.unlinkSync(file);
|
|
133
|
+
} catch {}
|
|
86
134
|
}
|
|
87
135
|
}
|
|
88
136
|
|
|
89
137
|
function agentPath() {
|
|
90
|
-
const dirs = new Set([
|
|
138
|
+
const dirs = new Set([
|
|
139
|
+
path.dirname(process.execPath),
|
|
140
|
+
"/opt/homebrew/bin",
|
|
141
|
+
"/usr/local/bin",
|
|
142
|
+
"/usr/bin",
|
|
143
|
+
"/bin",
|
|
144
|
+
]);
|
|
91
145
|
for (const command of ["claude", "codex", "pi", "opencode"]) {
|
|
92
146
|
const found = resolveCommand(command);
|
|
93
147
|
if (found) dirs.add(path.dirname(found));
|
|
@@ -97,78 +151,162 @@ function agentPath() {
|
|
|
97
151
|
|
|
98
152
|
function retireLegacyBridge() {
|
|
99
153
|
if (process.platform === "darwin") {
|
|
100
|
-
const legacy = path.join(
|
|
154
|
+
const legacy = path.join(
|
|
155
|
+
os.homedir(),
|
|
156
|
+
"Library",
|
|
157
|
+
"LaunchAgents",
|
|
158
|
+
"ai.tryharness.bridge.plist"
|
|
159
|
+
);
|
|
101
160
|
if (!fs.existsSync(legacy)) return;
|
|
102
161
|
run("launchctl", ["unload", legacy]);
|
|
103
|
-
try {
|
|
162
|
+
try {
|
|
163
|
+
fs.renameSync(legacy, legacy + ".retired");
|
|
164
|
+
} catch {}
|
|
104
165
|
} else if (process.platform === "linux") {
|
|
105
|
-
const legacy = path.join(
|
|
166
|
+
const legacy = path.join(
|
|
167
|
+
os.homedir(),
|
|
168
|
+
".config",
|
|
169
|
+
"systemd",
|
|
170
|
+
"user",
|
|
171
|
+
"tryharness-bridge.service"
|
|
172
|
+
);
|
|
106
173
|
if (!fs.existsSync(legacy)) return;
|
|
107
|
-
run("systemctl", [
|
|
108
|
-
|
|
174
|
+
run("systemctl", [
|
|
175
|
+
"--user",
|
|
176
|
+
"disable",
|
|
177
|
+
"--now",
|
|
178
|
+
"tryharness-bridge.service",
|
|
179
|
+
]);
|
|
180
|
+
try {
|
|
181
|
+
fs.renameSync(legacy, legacy + ".retired");
|
|
182
|
+
} catch {}
|
|
109
183
|
}
|
|
110
184
|
}
|
|
111
185
|
|
|
112
|
-
export function installService({
|
|
186
|
+
export function installService({
|
|
187
|
+
runtimeRoot,
|
|
188
|
+
nodePath = process.execPath,
|
|
189
|
+
platform = process.platform,
|
|
190
|
+
execute = run,
|
|
191
|
+
}) {
|
|
113
192
|
ensureDir(paths.home);
|
|
114
193
|
retireLegacyBridge();
|
|
115
194
|
const entry = path.join(runtimeRoot, "cli.mjs");
|
|
116
195
|
const envPath = agentPath();
|
|
117
|
-
if (platform === "win32")
|
|
196
|
+
if (platform === "win32")
|
|
197
|
+
return installWindowsTask({ runtimeRoot, nodePath, execute });
|
|
118
198
|
if (platform === "darwin") {
|
|
119
199
|
const plist = servicePath();
|
|
120
200
|
ensureDir(path.dirname(plist));
|
|
121
|
-
|
|
201
|
+
// MallocLargeCache=0: macOS otherwise keeps the large blocks ONNX Runtime frees (a screenshot's OCR held 1.6 GB after
|
|
202
|
+
// it finished; 255 MB with it off, image-redaction eval).
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
plist,
|
|
205
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
122
206
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
123
207
|
<plist version="1.0"><dict>
|
|
124
208
|
<key>Label</key><string>${LABEL}</string>
|
|
125
209
|
<key>ProgramArguments</key><array><string>${xml(nodePath)}</string><string>${xml(entry)}</string><string>daemon</string></array>
|
|
126
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(envPath)}</string></dict>
|
|
210
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(envPath)}</string><key>MallocLargeCache</key><string>0</string></dict>
|
|
127
211
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer>
|
|
128
212
|
<key>StandardOutPath</key><string>${xml(paths.log)}</string><key>StandardErrorPath</key><string>${xml(paths.log)}</string>
|
|
129
|
-
</dict></plist>\n`,
|
|
213
|
+
</dict></plist>\n`,
|
|
214
|
+
{ mode: 0o600 }
|
|
215
|
+
);
|
|
130
216
|
const domain = `gui/${process.getuid()}`;
|
|
131
217
|
run("launchctl", ["bootout", domain, plist]);
|
|
132
218
|
const started = run("launchctl", ["bootstrap", domain, plist]);
|
|
133
|
-
if (!started.ok)
|
|
219
|
+
if (!started.ok)
|
|
220
|
+
throw new Error(
|
|
221
|
+
`could not start Harness daemon with launchd: ${started.stderr || started.error}`
|
|
222
|
+
);
|
|
134
223
|
run("launchctl", ["kickstart", "-k", `${domain}/${LABEL}`]);
|
|
135
224
|
return { manager: "launchd", file: plist };
|
|
136
225
|
}
|
|
137
226
|
if (platform === "linux") {
|
|
138
227
|
const unit = servicePath();
|
|
139
228
|
ensureDir(path.dirname(unit));
|
|
140
|
-
fs.writeFileSync(
|
|
229
|
+
fs.writeFileSync(
|
|
230
|
+
unit,
|
|
231
|
+
`[Unit]\nDescription=Harness local daemon\nAfter=network-online.target\n\n[Service]\nExecStart=${JSON.stringify(nodePath)} ${JSON.stringify(entry)} daemon\nEnvironment=PATH=${envPath}\nRestart=always\nRestartSec=5\nStandardOutput=append:${paths.log}\nStandardError=append:${paths.log}\n\n[Install]\nWantedBy=default.target\n`,
|
|
232
|
+
{ mode: 0o600 }
|
|
233
|
+
);
|
|
141
234
|
const reload = run("systemctl", ["--user", "daemon-reload"]);
|
|
142
|
-
const started = run("systemctl", [
|
|
143
|
-
|
|
235
|
+
const started = run("systemctl", [
|
|
236
|
+
"--user",
|
|
237
|
+
"enable",
|
|
238
|
+
"--now",
|
|
239
|
+
"tryharness-daemon.service",
|
|
240
|
+
]);
|
|
241
|
+
if (!reload.ok || !started.ok)
|
|
242
|
+
throw new Error(
|
|
243
|
+
`could not start Harness daemon with systemd: ${started.stderr || reload.stderr}`
|
|
244
|
+
);
|
|
144
245
|
return { manager: "systemd", file: unit };
|
|
145
246
|
}
|
|
146
247
|
// Other Node platforms get a ready daemon for this login only. `doctor` reports that
|
|
147
248
|
// login persistence is unavailable rather than pretending it was set.
|
|
148
|
-
const child = spawn(nodePath, [entry, "daemon"], {
|
|
249
|
+
const child = spawn(nodePath, [entry, "daemon"], {
|
|
250
|
+
detached: true,
|
|
251
|
+
stdio: "ignore",
|
|
252
|
+
env: {
|
|
253
|
+
...process.env,
|
|
254
|
+
PATH: envPath,
|
|
255
|
+
...(process.platform === "darwin" ? { MallocLargeCache: "0" } : {}),
|
|
256
|
+
},
|
|
257
|
+
});
|
|
149
258
|
child.unref();
|
|
150
259
|
return { manager: "detached", file: null };
|
|
151
260
|
}
|
|
152
261
|
|
|
153
|
-
export function uninstallService({
|
|
262
|
+
export function uninstallService({
|
|
263
|
+
platform = process.platform,
|
|
264
|
+
execute = run,
|
|
265
|
+
} = {}) {
|
|
154
266
|
const file = servicePath(platform);
|
|
155
|
-
if (platform === "win32") {
|
|
267
|
+
if (platform === "win32") {
|
|
268
|
+
uninstallWindowsTask({ execute });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
156
271
|
if (platform === "darwin" && file) {
|
|
157
272
|
run("launchctl", ["bootout", `gui/${process.getuid()}`, file]);
|
|
158
|
-
try {
|
|
273
|
+
try {
|
|
274
|
+
fs.unlinkSync(file);
|
|
275
|
+
} catch {}
|
|
159
276
|
} else if (platform === "linux" && file) {
|
|
160
|
-
run("systemctl", [
|
|
161
|
-
|
|
277
|
+
run("systemctl", [
|
|
278
|
+
"--user",
|
|
279
|
+
"disable",
|
|
280
|
+
"--now",
|
|
281
|
+
"tryharness-daemon.service",
|
|
282
|
+
]);
|
|
283
|
+
try {
|
|
284
|
+
fs.unlinkSync(file);
|
|
285
|
+
} catch {}
|
|
162
286
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
163
287
|
}
|
|
164
288
|
}
|
|
165
289
|
|
|
166
|
-
export function serviceInfo({
|
|
290
|
+
export function serviceInfo({
|
|
291
|
+
platform = process.platform,
|
|
292
|
+
execute = run,
|
|
293
|
+
} = {}) {
|
|
167
294
|
const file = servicePath(platform);
|
|
168
295
|
if (platform === "win32") {
|
|
169
296
|
// Installed means the task is registered, not merely that its files exist.
|
|
170
|
-
const registered =
|
|
297
|
+
const registered =
|
|
298
|
+
Boolean(file && fs.existsSync(file)) &&
|
|
299
|
+
execute("schtasks", ["/Query", "/TN", WINDOWS_TASK]).ok;
|
|
171
300
|
return { manager: "task-scheduler", installed: registered, file };
|
|
172
301
|
}
|
|
173
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
manager:
|
|
304
|
+
platform === "darwin"
|
|
305
|
+
? "launchd"
|
|
306
|
+
: platform === "linux"
|
|
307
|
+
? "systemd"
|
|
308
|
+
: "detached",
|
|
309
|
+
installed: Boolean(file && fs.existsSync(file)),
|
|
310
|
+
file,
|
|
311
|
+
};
|
|
174
312
|
}
|
package/lib/snippets.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { nameOf } from "./members.mjs";
|
|
5
|
+
import { repositoryOf } from "./worktrees.mjs";
|
|
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.
|
|
11
|
+
|
|
12
|
+
const BEFORE = 5;
|
|
13
|
+
const LINES = 40;
|
|
14
|
+
|
|
15
|
+
export function createSnippets({
|
|
16
|
+
store,
|
|
17
|
+
search,
|
|
18
|
+
scrub,
|
|
19
|
+
wake = () => {},
|
|
20
|
+
notify = () => {},
|
|
21
|
+
log = () => {},
|
|
22
|
+
}) {
|
|
23
|
+
const read = (id) => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(store.meta(`snippet:${id}`) || "null");
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const save = (snippet) => {
|
|
31
|
+
store.meta(`snippet:${snippet.id}`, JSON.stringify(snippet));
|
|
32
|
+
const ids = JSON.parse(store.meta("snippets") || "[]");
|
|
33
|
+
if (!ids.includes(snippet.id))
|
|
34
|
+
store.meta("snippets", JSON.stringify([...ids, snippet.id].slice(-200)));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 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 }) => {
|
|
39
|
+
const thread = store.getTask(threadId);
|
|
40
|
+
const folder = thread?.kind === "goal" ? thread.goal.workspace : null;
|
|
41
|
+
if (!folder)
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error:
|
|
45
|
+
"this thread has no folder on the person's computer to show code from",
|
|
46
|
+
};
|
|
47
|
+
const words = String(about || "")
|
|
48
|
+
.trim()
|
|
49
|
+
.slice(0, 500);
|
|
50
|
+
if (!words) return { ok: false, error: "say what you need to see" };
|
|
51
|
+
const found = await search({ query: words, folder, limit: 1 });
|
|
52
|
+
const file = found?.ok ? found.files[0] : null;
|
|
53
|
+
if (!file)
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: found?.error || "nothing in this thread's folder matched that",
|
|
57
|
+
};
|
|
58
|
+
const root = repositoryOf(folder)?.root ?? folder;
|
|
59
|
+
let lines;
|
|
60
|
+
try {
|
|
61
|
+
lines = fs.readFileSync(path.join(root, file.path), "utf8").split("\n");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: `${file.path} could not be read (${error.code || error.message})`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const from = Math.max(1, file.line - BEFORE),
|
|
69
|
+
to = Math.min(lines.length, from + LINES - 1);
|
|
70
|
+
// A secret in the lines stays a placeholder for good: it is never filled back in for an agent elsewhere.
|
|
71
|
+
const { text, masked } = await scrub(lines.slice(from - 1, to).join("\n"));
|
|
72
|
+
const snippet = {
|
|
73
|
+
id: randomUUID(),
|
|
74
|
+
thread: threadId,
|
|
75
|
+
agent,
|
|
76
|
+
about: words,
|
|
77
|
+
path: file.path,
|
|
78
|
+
from,
|
|
79
|
+
to,
|
|
80
|
+
text,
|
|
81
|
+
masked,
|
|
82
|
+
state: "waiting",
|
|
83
|
+
at: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
save(snippet);
|
|
86
|
+
store.roomPost({
|
|
87
|
+
goalId: threadId,
|
|
88
|
+
author: "harness",
|
|
89
|
+
kind: "note",
|
|
90
|
+
text: `${nameOf(agent)} asks to see lines ${from} to ${to} of ${file.path} ("${words.slice(0, 120)}"). They stay on this computer until you share them.`,
|
|
91
|
+
});
|
|
92
|
+
notify(
|
|
93
|
+
`${nameOf(agent)} asks to see ${to - from + 1} lines of ${path.basename(file.path)}. Share or keep them in Harness.`,
|
|
94
|
+
snippet
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
snippet: { id: snippet.id, path: file.path, from, to, state: "waiting" },
|
|
99
|
+
note: "The person decides whether to share these lines; you are told in the thread.",
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** The person shares or keeps a waiting snippet. */
|
|
104
|
+
const decide = (id, share) => {
|
|
105
|
+
const snippet = read(id);
|
|
106
|
+
if (!snippet) return { ok: false, error: "no such request" };
|
|
107
|
+
if (snippet.state !== "waiting")
|
|
108
|
+
return { ok: false, error: `already ${snippet.state}` };
|
|
109
|
+
const done = {
|
|
110
|
+
...snippet,
|
|
111
|
+
state: share ? "shared" : "kept",
|
|
112
|
+
decidedAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
save(done);
|
|
115
|
+
if (share) {
|
|
116
|
+
const post = store.roomPost({
|
|
117
|
+
goalId: snippet.thread,
|
|
118
|
+
author: "harness",
|
|
119
|
+
kind: "post",
|
|
120
|
+
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\`\`\``,
|
|
122
|
+
});
|
|
123
|
+
wake(snippet.thread, post, [snippet.agent]);
|
|
124
|
+
} else
|
|
125
|
+
store.roomPost({
|
|
126
|
+
goalId: snippet.thread,
|
|
127
|
+
author: "harness",
|
|
128
|
+
kind: "note",
|
|
129
|
+
text: `The person kept lines ${snippet.from} to ${snippet.to} of ${snippet.path} on this computer.`,
|
|
130
|
+
});
|
|
131
|
+
log(`snippet ${id}: ${done.state}`);
|
|
132
|
+
return { ok: true, snippet: { id, state: done.state } };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const list = ({ all = false } = {}) =>
|
|
136
|
+
JSON.parse(store.meta("snippets") || "[]")
|
|
137
|
+
.map(read)
|
|
138
|
+
.filter((snippet) => snippet && (all || snippet.state === "waiting"));
|
|
139
|
+
return { request, decide, list };
|
|
140
|
+
}
|