tryharness 0.7.23 → 0.7.26
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 +5268 -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 +384 -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/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,30 +151,58 @@ 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
|
|
122
202
|
// it finished; 255 MB with it off, image-redaction eval).
|
|
123
|
-
fs.writeFileSync(
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
plist,
|
|
205
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
124
206
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
125
207
|
<plist version="1.0"><dict>
|
|
126
208
|
<key>Label</key><string>${LABEL}</string>
|
|
@@ -128,49 +210,103 @@ export function installService({ runtimeRoot, nodePath = process.execPath, platf
|
|
|
128
210
|
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(envPath)}</string><key>MallocLargeCache</key><string>0</string></dict>
|
|
129
211
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer>
|
|
130
212
|
<key>StandardOutPath</key><string>${xml(paths.log)}</string><key>StandardErrorPath</key><string>${xml(paths.log)}</string>
|
|
131
|
-
</dict></plist>\n`,
|
|
213
|
+
</dict></plist>\n`,
|
|
214
|
+
{ mode: 0o600 }
|
|
215
|
+
);
|
|
132
216
|
const domain = `gui/${process.getuid()}`;
|
|
133
217
|
run("launchctl", ["bootout", domain, plist]);
|
|
134
218
|
const started = run("launchctl", ["bootstrap", domain, plist]);
|
|
135
|
-
if (!started.ok)
|
|
219
|
+
if (!started.ok)
|
|
220
|
+
throw new Error(
|
|
221
|
+
`could not start Harness daemon with launchd: ${started.stderr || started.error}`
|
|
222
|
+
);
|
|
136
223
|
run("launchctl", ["kickstart", "-k", `${domain}/${LABEL}`]);
|
|
137
224
|
return { manager: "launchd", file: plist };
|
|
138
225
|
}
|
|
139
226
|
if (platform === "linux") {
|
|
140
227
|
const unit = servicePath();
|
|
141
228
|
ensureDir(path.dirname(unit));
|
|
142
|
-
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
|
+
);
|
|
143
234
|
const reload = run("systemctl", ["--user", "daemon-reload"]);
|
|
144
|
-
const started = run("systemctl", [
|
|
145
|
-
|
|
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
|
+
);
|
|
146
245
|
return { manager: "systemd", file: unit };
|
|
147
246
|
}
|
|
148
247
|
// Other Node platforms get a ready daemon for this login only. `doctor` reports that
|
|
149
248
|
// login persistence is unavailable rather than pretending it was set.
|
|
150
|
-
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
|
+
});
|
|
151
258
|
child.unref();
|
|
152
259
|
return { manager: "detached", file: null };
|
|
153
260
|
}
|
|
154
261
|
|
|
155
|
-
export function uninstallService({
|
|
262
|
+
export function uninstallService({
|
|
263
|
+
platform = process.platform,
|
|
264
|
+
execute = run,
|
|
265
|
+
} = {}) {
|
|
156
266
|
const file = servicePath(platform);
|
|
157
|
-
if (platform === "win32") {
|
|
267
|
+
if (platform === "win32") {
|
|
268
|
+
uninstallWindowsTask({ execute });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
158
271
|
if (platform === "darwin" && file) {
|
|
159
272
|
run("launchctl", ["bootout", `gui/${process.getuid()}`, file]);
|
|
160
|
-
try {
|
|
273
|
+
try {
|
|
274
|
+
fs.unlinkSync(file);
|
|
275
|
+
} catch {}
|
|
161
276
|
} else if (platform === "linux" && file) {
|
|
162
|
-
run("systemctl", [
|
|
163
|
-
|
|
277
|
+
run("systemctl", [
|
|
278
|
+
"--user",
|
|
279
|
+
"disable",
|
|
280
|
+
"--now",
|
|
281
|
+
"tryharness-daemon.service",
|
|
282
|
+
]);
|
|
283
|
+
try {
|
|
284
|
+
fs.unlinkSync(file);
|
|
285
|
+
} catch {}
|
|
164
286
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
165
287
|
}
|
|
166
288
|
}
|
|
167
289
|
|
|
168
|
-
export function serviceInfo({
|
|
290
|
+
export function serviceInfo({
|
|
291
|
+
platform = process.platform,
|
|
292
|
+
execute = run,
|
|
293
|
+
} = {}) {
|
|
169
294
|
const file = servicePath(platform);
|
|
170
295
|
if (platform === "win32") {
|
|
171
296
|
// Installed means the task is registered, not merely that its files exist.
|
|
172
|
-
const registered =
|
|
297
|
+
const registered =
|
|
298
|
+
Boolean(file && fs.existsSync(file)) &&
|
|
299
|
+
execute("schtasks", ["/Query", "/TN", WINDOWS_TASK]).ok;
|
|
173
300
|
return { manager: "task-scheduler", installed: registered, file };
|
|
174
301
|
}
|
|
175
|
-
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
|
+
};
|
|
176
312
|
}
|
package/lib/snippets.mjs
CHANGED
|
@@ -12,53 +12,129 @@ import { repositoryOf } from "./worktrees.mjs";
|
|
|
12
12
|
const BEFORE = 5;
|
|
13
13
|
const LINES = 40;
|
|
14
14
|
|
|
15
|
-
export function createSnippets({
|
|
16
|
-
|
|
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
|
+
};
|
|
17
30
|
const save = (snippet) => {
|
|
18
31
|
store.meta(`snippet:${snippet.id}`, JSON.stringify(snippet));
|
|
19
32
|
const ids = JSON.parse(store.meta("snippets") || "[]");
|
|
20
|
-
if (!ids.includes(snippet.id))
|
|
33
|
+
if (!ids.includes(snippet.id))
|
|
34
|
+
store.meta("snippets", JSON.stringify([...ids, snippet.id].slice(-200)));
|
|
21
35
|
};
|
|
22
36
|
|
|
23
37
|
/** A cloud agent's request: { ok, snippet } held for the person, or why there is nothing to hold. */
|
|
24
38
|
const request = async ({ threadId, agent, about }) => {
|
|
25
39
|
const thread = store.getTask(threadId);
|
|
26
40
|
const folder = thread?.kind === "goal" ? thread.goal.workspace : null;
|
|
27
|
-
if (!folder)
|
|
28
|
-
|
|
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);
|
|
29
50
|
if (!words) return { ok: false, error: "say what you need to see" };
|
|
30
51
|
const found = await search({ query: words, folder, limit: 1 });
|
|
31
52
|
const file = found?.ok ? found.files[0] : null;
|
|
32
|
-
if (!file)
|
|
53
|
+
if (!file)
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: found?.error || "nothing in this thread's folder matched that",
|
|
57
|
+
};
|
|
33
58
|
const root = repositoryOf(folder)?.root ?? folder;
|
|
34
59
|
let lines;
|
|
35
|
-
try {
|
|
36
|
-
|
|
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);
|
|
37
70
|
// A secret in the lines stays a placeholder for good: it is never filled back in for an agent elsewhere.
|
|
38
71
|
const { text, masked } = await scrub(lines.slice(from - 1, to).join("\n"));
|
|
39
|
-
const snippet = {
|
|
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
|
+
};
|
|
40
85
|
save(snippet);
|
|
41
|
-
store.roomPost({
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
};
|
|
44
101
|
};
|
|
45
102
|
|
|
46
103
|
/** The person shares or keeps a waiting snippet. */
|
|
47
104
|
const decide = (id, share) => {
|
|
48
105
|
const snippet = read(id);
|
|
49
106
|
if (!snippet) return { ok: false, error: "no such request" };
|
|
50
|
-
if (snippet.state !== "waiting")
|
|
51
|
-
|
|
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
|
+
};
|
|
52
114
|
save(done);
|
|
53
115
|
if (share) {
|
|
54
|
-
const post = store.roomPost({
|
|
55
|
-
|
|
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
|
+
});
|
|
56
123
|
wake(snippet.thread, post, [snippet.agent]);
|
|
57
|
-
} else
|
|
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
|
+
});
|
|
58
131
|
log(`snippet ${id}: ${done.state}`);
|
|
59
132
|
return { ok: true, snippet: { id, state: done.state } };
|
|
60
133
|
};
|
|
61
134
|
|
|
62
|
-
const list = ({ all = false } = {}) =>
|
|
135
|
+
const list = ({ all = false } = {}) =>
|
|
136
|
+
JSON.parse(store.meta("snippets") || "[]")
|
|
137
|
+
.map(read)
|
|
138
|
+
.filter((snippet) => snippet && (all || snippet.state === "waiting"));
|
|
63
139
|
return { request, decide, list };
|
|
64
140
|
}
|