agent-pager 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.
@@ -0,0 +1,51 @@
1
+ import { createHostedHttpHandlerFromEnv } from "./hosted-http.js";
2
+ let cachedHandler;
3
+ export async function handleVercelNodeRequest(req, res) {
4
+ try {
5
+ const handler = cachedHandler || (cachedHandler = createHostedHttpHandlerFromEnv());
6
+ const bodyText = await readBody(req);
7
+ const request = new Request(requestUrl(req), {
8
+ method: req.method || "GET",
9
+ headers: headersFromIncoming(req.headers),
10
+ body: allowsBody(req.method || "GET") ? bodyText : undefined
11
+ });
12
+ const response = await handler.handle(request);
13
+ res.statusCode = response.status;
14
+ response.headers.forEach((value, key) => res.setHeader(key, value));
15
+ res.end(Buffer.from(await response.arrayBuffer()));
16
+ }
17
+ catch (error) {
18
+ res.statusCode = 500;
19
+ res.setHeader("content-type", "application/json; charset=utf-8");
20
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "request failed" }));
21
+ }
22
+ }
23
+ function requestUrl(req) {
24
+ const host = req.headers.host || "localhost";
25
+ const forwardedProto = req.headers["x-forwarded-proto"];
26
+ const proto = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto || "https";
27
+ return new URL(req.url || "/", `${proto}://${host}`).toString();
28
+ }
29
+ function headersFromIncoming(headers) {
30
+ const out = new Headers();
31
+ for (const [key, value] of Object.entries(headers)) {
32
+ if (Array.isArray(value)) {
33
+ for (const child of value)
34
+ out.append(key, child);
35
+ }
36
+ else if (value !== undefined) {
37
+ out.set(key, value);
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ async function readBody(req) {
43
+ const chunks = [];
44
+ for await (const chunk of req) {
45
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
46
+ }
47
+ return Buffer.concat(chunks).toString("utf8");
48
+ }
49
+ function allowsBody(method) {
50
+ return !["GET", "HEAD"].includes(method.toUpperCase());
51
+ }
@@ -0,0 +1,28 @@
1
+ import { canonicalRequest, signText } from "./crypto.js";
2
+ import { loadLocalConfig } from "./local-config.js";
3
+ export async function apiRequest(path, options = {}) {
4
+ const config = options.config || loadLocalConfig();
5
+ const method = options.method || "GET";
6
+ const body = options.body === undefined ? "" : JSON.stringify(options.body);
7
+ const headers = {};
8
+ if (body) {
9
+ headers["content-type"] = "application/json";
10
+ }
11
+ if (options.signed !== false) {
12
+ const timestamp = new Date().toISOString();
13
+ headers["x-ap-device-id"] = config.deviceId;
14
+ headers["x-ap-timestamp"] = timestamp;
15
+ headers["x-ap-signature"] = signText(config.privateKeyPem, canonicalRequest(method, path, timestamp, body));
16
+ }
17
+ const response = await fetch(`${config.serverUrl}${path}`, {
18
+ method,
19
+ headers,
20
+ body: body || undefined
21
+ });
22
+ const text = await response.text();
23
+ const json = text ? JSON.parse(text) : null;
24
+ if (!response.ok) {
25
+ throw new Error(json?.error || `${response.status} ${response.statusText}`);
26
+ }
27
+ return json;
28
+ }
@@ -0,0 +1,29 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ export function configDir() {
5
+ return process.env.AGENT_PAGER_HOME || join(homedir(), ".agent-pager");
6
+ }
7
+ export function configPath() {
8
+ return resolve(configDir(), "config.json");
9
+ }
10
+ export function inboxPath() {
11
+ return resolve(configDir(), "inbox.jsonl");
12
+ }
13
+ export function loadLocalConfig() {
14
+ const path = configPath();
15
+ if (!existsSync(path)) {
16
+ throw new Error("not logged in. Run `agent-pager login --username <name>` first.");
17
+ }
18
+ return JSON.parse(readFileSync(path, "utf8"));
19
+ }
20
+ export function saveLocalConfig(config) {
21
+ const path = configPath();
22
+ mkdirSync(dirname(path), { recursive: true });
23
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
24
+ }
25
+ export function deleteLocalConfig() {
26
+ const path = configPath();
27
+ if (existsSync(path))
28
+ rmSync(path);
29
+ }
@@ -0,0 +1,341 @@
1
+ import readline from "node:readline";
2
+ import { apiRequest } from "./http-client.js";
3
+ import { loadLocalConfig } from "./local-config.js";
4
+ const tools = [
5
+ {
6
+ name: "page_friend",
7
+ description: "Send a concise page to an approved friend's active or most recent coding agent session.",
8
+ inputSchema: {
9
+ type: "object",
10
+ required: ["friend", "message"],
11
+ properties: {
12
+ friend: { type: "string", description: "Friend username to page." },
13
+ message: { type: "string", description: "Short action-oriented message. Do not include secrets." },
14
+ urgency: { type: "string", enum: ["gentle", "normal", "firm", "professionally-dramatic"] }
15
+ }
16
+ }
17
+ },
18
+ {
19
+ name: "reply_to_page",
20
+ description: "Reply to a received Agent Pager page.",
21
+ inputSchema: {
22
+ type: "object",
23
+ required: ["page_id", "message"],
24
+ properties: {
25
+ page_id: { type: "string" },
26
+ message: { type: "string" }
27
+ }
28
+ }
29
+ },
30
+ {
31
+ name: "list_friends",
32
+ description: "List approved Agent Pager friends and their limited reachable/busy/muted presence.",
33
+ inputSchema: { type: "object", properties: {} }
34
+ },
35
+ {
36
+ name: "get_share_link",
37
+ description: "Get this human's public Page my agent link. The link only creates friend requests for non-friends.",
38
+ inputSchema: { type: "object", properties: {} }
39
+ },
40
+ {
41
+ name: "request_friend",
42
+ description: "Create a pending friend request for a username only when the human asks to add or request access to that person.",
43
+ inputSchema: {
44
+ type: "object",
45
+ required: ["friend"],
46
+ properties: {
47
+ friend: { type: "string", description: "Username to request as a friend." },
48
+ note: { type: "string", description: "Optional short human-approved note." }
49
+ }
50
+ }
51
+ },
52
+ {
53
+ name: "list_friend_requests",
54
+ description: "List pending and resolved Agent Pager friend requests.",
55
+ inputSchema: { type: "object", properties: {} }
56
+ },
57
+ {
58
+ name: "approve_friend_request",
59
+ description: "Approve a pending friend request only when the human explicitly asks to approve that request.",
60
+ inputSchema: {
61
+ type: "object",
62
+ required: ["request_id"],
63
+ properties: {
64
+ request_id: { type: "string", description: "Friend request id to approve." }
65
+ }
66
+ }
67
+ },
68
+ {
69
+ name: "deny_friend_request",
70
+ description: "Deny a pending friend request only when the human explicitly asks to deny that request.",
71
+ inputSchema: {
72
+ type: "object",
73
+ required: ["request_id"],
74
+ properties: {
75
+ request_id: { type: "string", description: "Friend request id to deny." }
76
+ }
77
+ }
78
+ },
79
+ {
80
+ name: "cancel_friend_request",
81
+ description: "Cancel this user's outgoing pending friend request only when the human explicitly asks.",
82
+ inputSchema: {
83
+ type: "object",
84
+ required: ["request_id"],
85
+ properties: {
86
+ request_id: { type: "string", description: "Outgoing friend request id to cancel." }
87
+ }
88
+ }
89
+ },
90
+ {
91
+ name: "remove_friend",
92
+ description: "Remove an approved friendship only when the human explicitly asks to remove, unfriend, or end access for that friend.",
93
+ inputSchema: {
94
+ type: "object",
95
+ required: ["friend"],
96
+ properties: {
97
+ friend: { type: "string", description: "Friend username to remove." }
98
+ }
99
+ }
100
+ },
101
+ {
102
+ name: "check_pages",
103
+ description: "Check recent Agent Pager pages sent to or from this user.",
104
+ inputSchema: { type: "object", properties: {} }
105
+ },
106
+ {
107
+ name: "set_pager_status",
108
+ description: "Set Agent Pager presence status.",
109
+ inputSchema: {
110
+ type: "object",
111
+ required: ["status"],
112
+ properties: {
113
+ status: { type: "string", enum: ["online", "busy", "muted", "offline"] }
114
+ }
115
+ }
116
+ },
117
+ {
118
+ name: "mute_friend",
119
+ description: "Mute an approved friend so their pages are blocked for a duration or until changed.",
120
+ inputSchema: {
121
+ type: "object",
122
+ required: ["friend"],
123
+ properties: {
124
+ friend: { type: "string", description: "Friend username to mute." },
125
+ duration: { type: "string", description: "Duration such as 15m, 2h, 1d, or forever." },
126
+ reason: { type: "string", description: "Optional short reason for the audit log." }
127
+ }
128
+ }
129
+ },
130
+ {
131
+ name: "unmute_friend",
132
+ description: "Unmute a friend only when the human explicitly asks to allow their pages again.",
133
+ inputSchema: {
134
+ type: "object",
135
+ required: ["friend"],
136
+ properties: {
137
+ friend: { type: "string", description: "Friend username to unmute." }
138
+ }
139
+ }
140
+ },
141
+ {
142
+ name: "block_friend",
143
+ description: "Block a user from friend requests and pages only when the human explicitly asks for a block.",
144
+ inputSchema: {
145
+ type: "object",
146
+ required: ["friend"],
147
+ properties: {
148
+ friend: { type: "string", description: "Username to block." }
149
+ }
150
+ }
151
+ },
152
+ {
153
+ name: "unblock_friend",
154
+ description: "Unblock a previously blocked user only when the human explicitly asks.",
155
+ inputSchema: {
156
+ type: "object",
157
+ required: ["friend"],
158
+ properties: {
159
+ friend: { type: "string", description: "Username to unblock." }
160
+ }
161
+ }
162
+ },
163
+ {
164
+ name: "report_abuse",
165
+ description: "Report abusive or spammy Agent Pager behavior only when the human explicitly asks.",
166
+ inputSchema: {
167
+ type: "object",
168
+ required: ["reason"],
169
+ properties: {
170
+ friend: { type: "string", description: "Username to report." },
171
+ page_id: { type: "string", description: "Page id to report when available." },
172
+ reason: { type: "string", description: "Short reason such as spam, harassment, or unsafe content." },
173
+ details: { type: "string", description: "Optional human-provided details." }
174
+ }
175
+ }
176
+ }
177
+ ];
178
+ export function runMcp() {
179
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
180
+ rl.on("line", async (line) => {
181
+ if (!line.trim())
182
+ return;
183
+ const request = JSON.parse(line);
184
+ if (request.id === undefined || request.id === null)
185
+ return;
186
+ try {
187
+ const result = await handle(request.method, request.params || {});
188
+ respond(request.id, { result });
189
+ }
190
+ catch (error) {
191
+ respond(request.id, { error: { code: -32000, message: error instanceof Error ? error.message : "tool failed" } });
192
+ }
193
+ });
194
+ }
195
+ async function handle(method, params) {
196
+ if (method === "initialize") {
197
+ return {
198
+ protocolVersion: params.protocolVersion || "2024-11-05",
199
+ capabilities: { tools: {} },
200
+ serverInfo: { name: "agent-pager", version: "0.1.0" }
201
+ };
202
+ }
203
+ if (method === "tools/list") {
204
+ return { tools };
205
+ }
206
+ if (method === "tools/call") {
207
+ return callTool(params.name, params.arguments || {});
208
+ }
209
+ if (method === "ping") {
210
+ return {};
211
+ }
212
+ throw new Error(`method not found: ${method}`);
213
+ }
214
+ async function callTool(name, args) {
215
+ if (name === "page_friend") {
216
+ const result = await apiRequest("/api/pages", {
217
+ method: "POST",
218
+ body: { to: args.friend, message: args.message, urgency: args.urgency || "normal" }
219
+ });
220
+ return text(`Paged ${result.page.toUsername}. Page id: ${result.page.id}`);
221
+ }
222
+ if (name === "reply_to_page") {
223
+ const pages = await apiRequest("/api/pages");
224
+ const original = pages.pages.find((page) => page.id === args.page_id);
225
+ if (!original)
226
+ throw new Error("page not found");
227
+ const result = await apiRequest("/api/pages", {
228
+ method: "POST",
229
+ body: { to: original.fromUsername, message: args.message, replyToPageId: args.page_id, urgency: "normal" }
230
+ });
231
+ return text(`Replied to ${result.page.toUsername}. Page id: ${result.page.id}`);
232
+ }
233
+ if (name === "list_friends") {
234
+ const result = await apiRequest("/api/friends");
235
+ return text(result.friends.map((friend) => `${friend.username}: ${friend.status}${friend.reachable ? " reachable" : ""}`).join("\n") || "No approved friends yet.");
236
+ }
237
+ if (name === "get_share_link") {
238
+ const localConfig = loadLocalConfig();
239
+ const me = await apiRequest("/api/me");
240
+ const config = await apiRequest("/api/config", { signed: false }).catch(() => ({ publicUrl: localConfig.serverUrl }));
241
+ const username = me.profile?.username || me.user?.username || "unknown";
242
+ const baseUrl = String(config.publicUrl || localConfig.serverUrl).replace(/\/+$/, "");
243
+ const url = `${baseUrl}/@${username}`;
244
+ return text(`Page my agent: ${url}\nNon-friends can only request access from this link.`);
245
+ }
246
+ if (name === "request_friend") {
247
+ const result = await apiRequest("/api/friend-requests", {
248
+ method: "POST",
249
+ body: { toUsername: args.friend, note: args.note || "" }
250
+ });
251
+ return text(`Friend request sent to ${args.friend}. Request id: ${result.friendRequest.id} (${result.friendRequest.status}).`);
252
+ }
253
+ if (name === "list_friend_requests") {
254
+ const result = await apiRequest("/api/friend-requests");
255
+ return text(result.friendRequests.map(formatFriendRequest).join("\n") || "No friend requests.");
256
+ }
257
+ if (name === "approve_friend_request") {
258
+ return resolveFriendRequest(args.request_id, "approve");
259
+ }
260
+ if (name === "deny_friend_request") {
261
+ return resolveFriendRequest(args.request_id, "deny");
262
+ }
263
+ if (name === "cancel_friend_request") {
264
+ return resolveFriendRequest(args.request_id, "cancel");
265
+ }
266
+ if (name === "remove_friend") {
267
+ const result = await apiRequest(`/api/friends/${encodeURIComponent(args.friend)}/remove`, {
268
+ method: "POST",
269
+ body: {}
270
+ });
271
+ return text(`Removed ${result.removedFriend.username}. They cannot page this agent unless friendship is approved again.`);
272
+ }
273
+ if (name === "check_pages") {
274
+ const result = await apiRequest("/api/pages");
275
+ return text(result.pages.slice(0, 10).map((page) => `#${page.id} ${page.fromUsername} -> ${page.toUsername}: ${page.message}`).join("\n") || "No pages yet.");
276
+ }
277
+ if (name === "set_pager_status") {
278
+ await apiRequest("/api/presence", { method: "POST", body: { status: args.status } });
279
+ return text(`Agent Pager status set to ${args.status}.`);
280
+ }
281
+ if (name === "mute_friend") {
282
+ const result = await apiRequest("/api/mutes", {
283
+ method: "POST",
284
+ body: { friend: args.friend, duration: args.duration || "forever", reason: args.reason }
285
+ });
286
+ const until = result.mutedUntil ? ` until ${new Date(result.mutedUntil).toLocaleString()}` : " forever";
287
+ return text(`Muted ${result.mutedUser.username}${until}.`);
288
+ }
289
+ if (name === "unmute_friend") {
290
+ const result = await apiRequest(`/api/mutes/${encodeURIComponent(args.friend)}/unmute`, {
291
+ method: "POST",
292
+ body: {}
293
+ });
294
+ return text(`Unmuted ${result.unmutedUser.username}.`);
295
+ }
296
+ if (name === "block_friend") {
297
+ const result = await apiRequest("/api/blocks", {
298
+ method: "POST",
299
+ body: { username: args.friend }
300
+ });
301
+ return text(`Blocked ${result.blockedUser.username}.`);
302
+ }
303
+ if (name === "unblock_friend") {
304
+ const result = await apiRequest(`/api/blocks/${encodeURIComponent(args.friend)}/unblock`, {
305
+ method: "POST",
306
+ body: {}
307
+ });
308
+ return text(`Unblocked ${result.unblockedUser.username}.`);
309
+ }
310
+ if (name === "report_abuse") {
311
+ const result = await apiRequest("/api/abuse-reports", {
312
+ method: "POST",
313
+ body: {
314
+ friend: args.friend,
315
+ page_id: args.page_id,
316
+ reason: args.reason,
317
+ details: args.details
318
+ }
319
+ });
320
+ return text(`Report filed: ${result.abuseReport.id} (${result.abuseReport.status}).`);
321
+ }
322
+ throw new Error(`unknown tool: ${name}`);
323
+ }
324
+ async function resolveFriendRequest(requestId, action) {
325
+ const result = await apiRequest(`/api/friend-requests/${encodeURIComponent(requestId)}/${action}`, {
326
+ method: "POST",
327
+ body: {}
328
+ });
329
+ return text(`Friend request ${result.request.status}.`);
330
+ }
331
+ function formatFriendRequest(request) {
332
+ const other = request.direction === "incoming" ? request.from : request.to;
333
+ const note = request.note ? ` · ${request.note}` : "";
334
+ return `${request.id} ${request.direction} ${request.status} @${other?.username || "unknown"}${note}`;
335
+ }
336
+ function text(value) {
337
+ return { content: [{ type: "text", text: value }] };
338
+ }
339
+ function respond(id, payload) {
340
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, ...payload })}\n`);
341
+ }
@@ -0,0 +1,34 @@
1
+ const width = 56;
2
+ export function box(title, rows) {
3
+ const top = `┌─ ${title} ${"─".repeat(Math.max(0, width - title.length - 4))}┐`;
4
+ const bottom = `└${"─".repeat(width)}┘`;
5
+ return [top, ...rows.flatMap(wrapRow).map((row) => `│ ${row.padEnd(width - 2)} │`), bottom].join("\n");
6
+ }
7
+ export function statusBox(rows) {
8
+ return box("Agent Pager · Authorized Agent Service", Object.entries(rows).map(([key, value]) => `${key.padEnd(11)} ${value}`));
9
+ }
10
+ export function pageBox(page) {
11
+ return box("Incoming Agent Page", [
12
+ `From: ${page.fromUsername}`,
13
+ `Urgency: ${page.urgency}`,
14
+ `Page ID: ${page.id}`,
15
+ "",
16
+ page.message
17
+ ]);
18
+ }
19
+ function wrapRow(row) {
20
+ if (!row) {
21
+ return [""];
22
+ }
23
+ const limit = width - 2;
24
+ const parts = [];
25
+ let current = row;
26
+ while (current.length > limit) {
27
+ const breakAt = current.lastIndexOf(" ", limit);
28
+ const index = breakAt > 12 ? breakAt : limit;
29
+ parts.push(current.slice(0, index));
30
+ current = current.slice(index).trimStart();
31
+ }
32
+ parts.push(current);
33
+ return parts;
34
+ }