@herdingchats/friday 0.1.663 → 0.1.670
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/herd.mjs +242 -7
- package/package.json +2 -2
package/herd.mjs
CHANGED
|
@@ -18,7 +18,7 @@ var __export = (target, all) => {
|
|
|
18
18
|
var define_HERD_PACKAGE_default;
|
|
19
19
|
var init_define_HERD_PACKAGE = __esm({
|
|
20
20
|
"<define:HERD_PACKAGE>"() {
|
|
21
|
-
define_HERD_PACKAGE_default = { version: "0.1.
|
|
21
|
+
define_HERD_PACKAGE_default = { version: "0.1.670", gitHead: "ec9dd13ab4eb2461db3f70f5798e1444a4ab281f" };
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -515,6 +515,9 @@ var init_terminals = __esm({
|
|
|
515
515
|
z6.object({ type: z6.literal("input"), data: z6.string().max(64 * 1024) }),
|
|
516
516
|
z6.object({ type: z6.literal("resize"), ...size }),
|
|
517
517
|
z6.object({ type: z6.literal("drive") }),
|
|
518
|
+
// The browser's socket is open, so the screen can be sent: frames sent before a browser's
|
|
519
|
+
// socket has finished opening can be lost on the way, so it asks once it's ready.
|
|
520
|
+
z6.object({ type: z6.literal("ready") }),
|
|
518
521
|
z6.object({ type: z6.literal("watch") }),
|
|
519
522
|
z6.object({
|
|
520
523
|
type: z6.literal("closed"),
|
|
@@ -611,6 +614,7 @@ var init_workers = __esm({
|
|
|
611
614
|
"use strict";
|
|
612
615
|
init_define_HERD_PACKAGE();
|
|
613
616
|
init_activation();
|
|
617
|
+
init_terminals();
|
|
614
618
|
CAPABILITIES = [
|
|
615
619
|
"macos",
|
|
616
620
|
"linux",
|
|
@@ -685,6 +689,7 @@ var init_workers = __esm({
|
|
|
685
689
|
avoid: phrases.optional(),
|
|
686
690
|
harnesses: z8.record(z8.string().regex(/^[a-z][a-z0-9-]*$/), harnessConfigSchema).optional(),
|
|
687
691
|
builds: buildsSettingSchema.optional(),
|
|
692
|
+
terminals: z8.object({ remote: terminalAccessSchema.optional() }).strict().optional(),
|
|
688
693
|
repos: z8.array(configRepoSchema).max(CONFIG_REPOS_MAX, `at most ${CONFIG_REPOS_MAX} repos`).optional()
|
|
689
694
|
}).strict();
|
|
690
695
|
HERD_CONFIG_TYPE = `type HerdConfig = {
|
|
@@ -698,6 +703,11 @@ var init_workers = __esm({
|
|
|
698
703
|
* a build runner with its owner's approval, and a registered runner without it takes one.
|
|
699
704
|
*/
|
|
700
705
|
builds?: true | { max: number };
|
|
706
|
+
/**
|
|
707
|
+
* What the web may do with this machine's terminals: "drive" (the default) to watch and type,
|
|
708
|
+
* "watch" to only watch, or "off".
|
|
709
|
+
*/
|
|
710
|
+
terminals?: { remote?: "drive" | "watch" | "off" };
|
|
701
711
|
/**
|
|
702
712
|
* Repos the owner works in, so \`herd repos\` can tell an agent which one "the frontend" is. At
|
|
703
713
|
* most ${CONFIG_REPOS_MAX}. Listing one clones nothing, and agents can add repos that aren't listed.
|
|
@@ -4524,7 +4534,9 @@ function registration(config, clis, facts = {}, cloned = []) {
|
|
|
4524
4534
|
...facts.configError ? { configError: facts.configError.slice(0, 1e3) } : {},
|
|
4525
4535
|
...facts.gh ? { gh: facts.gh } : {},
|
|
4526
4536
|
...facts.folderUnavailable ? { folderUnavailable: facts.folderUnavailable } : {},
|
|
4527
|
-
herdVersion: HERD_VERSION
|
|
4537
|
+
herdVersion: HERD_VERSION,
|
|
4538
|
+
// Saying what it allows also says this daemon dials terminal sessions.
|
|
4539
|
+
terminals: { remote: facts.config?.terminals?.remote ?? "drive" }
|
|
4528
4540
|
};
|
|
4529
4541
|
}
|
|
4530
4542
|
function describeCli(name, version) {
|
|
@@ -10397,6 +10409,7 @@ var Daemon = class {
|
|
|
10397
10409
|
async onFrame(frame) {
|
|
10398
10410
|
if (frame.type === "offer") return this.onOffer(frame.offer);
|
|
10399
10411
|
if (frame.type === "sign_in") return this.options.onSignIn?.(frame);
|
|
10412
|
+
if (frame.type === "terminal_open") return this.options.onTerminalOpen?.(frame);
|
|
10400
10413
|
if (frame.type === "message") {
|
|
10401
10414
|
if (this.options.folderUnavailable?.()) {
|
|
10402
10415
|
if (this.held.get(frame.conversationId) !== "interrupt") {
|
|
@@ -10849,6 +10862,207 @@ var MachineChecks = class {
|
|
|
10849
10862
|
}
|
|
10850
10863
|
};
|
|
10851
10864
|
|
|
10865
|
+
// src/remote-terminal.ts
|
|
10866
|
+
init_define_HERD_PACKAGE();
|
|
10867
|
+
init_src();
|
|
10868
|
+
import { connect as connect3 } from "node:net";
|
|
10869
|
+
var BATCH_MS = 16;
|
|
10870
|
+
var BEHIND_BYTES = 1024 * 1024;
|
|
10871
|
+
var RemoteTerminals = class {
|
|
10872
|
+
constructor(options) {
|
|
10873
|
+
this.options = options;
|
|
10874
|
+
}
|
|
10875
|
+
options;
|
|
10876
|
+
open = /* @__PURE__ */ new Set();
|
|
10877
|
+
async opened(frame) {
|
|
10878
|
+
const { options } = this;
|
|
10879
|
+
const access = options.access();
|
|
10880
|
+
const resolved = await options.resolve(frame.target).catch((error) => ({
|
|
10881
|
+
refused: `herd couldn't find the terminal: ${error}`
|
|
10882
|
+
}));
|
|
10883
|
+
const refusal = access === "off" ? "This machine doesn't allow its terminals to be opened from the web." : "refused" in resolved ? resolved.refused : null;
|
|
10884
|
+
const mode = frame.mode === "drive" && access === "drive" ? "drive" : "watch";
|
|
10885
|
+
const target = await options.target(frame.sessionId);
|
|
10886
|
+
const Impl = options.WebSocketImpl ?? WebSocket;
|
|
10887
|
+
const socket = new Impl(target.url, { headers: target.headers });
|
|
10888
|
+
const session = new Session(socket, {
|
|
10889
|
+
...options,
|
|
10890
|
+
allowDrive: access === "drive",
|
|
10891
|
+
onEnd: () => this.open.delete(session)
|
|
10892
|
+
});
|
|
10893
|
+
this.open.add(session);
|
|
10894
|
+
socket.addEventListener("open", () => {
|
|
10895
|
+
if (refusal || "refused" in resolved) {
|
|
10896
|
+
session.refuse(refusal ?? "herd couldn't open that terminal.");
|
|
10897
|
+
return;
|
|
10898
|
+
}
|
|
10899
|
+
session.attach(resolved.terminal, resolved.socketPath, mode);
|
|
10900
|
+
});
|
|
10901
|
+
}
|
|
10902
|
+
/** Ends every session, as when the daemon stops. The terminals keep running. */
|
|
10903
|
+
stop() {
|
|
10904
|
+
for (const session of [...this.open]) session.end("machine_gone");
|
|
10905
|
+
}
|
|
10906
|
+
};
|
|
10907
|
+
var Session = class {
|
|
10908
|
+
constructor(socket, options) {
|
|
10909
|
+
this.socket = socket;
|
|
10910
|
+
this.options = options;
|
|
10911
|
+
socket.addEventListener("message", (event2) => this.fromSession(String(event2.data)));
|
|
10912
|
+
socket.addEventListener("close", () => this.end());
|
|
10913
|
+
socket.addEventListener("error", () => this.end());
|
|
10914
|
+
}
|
|
10915
|
+
socket;
|
|
10916
|
+
options;
|
|
10917
|
+
host = null;
|
|
10918
|
+
terminal = "";
|
|
10919
|
+
socketPath = "";
|
|
10920
|
+
pending = "";
|
|
10921
|
+
flushTimer = null;
|
|
10922
|
+
behind = false;
|
|
10923
|
+
drainTimer = null;
|
|
10924
|
+
ended = false;
|
|
10925
|
+
mode = "watch";
|
|
10926
|
+
refuse(detail2) {
|
|
10927
|
+
this.options.log?.(`A terminal session was refused: ${detail2}`);
|
|
10928
|
+
this.send({ type: "closed", reason: "refused", detail: detail2 });
|
|
10929
|
+
this.end();
|
|
10930
|
+
}
|
|
10931
|
+
/** Attaches to the terminal's host, watching or driving, which sends a fresh screen. */
|
|
10932
|
+
attach(terminal, socketPath, mode) {
|
|
10933
|
+
this.terminal = terminal;
|
|
10934
|
+
this.socketPath = socketPath;
|
|
10935
|
+
this.mode = mode;
|
|
10936
|
+
this.detachHost();
|
|
10937
|
+
const host = connect3(socketPath);
|
|
10938
|
+
this.host = host;
|
|
10939
|
+
let buffer = "";
|
|
10940
|
+
let answered = false;
|
|
10941
|
+
host.on(
|
|
10942
|
+
"connect",
|
|
10943
|
+
() => host.write(
|
|
10944
|
+
`${JSON.stringify(mode === "drive" ? { op: "attach", name: terminal } : { op: "watch", name: terminal })}
|
|
10945
|
+
`
|
|
10946
|
+
)
|
|
10947
|
+
);
|
|
10948
|
+
host.on("data", (chunk) => {
|
|
10949
|
+
buffer += chunk;
|
|
10950
|
+
let end = buffer.indexOf("\n");
|
|
10951
|
+
while (end >= 0) {
|
|
10952
|
+
const line2 = buffer.slice(0, end);
|
|
10953
|
+
buffer = buffer.slice(end + 1);
|
|
10954
|
+
end = buffer.indexOf("\n");
|
|
10955
|
+
let message;
|
|
10956
|
+
try {
|
|
10957
|
+
message = JSON.parse(line2);
|
|
10958
|
+
} catch {
|
|
10959
|
+
continue;
|
|
10960
|
+
}
|
|
10961
|
+
if (!answered) {
|
|
10962
|
+
answered = true;
|
|
10963
|
+
if (message.ok === false) {
|
|
10964
|
+
this.refuse(message.error ?? "The terminal's host wouldn't open it.");
|
|
10965
|
+
return;
|
|
10966
|
+
}
|
|
10967
|
+
this.send({ type: "mode", mode });
|
|
10968
|
+
if (message.ok === true) continue;
|
|
10969
|
+
}
|
|
10970
|
+
this.fromHost(message);
|
|
10971
|
+
}
|
|
10972
|
+
});
|
|
10973
|
+
host.on("error", (error) => {
|
|
10974
|
+
if (host === this.host)
|
|
10975
|
+
this.refuse(`The terminal's host couldn't be reached: ${error.message}`);
|
|
10976
|
+
});
|
|
10977
|
+
}
|
|
10978
|
+
fromHost(message) {
|
|
10979
|
+
if (message.type === "snapshot") {
|
|
10980
|
+
this.pending = "";
|
|
10981
|
+
this.send({ type: "snapshot", data: message.data ?? "" });
|
|
10982
|
+
} else if (message.type === "data") {
|
|
10983
|
+
if (this.behind) return;
|
|
10984
|
+
this.pending += message.data ?? "";
|
|
10985
|
+
this.flushTimer ??= setTimeout(() => this.flush(), this.options.batchMs ?? BATCH_MS);
|
|
10986
|
+
} else if (message.type === "exit") {
|
|
10987
|
+
this.flush();
|
|
10988
|
+
this.send({ type: "exit", exitCode: message.exitCode ?? null });
|
|
10989
|
+
this.end();
|
|
10990
|
+
}
|
|
10991
|
+
}
|
|
10992
|
+
flush() {
|
|
10993
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
10994
|
+
this.flushTimer = null;
|
|
10995
|
+
if (!this.pending) return;
|
|
10996
|
+
const data = this.pending;
|
|
10997
|
+
this.pending = "";
|
|
10998
|
+
this.send({ type: "data", data });
|
|
10999
|
+
if (this.socket.bufferedAmount > (this.options.behindBytes ?? BEHIND_BYTES)) {
|
|
11000
|
+
this.behind = true;
|
|
11001
|
+
this.drainTimer = setInterval(() => {
|
|
11002
|
+
if (this.socket.bufferedAmount > 0) return;
|
|
11003
|
+
if (this.drainTimer) clearInterval(this.drainTimer);
|
|
11004
|
+
this.drainTimer = null;
|
|
11005
|
+
this.behind = false;
|
|
11006
|
+
this.attach(this.terminal, this.socketPath, this.mode);
|
|
11007
|
+
}, this.options.drainPollMs ?? 100);
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
fromSession(raw) {
|
|
11011
|
+
let parsed;
|
|
11012
|
+
try {
|
|
11013
|
+
parsed = JSON.parse(raw);
|
|
11014
|
+
} catch {
|
|
11015
|
+
return;
|
|
11016
|
+
}
|
|
11017
|
+
const frame = terminalFrameSchema.safeParse(parsed);
|
|
11018
|
+
if (!frame.success) return;
|
|
11019
|
+
const { data } = frame;
|
|
11020
|
+
if (data.type === "closed") {
|
|
11021
|
+
this.end();
|
|
11022
|
+
return;
|
|
11023
|
+
}
|
|
11024
|
+
if (data.type === "drive" || data.type === "watch") {
|
|
11025
|
+
this.mode = data.type === "drive" && this.options.allowDrive ? "drive" : "watch";
|
|
11026
|
+
if (this.terminal) this.attach(this.terminal, this.socketPath, this.mode);
|
|
11027
|
+
return;
|
|
11028
|
+
}
|
|
11029
|
+
if (this.mode !== "drive" || !this.host) return;
|
|
11030
|
+
if (data.type === "input")
|
|
11031
|
+
this.host.write(`${JSON.stringify({ type: "input", data: data.data })}
|
|
11032
|
+
`);
|
|
11033
|
+
if (data.type === "resize") {
|
|
11034
|
+
this.host.write(`${JSON.stringify({ type: "resize", cols: data.cols, rows: data.rows })}
|
|
11035
|
+
`);
|
|
11036
|
+
}
|
|
11037
|
+
}
|
|
11038
|
+
send(frame) {
|
|
11039
|
+
if (this.socket.readyState !== WebSocket.OPEN) return;
|
|
11040
|
+
try {
|
|
11041
|
+
this.socket.send(JSON.stringify(frame));
|
|
11042
|
+
} catch {
|
|
11043
|
+
this.end();
|
|
11044
|
+
}
|
|
11045
|
+
}
|
|
11046
|
+
detachHost() {
|
|
11047
|
+
const host = this.host;
|
|
11048
|
+
this.host = null;
|
|
11049
|
+
host?.destroy();
|
|
11050
|
+
}
|
|
11051
|
+
end(reason) {
|
|
11052
|
+
if (this.ended) return;
|
|
11053
|
+
this.ended = true;
|
|
11054
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
11055
|
+
if (this.drainTimer) clearInterval(this.drainTimer);
|
|
11056
|
+
this.detachHost();
|
|
11057
|
+
if (reason) this.send({ type: "closed", reason });
|
|
11058
|
+
try {
|
|
11059
|
+
this.socket.close(1e3);
|
|
11060
|
+
} catch {
|
|
11061
|
+
}
|
|
11062
|
+
this.options.onEnd();
|
|
11063
|
+
}
|
|
11064
|
+
};
|
|
11065
|
+
|
|
10852
11066
|
// src/usage-reporter.ts
|
|
10853
11067
|
init_define_HERD_PACKAGE();
|
|
10854
11068
|
var USAGE_REPORT_MS = 5 * 60 * 1e3;
|
|
@@ -13933,13 +14147,33 @@ async function runDaemon(args) {
|
|
|
13933
14147
|
const api = apiUrl();
|
|
13934
14148
|
const terminals = new HostTerminals({ log: (line2) => console.log(line2) });
|
|
13935
14149
|
await terminals.ensureHost();
|
|
14150
|
+
const signIn = async () => isLocal(api) ? localSignIn() : { Authorization: `Bearer ${await accessToken()}` };
|
|
13936
14151
|
const daemonApi = {
|
|
13937
14152
|
fetch: apiFetch,
|
|
13938
14153
|
connection: async (machineId) => ({
|
|
13939
14154
|
url: `${api.replace(/^http/, "ws")}/api/machines/${encodeURIComponent(machineId)}/connect`,
|
|
13940
|
-
headers:
|
|
14155
|
+
headers: await signIn()
|
|
13941
14156
|
})
|
|
13942
14157
|
};
|
|
14158
|
+
const remoteTerminals = new RemoteTerminals({
|
|
14159
|
+
target: async (sessionId) => ({
|
|
14160
|
+
url: `${api.replace(/^http/, "ws")}/api/terminal-sessions/${sessionId}/machine?machine=${encodeURIComponent(loadConfig().machineId)}`,
|
|
14161
|
+
headers: await signIn()
|
|
14162
|
+
}),
|
|
14163
|
+
access: () => daemon?.configuration().config?.terminals?.remote ?? "drive",
|
|
14164
|
+
resolve: async (target) => {
|
|
14165
|
+
if (target.kind !== "conversation") {
|
|
14166
|
+
return { refused: "This machine can't open an agent's sign-in terminal from the web yet." };
|
|
14167
|
+
}
|
|
14168
|
+
const workspace = workspaces.get(target.conversationId);
|
|
14169
|
+
if (!workspace) return { refused: "This machine doesn't hold that conversation." };
|
|
14170
|
+
const lead = (await terminals.list()).find((t) => t.name === workspace.lead && !t.exited);
|
|
14171
|
+
if (!lead)
|
|
14172
|
+
return { refused: "The conversation's agent isn't running in a terminal right now." };
|
|
14173
|
+
return { terminal: lead.name, socketPath: await terminals.pathOf(lead.name) };
|
|
14174
|
+
},
|
|
14175
|
+
log: (line2) => console.log(line2)
|
|
14176
|
+
});
|
|
13943
14177
|
let activator = null;
|
|
13944
14178
|
let checks = null;
|
|
13945
14179
|
const log = (line2) => console.log(line2);
|
|
@@ -14066,6 +14300,7 @@ async function runDaemon(args) {
|
|
|
14066
14300
|
activations: () => activator?.states() ?? {},
|
|
14067
14301
|
gh: () => checks?.gh ?? null,
|
|
14068
14302
|
onSignIn: (frame) => void checks?.signIn(frame),
|
|
14303
|
+
onTerminalOpen: (frame) => void remoteTerminals.opened(frame).catch((error) => console.log(`A terminal session didn't open: ${error}`)),
|
|
14069
14304
|
onConfigChanged: () => checks?.activateChosen(),
|
|
14070
14305
|
folderUnavailable: () => folders.problem
|
|
14071
14306
|
});
|
|
@@ -14301,7 +14536,7 @@ async function runAgyHook(event2, stdin, env, options = {}) {
|
|
|
14301
14536
|
init_define_HERD_PACKAGE();
|
|
14302
14537
|
import { spawn as spawn13 } from "node:child_process";
|
|
14303
14538
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
14304
|
-
import { connect as
|
|
14539
|
+
import { connect as connect4 } from "node:net";
|
|
14305
14540
|
import { homedir as homedir9 } from "node:os";
|
|
14306
14541
|
import { join as join36 } from "node:path";
|
|
14307
14542
|
var MAX_INPUT = 64 * 1024;
|
|
@@ -14397,7 +14632,7 @@ function runOwnCommand(command, input, cwd, timeoutMs = OWN_COMMAND_TIMEOUT_MS)
|
|
|
14397
14632
|
}
|
|
14398
14633
|
function tellDaemon(path, request, timeoutMs = REPORT_TIMEOUT_MS) {
|
|
14399
14634
|
return new Promise((resolve8) => {
|
|
14400
|
-
const socket =
|
|
14635
|
+
const socket = connect4(path);
|
|
14401
14636
|
const done = () => {
|
|
14402
14637
|
clearTimeout(timer);
|
|
14403
14638
|
socket.destroy();
|
|
@@ -14680,11 +14915,11 @@ init_define_HERD_PACKAGE();
|
|
|
14680
14915
|
|
|
14681
14916
|
// src/attach.ts
|
|
14682
14917
|
init_define_HERD_PACKAGE();
|
|
14683
|
-
import { connect as
|
|
14918
|
+
import { connect as connect5 } from "node:net";
|
|
14684
14919
|
var DETACH_KEY = "";
|
|
14685
14920
|
function attach(name, { stdin, stdout } = { stdin: process.stdin, stdout: process.stdout }, path = currentHostSocketPath(), { watch: watch2 = false } = {}) {
|
|
14686
14921
|
return new Promise((resolve8, reject) => {
|
|
14687
|
-
const socket =
|
|
14922
|
+
const socket = connect5(path);
|
|
14688
14923
|
const size2 = () => ({ cols: stdout.columns || 120, rows: stdout.rows || 40 });
|
|
14689
14924
|
let buffer = "";
|
|
14690
14925
|
let started = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herdingchats/friday",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.670",
|
|
4
4
|
"description": "Friday, HerdingChats' agent daemon, and the herd command it runs agents with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"satori": "0.32.0",
|
|
23
23
|
"zod": "4.6.7"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "ec9dd13ab4eb2461db3f70f5798e1444a4ab281f"
|
|
26
26
|
}
|