agentgather 0.1.0
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/LICENSE +21 -0
- package/README.md +418 -0
- package/SECURITY.md +104 -0
- package/dist/src/auth/index.js +1 -0
- package/dist/src/auth/tokens.js +12 -0
- package/dist/src/browser/room.css +666 -0
- package/dist/src/browser/room.html +80 -0
- package/dist/src/browser/room.js +435 -0
- package/dist/src/cli/args.js +29 -0
- package/dist/src/cli/commands/attend/index.js +26 -0
- package/dist/src/cli/commands/broker/index.js +61 -0
- package/dist/src/cli/commands/doctor/index.js +93 -0
- package/dist/src/cli/commands/export/index.js +42 -0
- package/dist/src/cli/commands/handoff/index.js +41 -0
- package/dist/src/cli/commands/instructions/index.js +7 -0
- package/dist/src/cli/commands/message/index.js +50 -0
- package/dist/src/cli/commands/message/transport.js +108 -0
- package/dist/src/cli/commands/room/index.js +350 -0
- package/dist/src/cli/commands/tunnel/index.js +131 -0
- package/dist/src/cli/commands/watch/index.js +16 -0
- package/dist/src/cli/context.js +9 -0
- package/dist/src/cli/help.js +53 -0
- package/dist/src/cli/index.js +63 -0
- package/dist/src/cli/state.js +40 -0
- package/dist/src/protocol/attendance.js +20 -0
- package/dist/src/protocol/index.js +7 -0
- package/dist/src/protocol/instructions.js +29 -0
- package/dist/src/protocol/mentions.js +48 -0
- package/dist/src/protocol/messages.js +71 -0
- package/dist/src/protocol/types.js +1 -0
- package/dist/src/protocol/urls.js +9 -0
- package/dist/src/protocol/validation.js +21 -0
- package/dist/src/server/errors.js +12 -0
- package/dist/src/server/http.js +583 -0
- package/dist/src/server/index.js +2 -0
- package/dist/src/server/wait.js +44 -0
- package/dist/src/storage/index.js +4 -0
- package/dist/src/storage/lock.js +93 -0
- package/dist/src/storage/paths.js +18 -0
- package/dist/src/storage/room-store.js +302 -0
- package/dist/src/storage/secure-fs.js +28 -0
- package/dist/src/tunnel/broker.js +440 -0
- package/dist/src/tunnel/client.js +144 -0
- package/dist/src/tunnel/forwarding.js +176 -0
- package/dist/src/tunnel/host-session.js +133 -0
- package/dist/src/tunnel/index.js +8 -0
- package/dist/src/tunnel/limits.js +81 -0
- package/dist/src/tunnel/logging.js +70 -0
- package/dist/src/tunnel/protocol.js +46 -0
- package/dist/src/tunnel/relay.js +106 -0
- package/docs/FOUNDING-TICKETS.md +759 -0
- package/docs/PROPOSAL.md +2120 -0
- package/docs/agentgather-dev-deployment-guide.md +305 -0
- package/docs/agentgather-dev-tunnel-architecture.md +349 -0
- package/docs/deploy-rooms-agentgather-dev.md +152 -0
- package/docs/dogfood/release-dogfood.md +61 -0
- package/docs/dogfood/sanitized-room-log.jsonl +6 -0
- package/docs/host-guide.md +282 -0
- package/docs/operator-runbook.md +248 -0
- package/docs/remote-exposure.md +269 -0
- package/docs/room-brief-and-attend-card.md +110 -0
- package/package.json +49 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { flagBoolean, parseArgs } from "../../args.js";
|
|
3
|
+
import { readCurrent, tokensPath } from "../../state.js";
|
|
4
|
+
import { readRoomState, roomPaths } from "../../../storage/index.js";
|
|
5
|
+
export async function runDoctorCommand(argv, context) {
|
|
6
|
+
const args = parseArgs(argv);
|
|
7
|
+
const checks = [];
|
|
8
|
+
const current = await readCurrentSafe(context.home);
|
|
9
|
+
checks.push({
|
|
10
|
+
name: "current-room",
|
|
11
|
+
ok: current !== null,
|
|
12
|
+
message: current === null ? "no current room; run agentgather room join or room start" : `room=${current.roomId} alias=${current.alias}`
|
|
13
|
+
});
|
|
14
|
+
if (current !== null) {
|
|
15
|
+
const paths = roomPaths(context.home, current.roomId);
|
|
16
|
+
checks.push(await fileCheck("room-state", paths.state));
|
|
17
|
+
checks.push(await fileCheck("messages-log", paths.messages));
|
|
18
|
+
checks.push(await fileCheck("participants", paths.participants));
|
|
19
|
+
checks.push(await fileCheck("token-store", tokensPath(context.home, current.roomId)));
|
|
20
|
+
checks.push(await lockCheck(paths.lock));
|
|
21
|
+
checks.push(await roomStateCheck(paths));
|
|
22
|
+
checks.push(await serverCheck(current.baseUrl, current.token));
|
|
23
|
+
}
|
|
24
|
+
const ok = checks.every((check) => check.ok);
|
|
25
|
+
if (flagBoolean(args, "json")) {
|
|
26
|
+
context.stdout.write(`${JSON.stringify({ ok, checks })}\n`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
for (const check of checks) {
|
|
30
|
+
context.stdout.write(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ok ? 0 : 1;
|
|
34
|
+
}
|
|
35
|
+
async function readCurrentSafe(home) {
|
|
36
|
+
try {
|
|
37
|
+
return await readCurrent(home);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (isNotFound(error))
|
|
41
|
+
return null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function fileCheck(name, file) {
|
|
46
|
+
try {
|
|
47
|
+
await stat(file);
|
|
48
|
+
return { name, ok: true, message: "present" };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return { name, ok: false, message: isNotFound(error) ? "missing" : errorMessage(error) };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function lockCheck(lockPath) {
|
|
55
|
+
try {
|
|
56
|
+
await stat(lockPath);
|
|
57
|
+
return { name: "writer-lock", ok: false, message: "lock file exists; another writer may be active or stale" };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { name: "writer-lock", ok: isNotFound(error), message: isNotFound(error) ? "clear" : errorMessage(error) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function roomStateCheck(paths) {
|
|
64
|
+
try {
|
|
65
|
+
const state = await readRoomState(paths);
|
|
66
|
+
return { name: "room-status", ok: true, message: state.status };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return { name: "room-status", ok: false, message: errorMessage(error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function serverCheck(baseUrl, token) {
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(new URL("/status", baseUrl), {
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bearer ${token}`
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
return { name: "room-server", ok: false, message: `HTTP ${response.status}` };
|
|
81
|
+
}
|
|
82
|
+
return { name: "room-server", ok: true, message: "reachable" };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return { name: "room-server", ok: false, message: "not reachable at current baseUrl" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isNotFound(error) {
|
|
89
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
90
|
+
}
|
|
91
|
+
function errorMessage(error) {
|
|
92
|
+
return error instanceof Error ? error.message : String(error);
|
|
93
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { flagBoolean, flagString, parseArgs } from "../../args.js";
|
|
4
|
+
import { readCurrent } from "../../state.js";
|
|
5
|
+
import { readBrief, readMessages, readParticipants, readRoomState, roomPaths } from "../../../storage/index.js";
|
|
6
|
+
export async function runExportCommand(argv, context) {
|
|
7
|
+
const args = parseArgs(argv);
|
|
8
|
+
const current = await readCurrent(context.home);
|
|
9
|
+
const output = flagString(args, "output") ?? path.join(process.cwd(), `${current.roomId}-export.md`);
|
|
10
|
+
const paths = roomPaths(context.home, current.roomId);
|
|
11
|
+
const [state, brief, participants, messages] = await Promise.all([
|
|
12
|
+
readRoomState(paths),
|
|
13
|
+
readBrief(context.home, current.roomId),
|
|
14
|
+
readParticipants(paths),
|
|
15
|
+
readMessages(context.home, current.roomId)
|
|
16
|
+
]);
|
|
17
|
+
const body = [
|
|
18
|
+
`# Agent Gather Room Export: ${state.id}`,
|
|
19
|
+
"",
|
|
20
|
+
`Status: ${state.status}`,
|
|
21
|
+
`Exported at: ${new Date().toISOString()}`,
|
|
22
|
+
`Brief version: ${brief.brief_version}`,
|
|
23
|
+
"",
|
|
24
|
+
"## Room Brief",
|
|
25
|
+
"",
|
|
26
|
+
brief.body || "(empty)",
|
|
27
|
+
"",
|
|
28
|
+
"## Participants",
|
|
29
|
+
"",
|
|
30
|
+
...participants.map((participant) => `- ${participant.alias}: ${participant.kind}, ${participant.location}, ${participant.install}, ${participant.attention}, last_seen=${participant.lastSeenAt}`),
|
|
31
|
+
"",
|
|
32
|
+
"## Messages",
|
|
33
|
+
"",
|
|
34
|
+
...messages.map((message) => `- [#${message.id}] ${message.ts} ${message.from} (${message.type}): ${message.text}`)
|
|
35
|
+
].join("\n");
|
|
36
|
+
await writeFile(output, `${body}\n`);
|
|
37
|
+
return emit(context, flagBoolean(args, "json"), { ok: true, room: current.roomId, output, messages: messages.length }, `Exported ${messages.length} messages to ${output}\n`);
|
|
38
|
+
}
|
|
39
|
+
function emit(context, json, value, text) {
|
|
40
|
+
context.stdout.write(json ? `${JSON.stringify(value)}\n` : text);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { flagBoolean, flagString, parseArgs } from "../../args.js";
|
|
3
|
+
import { sendMessage } from "../message/transport.js";
|
|
4
|
+
export const MAX_HANDOFF_SUMMARY_LENGTH = 12_000;
|
|
5
|
+
export async function runHandoffCommand(argv, context) {
|
|
6
|
+
const args = parseArgs(argv);
|
|
7
|
+
const target = args.positional[0];
|
|
8
|
+
const summarySource = flagString(args, "summary");
|
|
9
|
+
if (target === undefined || summarySource === undefined) {
|
|
10
|
+
throw new Error("handoff requires <alias> and --summary");
|
|
11
|
+
}
|
|
12
|
+
const summary = await readSummary(summarySource);
|
|
13
|
+
if (summary.length > MAX_HANDOFF_SUMMARY_LENGTH) {
|
|
14
|
+
throw new Error(`handoff summary must be <= ${MAX_HANDOFF_SUMMARY_LENGTH} characters`);
|
|
15
|
+
}
|
|
16
|
+
const input = {
|
|
17
|
+
type: "handoff",
|
|
18
|
+
text: `@${target} HANDOFF\n\n${summary}`
|
|
19
|
+
};
|
|
20
|
+
const result = await sendMessage(context, input);
|
|
21
|
+
if (flagBoolean(args, "json")) {
|
|
22
|
+
context.stdout.write(`${JSON.stringify(result)}\n`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
context.stdout.write(`handoff #${result.message.id} sent to @${target}\n`);
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
async function readSummary(source) {
|
|
30
|
+
if (source.length > MAX_HANDOFF_SUMMARY_LENGTH)
|
|
31
|
+
return source;
|
|
32
|
+
try {
|
|
33
|
+
return await readFile(source, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
37
|
+
return source;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { parseAgentKind, renderAgentInstructions } from "../../../protocol/index.js";
|
|
2
|
+
import { flagString, parseArgs } from "../../args.js";
|
|
3
|
+
export async function runInstructionsCommand(argv, context) {
|
|
4
|
+
const args = parseArgs(argv);
|
|
5
|
+
context.stdout.write(`${renderAgentInstructions(parseAgentKind(flagString(args, "agent")))}\n`);
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { flagBoolean, flagString, parseArgs } from "../../args.js";
|
|
2
|
+
import { currentSinceId, formatMessages, listMessages, parseSinceId, readAndStoreCursor, sendMessage } from "./transport.js";
|
|
3
|
+
export async function runSendCommand(argv, context) {
|
|
4
|
+
const args = parseArgs(argv);
|
|
5
|
+
const target = args.positional[0];
|
|
6
|
+
const text = flagString(args, "text") ?? args.positional.slice(1).join(" ");
|
|
7
|
+
if (target === undefined || text.length === 0) {
|
|
8
|
+
throw new Error("send requires <alias> and message text");
|
|
9
|
+
}
|
|
10
|
+
const clientMsgId = flagString(args, "client-msg-id");
|
|
11
|
+
const input = {
|
|
12
|
+
text: `@${target} ${text}`,
|
|
13
|
+
...(clientMsgId === undefined ? {} : { client_msg_id: clientMsgId })
|
|
14
|
+
};
|
|
15
|
+
const result = await sendMessage(context, input);
|
|
16
|
+
return emit(context, flagBoolean(args, "json"), result, `sent #${result.message.id} to @${target}\n`);
|
|
17
|
+
}
|
|
18
|
+
export async function runReplyCommand(argv, context) {
|
|
19
|
+
const args = parseArgs(argv);
|
|
20
|
+
const replyTo = args.positional[0] === undefined ? undefined : parseSinceId(args.positional[0]);
|
|
21
|
+
const text = flagString(args, "text") ?? args.positional.slice(1).join(" ");
|
|
22
|
+
if (replyTo === undefined || text.length === 0) {
|
|
23
|
+
throw new Error("reply requires <message_id> and message text");
|
|
24
|
+
}
|
|
25
|
+
const clientMsgId = flagString(args, "client-msg-id");
|
|
26
|
+
const input = {
|
|
27
|
+
type: "reply",
|
|
28
|
+
text,
|
|
29
|
+
reply_to: replyTo,
|
|
30
|
+
...(clientMsgId === undefined ? {} : { client_msg_id: clientMsgId })
|
|
31
|
+
};
|
|
32
|
+
const result = await sendMessage(context, input);
|
|
33
|
+
return emit(context, flagBoolean(args, "json"), result, `replied #${result.message.id} to #${replyTo}\n`);
|
|
34
|
+
}
|
|
35
|
+
export async function runMessagesCommand(argv, context) {
|
|
36
|
+
const args = parseArgs(argv);
|
|
37
|
+
const sinceId = await currentSinceId(context, flagString(args, "since"));
|
|
38
|
+
const result = await listMessages(context, sinceId);
|
|
39
|
+
return emit(context, flagBoolean(args, "json"), result, formatMessages(result.messages));
|
|
40
|
+
}
|
|
41
|
+
export async function runReadCommand(argv, context) {
|
|
42
|
+
const args = parseArgs(argv);
|
|
43
|
+
const sinceId = await currentSinceId(context, flagString(args, "since"));
|
|
44
|
+
const result = await readAndStoreCursor(context, sinceId);
|
|
45
|
+
return emit(context, flagBoolean(args, "json"), result, formatMessages(result.messages));
|
|
46
|
+
}
|
|
47
|
+
function emit(context, json, value, text) {
|
|
48
|
+
context.stdout.write(json ? `${JSON.stringify(value)}\n` : text);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { appendMessageResult, readCursor, readMessages, writeCursor } from "../../../storage/index.js";
|
|
2
|
+
import { readCurrent } from "../../state.js";
|
|
3
|
+
export async function currentSinceId(context, rawSince) {
|
|
4
|
+
if (rawSince !== undefined)
|
|
5
|
+
return parseSinceId(rawSince);
|
|
6
|
+
const current = await readCurrent(context.home);
|
|
7
|
+
return readCursor(context.home, current.roomId, current.alias);
|
|
8
|
+
}
|
|
9
|
+
export async function sendMessage(context, input) {
|
|
10
|
+
const current = await readCurrent(context.home);
|
|
11
|
+
const sent = await requestJson(current, "/messages", "POST", input);
|
|
12
|
+
if (sent !== null)
|
|
13
|
+
return sent;
|
|
14
|
+
const result = await appendMessageResult({
|
|
15
|
+
root: context.home,
|
|
16
|
+
roomId: current.roomId,
|
|
17
|
+
from: current.alias,
|
|
18
|
+
input
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
message: result.message,
|
|
23
|
+
...(result.idempotent ? { idempotent: true } : {})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function listMessages(context, sinceId) {
|
|
27
|
+
const current = await readCurrent(context.home);
|
|
28
|
+
const path = `/messages?since_id=${sinceId}`;
|
|
29
|
+
const remote = await requestJson(current, path, "GET");
|
|
30
|
+
if (remote !== null) {
|
|
31
|
+
return {
|
|
32
|
+
...remote,
|
|
33
|
+
next_cmd: `agentgather messages --since ${remote.next_since_id} --json`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const messages = (await readMessages(context.home, current.roomId)).filter((message) => message.id > sinceId);
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
messages,
|
|
40
|
+
next_since_id: messages.at(-1)?.id ?? sinceId,
|
|
41
|
+
next_cmd: `agentgather messages --since ${messages.at(-1)?.id ?? sinceId} --json`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function readAndStoreCursor(context, sinceId) {
|
|
45
|
+
const current = await readCurrent(context.home);
|
|
46
|
+
const result = await listMessages(context, sinceId);
|
|
47
|
+
await writeCursor(context.home, current.roomId, current.alias, result.next_since_id);
|
|
48
|
+
return {
|
|
49
|
+
...result,
|
|
50
|
+
next_cmd: `agentgather read --since ${result.next_since_id} --json`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function waitOnce(context, sinceId) {
|
|
54
|
+
const current = await readCurrent(context.home);
|
|
55
|
+
const path = `/wait?participant=${current.alias}&since_id=${sinceId}`;
|
|
56
|
+
const response = await requestJson(current, path, "GET");
|
|
57
|
+
if (response === null)
|
|
58
|
+
throw new Error("room server is not reachable for watch");
|
|
59
|
+
await writeCursor(context.home, current.roomId, current.alias, response.next_since_id);
|
|
60
|
+
return {
|
|
61
|
+
...response,
|
|
62
|
+
cli_next_cmd: response.keep_waiting ? `agentgather watch --since ${response.next_since_id} --json` : null
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function formatMessages(messages) {
|
|
66
|
+
if (messages.length === 0)
|
|
67
|
+
return "(no messages)\n";
|
|
68
|
+
return messages.map((message) => `${message.id} ${message.ts} ${message.from}: ${message.text}`).join("\n") + "\n";
|
|
69
|
+
}
|
|
70
|
+
export function parseSinceId(raw) {
|
|
71
|
+
const sinceId = Number(raw);
|
|
72
|
+
if (!Number.isSafeInteger(sinceId) || sinceId < 0) {
|
|
73
|
+
throw new Error("since must be a non-negative safe integer");
|
|
74
|
+
}
|
|
75
|
+
return sinceId;
|
|
76
|
+
}
|
|
77
|
+
async function requestJson(current, path, method, body) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(new URL(path, current.baseUrl), {
|
|
80
|
+
method,
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${current.token}`,
|
|
83
|
+
"Content-Type": "application/json"
|
|
84
|
+
},
|
|
85
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) })
|
|
86
|
+
});
|
|
87
|
+
const payload = await readResponseJson(response);
|
|
88
|
+
if (response.status === 404)
|
|
89
|
+
return null;
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(payload.message ?? `request failed with HTTP ${response.status}`);
|
|
92
|
+
}
|
|
93
|
+
return payload;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof TypeError)
|
|
97
|
+
return null;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function readResponseJson(response) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(await response.text());
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|