message-cp 0.0.0-stage → 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 +45 -2
- package/dist/accounts.js +279 -0
- package/dist/agents.js +507 -0
- package/dist/approve.js +260 -0
- package/dist/channels.js +262 -0
- package/dist/cli.js +523 -0
- package/dist/client-metadata.js +167 -0
- package/dist/connect.js +85 -0
- package/dist/dashboard.js +4961 -0
- package/dist/db.js +605 -0
- package/dist/http.js +1933 -0
- package/dist/index.js +21 -0
- package/dist/init.js +960 -0
- package/dist/login.js +530 -0
- package/dist/messages.js +513 -0
- package/dist/oauth.js +384 -0
- package/dist/server.js +420 -0
- package/dist/sessions.js +53 -0
- package/dist/settings.js +13 -0
- package/dist/wait.js +137 -0
- package/dist/webhooks.js +464 -0
- package/package.json +44 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, startHttp } from "./http.js";
|
|
5
|
+
import { AgentListError, KeyResetError, PersonError, PurgeError, ToolError, addPerson, listPeople, listServerAgents, markWaiting, purgeAgent, removePerson, resetAgentKey, setPersonAdmin } from "./agents.js";
|
|
6
|
+
import { AccountError, addAccount, addToken, listAccounts, listTokens, revokeToken } from "./accounts.js";
|
|
7
|
+
import { ConnectError, clientSetup, connectAgent, mcpEndpoint, publicUrl as checkedPublicUrl } from "./connect.js";
|
|
8
|
+
import { OWNER_ACCOUNT, openDb } from "./db.js";
|
|
9
|
+
import { CLIENT_IDS, InitError, askOnTerminal, ownServer, proveRemote, proveServer, readConnectFile, realRunner, runInit } from "./init.js";
|
|
10
|
+
import { LOGIN_CODE_TTL_MS, LoginError, dashboardUrl, loginLink, makeLoginCode } from "./login.js";
|
|
11
|
+
import { hasUnread, unreadSummary } from "./messages.js";
|
|
12
|
+
import { setting } from "./settings.js";
|
|
13
|
+
import { formatUnread } from "./wait.js";
|
|
14
|
+
import { WebhookError, addWebhook, formatOn, listWebhooks, removeWebhook, webhookMaxPerHour } from "./webhooks.js";
|
|
15
|
+
// Each command's usage, printed by its own usage error and, all together, by `message-cp help`.
|
|
16
|
+
const USAGE = {
|
|
17
|
+
serve: "message-cp serve [--host <address>] [--port <port>] [--public-url <url>]",
|
|
18
|
+
wait: "message-cp wait <name>",
|
|
19
|
+
account: "message-cp account add <name> | message-cp account list | message-cp token add <account> [--read-only | --agent-only | --person <name>] | message-cp token list | message-cp token revoke <token id>",
|
|
20
|
+
webhook: "message-cp webhook add --url <url> --on <#channel | mention:<agent> | agent:<agent>> [--account <name>] [--no-text] [--header-stdin] [--max-per-hour <n>] [--body <json>] | message-cp webhook list [--account <name>] | message-cp webhook remove <webhook id>",
|
|
21
|
+
person: "message-cp person add <name> [--account <name>] [--admin] | message-cp person admin <name> [--off] [--account <name>] | message-cp person remove <name> [--account <name>] | message-cp person list [--account <name>]",
|
|
22
|
+
agent: "message-cp agent list [--account <name>] | message-cp agent remove <name> [--account <name>] | message-cp agent key reset <name> [--account <name>]",
|
|
23
|
+
connect: "message-cp connect <agent> --url <public url> [--account <name>] [--new-key]",
|
|
24
|
+
"login-link": "message-cp login-link --url <public url> [--account <name>] [--person <name>]",
|
|
25
|
+
init: `message-cp init [--client <${CLIENT_IDS.join("|")}>] [--url <public url> --agent <name> | --from <file connect wrote>] (with --url, the token from MESSAGE_CP_HTTP_TOKEN and, unless the token is bound to the agent, the key from MESSAGE_CP_AGENT_KEY)`,
|
|
26
|
+
};
|
|
27
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
28
|
+
if (command === "serve") {
|
|
29
|
+
const usage = () => {
|
|
30
|
+
console.error(`usage: ${USAGE.serve}`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
};
|
|
33
|
+
const opts = new Map();
|
|
34
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
35
|
+
const name = rest[i];
|
|
36
|
+
const value = rest[i + 1];
|
|
37
|
+
if ((name !== "--host" && name !== "--port" && name !== "--public-url") || opts.has(name) || !value || value.startsWith("--"))
|
|
38
|
+
usage();
|
|
39
|
+
opts.set(name, value);
|
|
40
|
+
}
|
|
41
|
+
const token = setting("HTTP_TOKEN");
|
|
42
|
+
if (!token) {
|
|
43
|
+
console.error("message-cp serve: set MESSAGE_CP_HTTP_TOKEN to a non-empty value; every request must carry it.");
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
const port = opts.has("--port") ? Number(opts.get("--port")) : DEFAULT_HTTP_PORT;
|
|
47
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535)
|
|
48
|
+
usage();
|
|
49
|
+
const host = opts.get("--host") ?? DEFAULT_HTTP_HOST;
|
|
50
|
+
const publicUrl = opts.get("--public-url");
|
|
51
|
+
if (publicUrl !== undefined) {
|
|
52
|
+
let url;
|
|
53
|
+
try {
|
|
54
|
+
url = checkedPublicUrl(publicUrl);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (!(err instanceof ConnectError))
|
|
58
|
+
throw err;
|
|
59
|
+
usage();
|
|
60
|
+
}
|
|
61
|
+
// The address without /mcp, which httpWaitCommand builds /wait on.
|
|
62
|
+
if (/\/mcp\/*$/.test(url.pathname))
|
|
63
|
+
usage();
|
|
64
|
+
}
|
|
65
|
+
const http = await startHttp(openDb(), { token, host, port, publicUrl });
|
|
66
|
+
console.error(`message-cp serve: listening on http://${host}:${http.port}/mcp`);
|
|
67
|
+
const shutdown = () => void http.close().then(() => process.exit(0));
|
|
68
|
+
process.on("SIGINT", shutdown);
|
|
69
|
+
process.on("SIGTERM", shutdown);
|
|
70
|
+
}
|
|
71
|
+
else if (command === "wait") {
|
|
72
|
+
const name = rest[0];
|
|
73
|
+
if (!name) {
|
|
74
|
+
console.error(`usage: ${USAGE.wait}`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
const db = openDb();
|
|
78
|
+
for (;;) {
|
|
79
|
+
let unread;
|
|
80
|
+
try {
|
|
81
|
+
// `wait` reads the database file directly, so it is the owner's account, like stdio.
|
|
82
|
+
unread = unreadSummary(db, OWNER_ACCOUNT, name);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
// A lock held past the busy timeout is transient: try again at the next check.
|
|
86
|
+
const code = err?.code;
|
|
87
|
+
if (typeof code !== "string" || !code.startsWith("SQLITE_BUSY"))
|
|
88
|
+
throw err;
|
|
89
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!unread) {
|
|
93
|
+
console.error(`message-cp wait: no agent named '${name}'.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (hasUnread(unread)) {
|
|
97
|
+
// Exit from the write callback so a piped stdout is flushed before the process ends.
|
|
98
|
+
const err = await new Promise((r) => process.stdout.write(`${formatUnread(unread)}\n`, r));
|
|
99
|
+
// A summary that was not delivered must not look like a successful wake-up.
|
|
100
|
+
if (err)
|
|
101
|
+
console.error(`message-cp wait: could not write the summary: ${err.message}`);
|
|
102
|
+
process.exit(err ? 1 : 0);
|
|
103
|
+
}
|
|
104
|
+
// A check that found nothing marks the agent waiting, as `wait_for_mail` does.
|
|
105
|
+
markWaiting(db, OWNER_ACCOUNT, name);
|
|
106
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (command === "account" || command === "token") {
|
|
110
|
+
// Run on the server, against the database serve uses. A new token is printed this once, to stdout, and only its
|
|
111
|
+
// hash is stored; everything else goes to stderr, except `list`, which never prints a token or a hash.
|
|
112
|
+
const usage = () => {
|
|
113
|
+
console.error(`usage: ${USAGE.account}`);
|
|
114
|
+
process.exit(2);
|
|
115
|
+
};
|
|
116
|
+
const [action, arg, extra, ...more] = rest;
|
|
117
|
+
// Only `token add <account>` takes a flag: --read-only, for a token that can only read, --agent-only, for one that
|
|
118
|
+
// cannot act as a person, or --person <name>, for one that sends and posts only as that person.
|
|
119
|
+
const tokenAdd = command === "token" && action === "add";
|
|
120
|
+
const readOnly = tokenAdd && extra === "--read-only";
|
|
121
|
+
const agentOnly = tokenAdd && extra === "--agent-only";
|
|
122
|
+
const person = tokenAdd && extra === "--person" ? more.shift() : undefined;
|
|
123
|
+
if ((extra !== undefined && !readOnly && !agentOnly && (person === undefined || person.startsWith("--"))) || more.length > 0)
|
|
124
|
+
usage();
|
|
125
|
+
const db = openDb();
|
|
126
|
+
const printToken = (t) => {
|
|
127
|
+
const kind = t.readOnly ? "read-only token" : t.agentOnly ? "agent token" : t.person ? "person token" : "token";
|
|
128
|
+
const as = t.person ? `, sending and posting only as '${t.person}'` : "";
|
|
129
|
+
console.error(`message-cp: ${kind} ${t.id} for account '${t.account}'${as}. It is shown only now; keep it.`);
|
|
130
|
+
console.log(t.token);
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
if (command === "account" && action === "add" && arg)
|
|
134
|
+
printToken(addAccount(db, arg));
|
|
135
|
+
else if (command === "account" && action === "list" && arg === undefined) {
|
|
136
|
+
for (const a of listAccounts(db)) {
|
|
137
|
+
console.log(`${a.name}\tcreated ${a.createdAt}\ttokens: ${a.tokenIds.length ? a.tokenIds.join(", ") : "none"}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (command === "token" && action === "add" && arg && !arg.startsWith("--")) {
|
|
141
|
+
printToken(addToken(db, arg, readOnly, agentOnly, undefined, person));
|
|
142
|
+
}
|
|
143
|
+
else if (command === "token" && action === "list" && arg === undefined) {
|
|
144
|
+
for (const t of listTokens(db)) {
|
|
145
|
+
// An OAuth token's client goes after its kind: its name, else its metadata host, so the operator can tell which to
|
|
146
|
+
// revoke. The client chose its name, so control and format characters (a tab, a newline, an escape) are replaced.
|
|
147
|
+
const client = t.client ? `\tclient ${(t.client.name ?? t.client.host ?? "(unnamed)").replace(/[\p{Cc}\p{Cf}]/gu, "\uFFFD")}` : "";
|
|
148
|
+
const used = t.lastUsedAt === null ? "never used" : `last used ${t.lastUsedAt}`;
|
|
149
|
+
console.log(`${t.id}\t${t.account}\t${t.kind}${client}\tcreated ${t.createdAt}\t${used}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (command === "token" && action === "revoke" && arg) {
|
|
153
|
+
const id = Number(arg);
|
|
154
|
+
if (!/^[0-9]+$/.test(arg) || !Number.isSafeInteger(id))
|
|
155
|
+
usage();
|
|
156
|
+
const revoked = revokeToken(db, id);
|
|
157
|
+
// Agent names match NAME_PATTERN, so they are printed as they are.
|
|
158
|
+
const ended = revoked.agents.length > 0 ? ` Ended the agents it registered: ${revoked.agents.join(", ")}.` : "";
|
|
159
|
+
console.error(`message-cp: revoked token ${id} of account '${revoked.account}'.${ended}`);
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
usage();
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
if (!(err instanceof AccountError))
|
|
166
|
+
throw err;
|
|
167
|
+
console.error(`message-cp: ${err.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
db.close();
|
|
171
|
+
}
|
|
172
|
+
else if (command === "webhook") {
|
|
173
|
+
// Run on the server, like `account` and `token`. A new webhook's secret is printed this once, to stdout; everything
|
|
174
|
+
// else goes to stderr, except `list`, which never prints a secret. `--header-stdin` reads the header line from stdin,
|
|
175
|
+
// so its value is never in an argument list, and the value is never printed. Nor is a `--body`.
|
|
176
|
+
const usage = () => {
|
|
177
|
+
console.error(`usage: ${USAGE.webhook}`);
|
|
178
|
+
process.exit(2);
|
|
179
|
+
};
|
|
180
|
+
const [action, ...args] = rest;
|
|
181
|
+
const valued = action === "add" ? ["--url", "--on", "--account", "--max-per-hour", "--body"] : action === "list" ? ["--account"] : [];
|
|
182
|
+
const opts = new Map();
|
|
183
|
+
let noText = false;
|
|
184
|
+
let headerStdin = false;
|
|
185
|
+
if (action === "add" || action === "list") {
|
|
186
|
+
for (let i = 0; i < args.length; i++) {
|
|
187
|
+
const name = args[i];
|
|
188
|
+
if (action === "add" && name === "--no-text" && !noText) {
|
|
189
|
+
noText = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (action === "add" && name === "--header-stdin" && !headerStdin) {
|
|
193
|
+
headerStdin = true;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const value = args[++i];
|
|
197
|
+
if (!valued.includes(name) || opts.has(name) || !value || value.startsWith("--"))
|
|
198
|
+
usage();
|
|
199
|
+
opts.set(name, value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const db = openDb();
|
|
203
|
+
try {
|
|
204
|
+
if (action === "add" && opts.has("--url") && opts.has("--on")) {
|
|
205
|
+
const account = opts.get("--account") ?? "owner";
|
|
206
|
+
const max = opts.get("--max-per-hour");
|
|
207
|
+
const maxPerHour = max === undefined ? undefined : webhookMaxPerHour(max);
|
|
208
|
+
let header;
|
|
209
|
+
if (headerStdin) {
|
|
210
|
+
// One line, with at most one trailing newline; checked in addWebhook, which never repeats the value.
|
|
211
|
+
const lines = readFileSync(0, "utf8").replace(/\r?\n$/, "").split(/\r?\n/);
|
|
212
|
+
if (lines.filter((l) => l !== "").length > 1)
|
|
213
|
+
throw new WebhookError("--header-stdin takes one line '<Name>: <value>'.");
|
|
214
|
+
header = lines.find((l) => l !== "") ?? "";
|
|
215
|
+
if (header === "")
|
|
216
|
+
throw new WebhookError("--header-stdin read nothing: give one line '<Name>: <value>' on stdin.");
|
|
217
|
+
}
|
|
218
|
+
const w = addWebhook(db, account, opts.get("--url"), opts.get("--on"), !noText, header, maxPerHour, opts.get("--body"));
|
|
219
|
+
console.error(`message-cp: webhook ${w.id} for account '${w.account}'. Its secret is shown only now; keep it.`);
|
|
220
|
+
console.log(w.secret);
|
|
221
|
+
}
|
|
222
|
+
else if (action === "list") {
|
|
223
|
+
for (const w of listWebhooks(db, opts.get("--account"))) {
|
|
224
|
+
const header = w.headerName === null ? "" : `\theader: ${w.headerName}`;
|
|
225
|
+
const max = w.maxPerHour === null ? "" : `\tmax: ${w.maxPerHour}/hour`;
|
|
226
|
+
const body = w.fixedBody ? "\tbody: fixed" : "";
|
|
227
|
+
console.log(`${w.id}\t${w.account}\t${w.url}\t${formatOn(w)}\t${w.includeText ? "text" : "no text"}${header}${max}${body}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (action === "remove" && args.length === 1) {
|
|
231
|
+
const arg = args[0];
|
|
232
|
+
const id = Number(arg);
|
|
233
|
+
if (!/^[0-9]+$/.test(arg) || !Number.isSafeInteger(id))
|
|
234
|
+
usage();
|
|
235
|
+
console.error(`message-cp: removed webhook ${id} of account '${removeWebhook(db, id)}'.`);
|
|
236
|
+
}
|
|
237
|
+
else
|
|
238
|
+
usage();
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
if (!(err instanceof WebhookError))
|
|
242
|
+
throw err;
|
|
243
|
+
console.error(`message-cp: ${err.message}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
db.close();
|
|
247
|
+
}
|
|
248
|
+
else if (command === "person") {
|
|
249
|
+
// Run on the server, like `webhook`. A person has no key or token, so nothing secret is printed.
|
|
250
|
+
const usage = () => {
|
|
251
|
+
console.error(`usage: ${USAGE.person}`);
|
|
252
|
+
process.exit(2);
|
|
253
|
+
};
|
|
254
|
+
const [action, ...args] = rest;
|
|
255
|
+
const named = action === "add" || action === "admin" || action === "remove";
|
|
256
|
+
const name = named ? args.shift() : undefined;
|
|
257
|
+
if ((!named && action !== "list") || (named && (!name || name.startsWith("--"))))
|
|
258
|
+
usage();
|
|
259
|
+
// `--admin` on `add` and `--off` on `admin` take no value, and may come once, anywhere among the arguments; it is taken
|
|
260
|
+
// out before the `--account` pair is read, so `--account --admin owner` reads as `--account owner --admin`.
|
|
261
|
+
const flag = action === "add" ? "--admin" : action === "admin" ? "--off" : undefined;
|
|
262
|
+
const flagged = flag !== undefined && args.includes(flag);
|
|
263
|
+
if (flagged)
|
|
264
|
+
args.splice(args.indexOf(flag), 1);
|
|
265
|
+
const opts = new Map();
|
|
266
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
267
|
+
const option = args[i];
|
|
268
|
+
const value = args[i + 1];
|
|
269
|
+
if (option !== "--account" || opts.has(option) || !value || value.startsWith("--"))
|
|
270
|
+
usage();
|
|
271
|
+
opts.set(option, value);
|
|
272
|
+
}
|
|
273
|
+
const db = openDb();
|
|
274
|
+
try {
|
|
275
|
+
if (action === "add") {
|
|
276
|
+
const p = addPerson(db, opts.get("--account") ?? "owner", name, flagged);
|
|
277
|
+
const admin = p.admin ? ", an admin," : "";
|
|
278
|
+
console.error(`message-cp: person '${p.name}'${admin} in account '${p.account}'. Agents there can message and @mention them.`);
|
|
279
|
+
}
|
|
280
|
+
else if (action === "admin") {
|
|
281
|
+
const p = setPersonAdmin(db, opts.get("--account") ?? "owner", name, !flagged);
|
|
282
|
+
console.error(`message-cp: person '${p.name}' of account '${p.account}' is ${p.admin ? "now an admin" : "no longer an admin"}.`);
|
|
283
|
+
}
|
|
284
|
+
else if (action === "remove") {
|
|
285
|
+
const p = removePerson(db, opts.get("--account") ?? "owner", name);
|
|
286
|
+
console.error(`message-cp: removed person '${p.name}' of account '${p.account}', with ${p.messages} direct message(s) and ${p.mentions} mention(s). The name is free.`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
for (const p of listPeople(db, opts.get("--account")))
|
|
290
|
+
console.log(`${p.name}\t${p.account}\tadded ${p.addedAt}${p.admin ? "\tadmin" : ""}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
if (!(err instanceof PersonError))
|
|
295
|
+
throw err;
|
|
296
|
+
console.error(`message-cp: ${err.message}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
db.close();
|
|
300
|
+
}
|
|
301
|
+
else if (command === "agent") {
|
|
302
|
+
// Run on the server, like `person`. `remove` prints only names and counts, never a message body or a key. `key reset`
|
|
303
|
+
// prints the new key this once, to stdout, and only names to stderr; it never prints the old key or a hash. `list`
|
|
304
|
+
// prints names, accounts and times, never a key or a hash.
|
|
305
|
+
const usage = () => {
|
|
306
|
+
console.error(`usage: ${USAGE.agent}`);
|
|
307
|
+
process.exit(2);
|
|
308
|
+
};
|
|
309
|
+
const reset = rest[0] === "key" && rest[1] === "reset";
|
|
310
|
+
const list = rest[0] === "list";
|
|
311
|
+
const [action, name, ...args] = reset ? ["key reset", ...rest.slice(2)] : list ? ["list", undefined, ...rest.slice(1)] : rest;
|
|
312
|
+
if (!list && ((action !== "remove" && !reset) || !name || name.startsWith("--")))
|
|
313
|
+
usage();
|
|
314
|
+
const opts = new Map();
|
|
315
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
316
|
+
const option = args[i];
|
|
317
|
+
const value = args[i + 1];
|
|
318
|
+
if (option !== "--account" || opts.has(option) || !value || value.startsWith("--"))
|
|
319
|
+
usage();
|
|
320
|
+
opts.set(option, value);
|
|
321
|
+
}
|
|
322
|
+
let db;
|
|
323
|
+
try {
|
|
324
|
+
db = openDb();
|
|
325
|
+
if (list) {
|
|
326
|
+
for (const a of listServerAgents(db, opts.get("--account"))) {
|
|
327
|
+
console.log(`${a.name}\t${a.account}\tregistered ${a.registeredAt}\tlast seen ${a.lastSeen}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (reset) {
|
|
331
|
+
const k = resetAgentKey(db, opts.get("--account") ?? "owner", name);
|
|
332
|
+
console.error(`message-cp: new key for agent '${k.name}' of account '${k.account}'. It is shown only now; keep it. The old key no longer works.`);
|
|
333
|
+
console.log(k.key);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const p = purgeAgent(db, opts.get("--account") ?? "owner", name);
|
|
337
|
+
console.error(`message-cp: removed agent '${p.name}' of account '${p.account}', with ${p.messages} direct message(s) and ${p.mentions} mention(s). The name is free.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
// A reset that could not take the write lock changed nothing: say so in one line, never with a stack trace.
|
|
342
|
+
const code = err?.code;
|
|
343
|
+
if (reset && typeof code === "string" && code.startsWith("SQLITE_BUSY")) {
|
|
344
|
+
console.error("message-cp: the database is busy; nothing was changed. Try again.");
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
if (!(err instanceof PurgeError) && !(err instanceof KeyResetError) && !(err instanceof AgentListError))
|
|
348
|
+
throw err;
|
|
349
|
+
console.error(`message-cp: ${err.message}`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
db.close();
|
|
353
|
+
}
|
|
354
|
+
else if (command === "connect") {
|
|
355
|
+
// Run on the server, like `agent`. The token and the agent's key are printed this once, to stdout, inside the client
|
|
356
|
+
// setup; stderr says what was made and how to revoke the token, and never carries a token, a key or a hash.
|
|
357
|
+
const usage = () => {
|
|
358
|
+
console.error(`usage: ${USAGE.connect}`);
|
|
359
|
+
process.exit(2);
|
|
360
|
+
};
|
|
361
|
+
const [name, ...args] = rest;
|
|
362
|
+
if (!name || name.startsWith("--"))
|
|
363
|
+
usage();
|
|
364
|
+
const opts = new Map();
|
|
365
|
+
let newKey = false;
|
|
366
|
+
for (let i = 0; i < args.length; i++) {
|
|
367
|
+
const option = args[i];
|
|
368
|
+
if (option === "--new-key" && !newKey) {
|
|
369
|
+
newKey = true;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const value = args[++i];
|
|
373
|
+
if ((option !== "--url" && option !== "--account") || opts.has(option) || !value || value.startsWith("--"))
|
|
374
|
+
usage();
|
|
375
|
+
opts.set(option, value);
|
|
376
|
+
}
|
|
377
|
+
if (!opts.has("--url"))
|
|
378
|
+
usage();
|
|
379
|
+
let db;
|
|
380
|
+
try {
|
|
381
|
+
// The URL first, so a refused one changes nothing.
|
|
382
|
+
const endpoint = mcpEndpoint(opts.get("--url"));
|
|
383
|
+
db = openDb();
|
|
384
|
+
const c = connectAgent(db, opts.get("--account") ?? "owner", name, newKey);
|
|
385
|
+
console.error(c.reset
|
|
386
|
+
? `message-cp: new key for agent '${c.name}' of account '${c.account}'; its old key no longer works and its current sessions have ended.`
|
|
387
|
+
: `message-cp: agent '${c.name}' in account '${c.account}'.`);
|
|
388
|
+
console.error(`message-cp: agent token ${c.tokenId} for account '${c.account}'. The output holds this token and the agent's key, shown only now; keep them. To revoke the token: message-cp token list, then message-cp token revoke ${c.tokenId}.`);
|
|
389
|
+
console.log(clientSetup(endpoint, c).replace(/\n$/, ""));
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
const code = err?.code;
|
|
393
|
+
if (typeof code === "string" && code.startsWith("SQLITE_BUSY")) {
|
|
394
|
+
console.error("message-cp: the database is busy; nothing was changed. Try again.");
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
if (!(err instanceof ConnectError) && !(err instanceof ToolError) && !(err instanceof KeyResetError) && !(err instanceof AccountError))
|
|
398
|
+
throw err;
|
|
399
|
+
console.error(`message-cp: ${err.message}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
db.close();
|
|
403
|
+
}
|
|
404
|
+
else if (command === "login-link") {
|
|
405
|
+
// Run on the server, like `connect`. The link, carrying a one-time code, is printed this once, to stdout; stderr says
|
|
406
|
+
// what it signs in as and when it expires, and never carries a code, a token or a hash.
|
|
407
|
+
const usage = () => {
|
|
408
|
+
console.error(`usage: ${USAGE["login-link"]}`);
|
|
409
|
+
process.exit(2);
|
|
410
|
+
};
|
|
411
|
+
const opts = new Map();
|
|
412
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
413
|
+
const option = rest[i];
|
|
414
|
+
const value = rest[i + 1];
|
|
415
|
+
if (!["--url", "--account", "--person"].includes(option) || opts.has(option) || !value || value.startsWith("--"))
|
|
416
|
+
usage();
|
|
417
|
+
opts.set(option, value);
|
|
418
|
+
}
|
|
419
|
+
if (!opts.has("--url"))
|
|
420
|
+
usage();
|
|
421
|
+
let db;
|
|
422
|
+
try {
|
|
423
|
+
// The URL first, so a refused one changes nothing.
|
|
424
|
+
const base = dashboardUrl(opts.get("--url"));
|
|
425
|
+
db = openDb();
|
|
426
|
+
const c = makeLoginCode(db, opts.get("--account") ?? "owner", opts.get("--person"));
|
|
427
|
+
const as = c.person ? `person '${c.person}' of account '${c.account}' (a person token)` : `the owner of account '${c.account}' (a full token)`;
|
|
428
|
+
console.error(`message-cp: this link signs one browser in to the dashboard as ${as}. It works once, and expires in ${LOGIN_CODE_TTL_MS / 60000} minutes, at ${new Date(c.expiresAt).toISOString()}.`);
|
|
429
|
+
console.log(loginLink(base, c.code).href);
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
const code = err?.code;
|
|
433
|
+
if (typeof code === "string" && code.startsWith("SQLITE_BUSY")) {
|
|
434
|
+
console.error("message-cp: the database is busy; nothing was changed. Try again.");
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
if (!(err instanceof ConnectError) && !(err instanceof LoginError))
|
|
438
|
+
throw err;
|
|
439
|
+
console.error(`message-cp: ${err.message}`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
db.close();
|
|
443
|
+
}
|
|
444
|
+
else if (command === "init") {
|
|
445
|
+
// Run on the machine the coding agent runs on. Adds the stdio server, or with --from or --url and --agent the team
|
|
446
|
+
// server, to one client's user-level configuration and checks it with a test agent that is removed afterwards; nothing
|
|
447
|
+
// it prints holds a token or key, which come only from the environment or the file `connect` wrote, never from a flag
|
|
448
|
+
// that shell history would keep.
|
|
449
|
+
const usage = () => {
|
|
450
|
+
console.error(`usage: ${USAGE.init}`);
|
|
451
|
+
process.exit(2);
|
|
452
|
+
};
|
|
453
|
+
const opts = new Map();
|
|
454
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
455
|
+
const option = rest[i];
|
|
456
|
+
const value = rest[i + 1];
|
|
457
|
+
if (!["--client", "--url", "--agent", "--from"].includes(option) || opts.has(option) || !value || value.startsWith("--"))
|
|
458
|
+
usage();
|
|
459
|
+
if (option === "--client" && !CLIENT_IDS.includes(value))
|
|
460
|
+
usage();
|
|
461
|
+
opts.set(option, value);
|
|
462
|
+
}
|
|
463
|
+
if (opts.has("--url") !== opts.has("--agent"))
|
|
464
|
+
usage();
|
|
465
|
+
if (opts.has("--from") && opts.has("--url"))
|
|
466
|
+
usage();
|
|
467
|
+
const client = opts.get("--client");
|
|
468
|
+
let remote;
|
|
469
|
+
if (opts.has("--from")) {
|
|
470
|
+
try {
|
|
471
|
+
remote = readConnectFile(opts.get("--from"));
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
if (!(err instanceof InitError))
|
|
475
|
+
throw err;
|
|
476
|
+
console.error(`message-cp init: ${err.message}`);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (opts.has("--url")) {
|
|
481
|
+
let endpoint;
|
|
482
|
+
try {
|
|
483
|
+
endpoint = mcpEndpoint(opts.get("--url"));
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
if (!(err instanceof ConnectError))
|
|
487
|
+
throw err;
|
|
488
|
+
console.error(`message-cp init: ${err.message} Nothing was changed.`);
|
|
489
|
+
process.exit(2);
|
|
490
|
+
}
|
|
491
|
+
const token = process.env.MESSAGE_CP_HTTP_TOKEN;
|
|
492
|
+
// Optional: a token bound to the agent needs no key, and `checkRemote` refuses an unbound one without it.
|
|
493
|
+
const key = process.env.MESSAGE_CP_AGENT_KEY || undefined;
|
|
494
|
+
if (!token) {
|
|
495
|
+
console.error("message-cp init: with --url, set MESSAGE_CP_HTTP_TOKEN to the agent token (as `message-cp connect` printed it). Nothing was changed.");
|
|
496
|
+
console.error(`usage: ${USAGE.init}`);
|
|
497
|
+
process.exit(2);
|
|
498
|
+
}
|
|
499
|
+
remote = { endpoint, agent: opts.get("--agent"), token, key };
|
|
500
|
+
}
|
|
501
|
+
process.exitCode = await runInit({
|
|
502
|
+
client,
|
|
503
|
+
remote,
|
|
504
|
+
machine: { home: homedir(), path: process.env.PATH ?? "", platform: process.platform, appData: process.env.APPDATA },
|
|
505
|
+
run: realRunner,
|
|
506
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
507
|
+
ask: askOnTerminal,
|
|
508
|
+
prove: () => (remote === undefined ? proveServer(ownServer()) : proveRemote(remote)),
|
|
509
|
+
out: (line) => console.log(line),
|
|
510
|
+
err: (line) => console.error(line),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
else if (command === undefined) {
|
|
514
|
+
await import("./index.js");
|
|
515
|
+
}
|
|
516
|
+
else if (command === "help" || command === "--help") {
|
|
517
|
+
// With no command, message-cp runs the stdio server, so help names that too.
|
|
518
|
+
console.log(["usage: message-cp runs the stdio MCP server", ...Object.values(USAGE).flatMap((u) => u.split(" | message-cp ").map((line, i) => ` ${i ? "message-cp " : ""}${line}`))].join("\n"));
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
console.error(`message-cp: unknown command '${command}'. Commands: serve, wait, account, token, webhook, person, agent, connect, login-link, init, help`);
|
|
522
|
+
process.exit(2);
|
|
523
|
+
}
|