@quaglius/ai-comms 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/.claude-plugin/plugin.json +35 -0
- package/LICENSE +21 -0
- package/README.md +481 -0
- package/bin/ai-comms.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +303 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +136 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +49 -0
- package/dist/context.js +143 -0
- package/dist/context.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +245 -0
- package/dist/daemon.js.map +1 -0
- package/dist/discord.d.ts +52 -0
- package/dist/discord.js +203 -0
- package/dist/discord.js.map +1 -0
- package/dist/envelope.d.ts +208 -0
- package/dist/envelope.js +209 -0
- package/dist/envelope.js.map +1 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +169 -0
- package/dist/mcp.js.map +1 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +99 -0
- package/dist/migrate.js.map +1 -0
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +47 -0
- package/dist/paths.js.map +1 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +70 -0
- package/dist/prompt.js.map +1 -0
- package/dist/secrets.d.ts +15 -0
- package/dist/secrets.js +72 -0
- package/dist/secrets.js.map +1 -0
- package/dist/store.d.ts +46 -0
- package/dist/store.js +186 -0
- package/dist/store.js.map +1 -0
- package/docs/INSTALL.md +128 -0
- package/docs/OPEN-QUESTIONS.md +53 -0
- package/docs/PROTOCOL.md +81 -0
- package/docs/SETUP-FOR-AGENTS.md +293 -0
- package/docs/SPEC-v0.md +142 -0
- package/docs/SPEC-v1.md +190 -0
- package/package.json +61 -0
- package/skills/ai-comms/SKILL.md +71 -0
package/dist/context.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { REPO_COMMS_FILENAME } from './paths.js';
|
|
6
|
+
export const RepoCommsSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
project: z.string().min(1),
|
|
9
|
+
repo: z.string().min(1),
|
|
10
|
+
discord: z.object({ channelId: z.string().min(1) }).strict(),
|
|
11
|
+
team: z.array(z.string().min(1)).optional(),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
14
|
+
export class ContextError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ContextError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function findRepoCommsFile(startDir) {
|
|
21
|
+
let dir = path.resolve(startDir);
|
|
22
|
+
const root = path.parse(dir).root;
|
|
23
|
+
while (true) {
|
|
24
|
+
const candidate = path.join(dir, REPO_COMMS_FILENAME);
|
|
25
|
+
if (existsSync(candidate))
|
|
26
|
+
return candidate;
|
|
27
|
+
if (dir === root)
|
|
28
|
+
break;
|
|
29
|
+
dir = path.dirname(dir);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export function loadRepoComms(filePath) {
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new ContextError(`${filePath} is not valid JSON. Fix the file or run "ai-comms link" again.`);
|
|
40
|
+
}
|
|
41
|
+
const parsed = RepoCommsSchema.safeParse(raw);
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
const issues = parsed.error.issues
|
|
44
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
45
|
+
.join('; ');
|
|
46
|
+
throw new ContextError(`${filePath} is invalid: ${issues}. Run "ai-comms link" to regenerate it.`);
|
|
47
|
+
}
|
|
48
|
+
return parsed.data;
|
|
49
|
+
}
|
|
50
|
+
function resolveRepoFromUserConfig(cwd, config, project) {
|
|
51
|
+
const projectConfig = config.projects?.[project];
|
|
52
|
+
if (!projectConfig)
|
|
53
|
+
return null;
|
|
54
|
+
const resolvedCwd = path.resolve(cwd);
|
|
55
|
+
const repos = projectConfig.repos ?? [];
|
|
56
|
+
for (const entry of repos) {
|
|
57
|
+
const repoPath = path.resolve(entry.path);
|
|
58
|
+
if (resolvedCwd === repoPath || resolvedCwd.startsWith(repoPath + path.sep)) {
|
|
59
|
+
return {
|
|
60
|
+
repo: entry.name,
|
|
61
|
+
channelId: projectConfig.discord.channelId,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
export function resolveContext(cwd, config, options = {}) {
|
|
68
|
+
const cfg = config ?? loadConfig();
|
|
69
|
+
const repoCommsPath = findRepoCommsFile(cwd);
|
|
70
|
+
if (repoCommsPath && !options.projectOverride) {
|
|
71
|
+
const repoComms = loadRepoComms(repoCommsPath);
|
|
72
|
+
return {
|
|
73
|
+
project: repoComms.project,
|
|
74
|
+
repo: repoComms.repo,
|
|
75
|
+
dev: cfg.identity.dev,
|
|
76
|
+
agent: cfg.identity.agent,
|
|
77
|
+
channelId: repoComms.discord.channelId,
|
|
78
|
+
team: repoComms.team ?? [],
|
|
79
|
+
repoCommsPath,
|
|
80
|
+
source: 'repo',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const project = options.projectOverride ?? cfg.defaultProject;
|
|
84
|
+
if (!project) {
|
|
85
|
+
throw new ContextError(contextResolutionError());
|
|
86
|
+
}
|
|
87
|
+
if (repoCommsPath && options.projectOverride) {
|
|
88
|
+
const repoComms = loadRepoComms(repoCommsPath);
|
|
89
|
+
if (repoComms.project !== options.projectOverride) {
|
|
90
|
+
const fromUser = resolveRepoFromUserConfig(cwd, cfg, options.projectOverride);
|
|
91
|
+
if (fromUser) {
|
|
92
|
+
return buildUserConfigContext(cfg, project, fromUser.repo, fromUser.channelId, null);
|
|
93
|
+
}
|
|
94
|
+
throw new ContextError(`Project "${options.projectOverride}" does not match ${repoCommsPath} ` +
|
|
95
|
+
`(project=${repoComms.project}).`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
project: repoComms.project,
|
|
99
|
+
repo: repoComms.repo,
|
|
100
|
+
dev: cfg.identity.dev,
|
|
101
|
+
agent: cfg.identity.agent,
|
|
102
|
+
channelId: repoComms.discord.channelId,
|
|
103
|
+
team: repoComms.team ?? [],
|
|
104
|
+
repoCommsPath,
|
|
105
|
+
source: 'repo',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const fromUser = resolveRepoFromUserConfig(cwd, cfg, project);
|
|
109
|
+
if (fromUser) {
|
|
110
|
+
return buildUserConfigContext(cfg, project, fromUser.repo, fromUser.channelId, null);
|
|
111
|
+
}
|
|
112
|
+
// Without .ai-comms.json and without a declared repo, don't invent the repo name:
|
|
113
|
+
// a misnamed repo silently breaks claim scoping, which is the tool's core job.
|
|
114
|
+
throw new ContextError(contextResolutionError());
|
|
115
|
+
}
|
|
116
|
+
function buildUserConfigContext(config, project, repo, channelId, repoCommsPath) {
|
|
117
|
+
return {
|
|
118
|
+
project,
|
|
119
|
+
repo,
|
|
120
|
+
dev: config.identity.dev,
|
|
121
|
+
agent: config.identity.agent,
|
|
122
|
+
channelId,
|
|
123
|
+
team: [],
|
|
124
|
+
repoCommsPath,
|
|
125
|
+
source: 'user-config',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function contextResolutionError() {
|
|
129
|
+
return ('Could not resolve project/repo context.\n' +
|
|
130
|
+
' · Run "ai-comms link" inside the repo to create .ai-comms.json, or\n' +
|
|
131
|
+
' · Pass --project <name> if it is already configured in ~/.ai-comms/config.json.');
|
|
132
|
+
}
|
|
133
|
+
export function validateRecipient(to, team, dev) {
|
|
134
|
+
for (const recipient of to) {
|
|
135
|
+
if (recipient === '*' || recipient === dev)
|
|
136
|
+
continue;
|
|
137
|
+
if (team.length > 0 && !team.includes(recipient)) {
|
|
138
|
+
return `recipient "${recipient}" is not in team: ${team.join(', ')}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon.d.ts
ADDED
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Client, GatewayIntentBits } from 'discord.js';
|
|
4
|
+
import notifier from 'node-notifier';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { parseEnvelopeFromMessage } from './envelope.js';
|
|
7
|
+
import { getEffectiveToken } from './secrets.js';
|
|
8
|
+
import { appendEnvelope, getDaemonLogPath, loadCursor, saveCursor, writeDaemonPid, removeDaemonPid, } from './store.js';
|
|
9
|
+
import { getConfigDir, getProjectDir } from './paths.js';
|
|
10
|
+
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
11
|
+
function collectTokenGroups(config) {
|
|
12
|
+
const byToken = new Map();
|
|
13
|
+
for (const [project, projectConfig] of Object.entries(config.projects ?? {})) {
|
|
14
|
+
const channelId = projectConfig.discord.channelId;
|
|
15
|
+
try {
|
|
16
|
+
const { token } = getEffectiveToken(project);
|
|
17
|
+
const existing = byToken.get(token) ?? [];
|
|
18
|
+
if (!existing.some((c) => c.channelId === channelId && c.project === project)) {
|
|
19
|
+
existing.push({ channelId, project });
|
|
20
|
+
}
|
|
21
|
+
byToken.set(token, existing);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// project without token: daemon skips it; doctor will report it
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...byToken.entries()].map(([token, channels]) => ({ token, channels }));
|
|
28
|
+
}
|
|
29
|
+
function daemonLog(project, message, verbose) {
|
|
30
|
+
mkdirSync(getProjectDir(project), { recursive: true });
|
|
31
|
+
rotateLogIfNeeded(project);
|
|
32
|
+
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
33
|
+
appendFileSync(getDaemonLogPath(project), line, 'utf8');
|
|
34
|
+
if (verbose) {
|
|
35
|
+
process.stderr.write(`[${project}] ${line}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function rotateLogIfNeeded(project) {
|
|
39
|
+
const logPath = getDaemonLogPath(project);
|
|
40
|
+
if (!existsSync(logPath))
|
|
41
|
+
return;
|
|
42
|
+
const size = statSync(logPath).size;
|
|
43
|
+
if (size < MAX_LOG_BYTES)
|
|
44
|
+
return;
|
|
45
|
+
const rotated = path.join(getProjectDir(project), 'daemon.log.1');
|
|
46
|
+
if (existsSync(rotated)) {
|
|
47
|
+
try {
|
|
48
|
+
renameSync(rotated, path.join(getProjectDir(project), 'daemon.log.2'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
renameSync(logPath, rotated);
|
|
55
|
+
}
|
|
56
|
+
function shouldNotify(envelope, dev) {
|
|
57
|
+
const directed = envelope.to.includes('*') || envelope.to.includes(dev);
|
|
58
|
+
if (!directed)
|
|
59
|
+
return false;
|
|
60
|
+
if (new Date(envelope.ttl) <= new Date())
|
|
61
|
+
return false;
|
|
62
|
+
if (envelope.hops >= 3)
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
function notifyEnvelope(envelope, dev) {
|
|
67
|
+
const sound = (envelope.type === 'need' || envelope.type === 'ask') &&
|
|
68
|
+
envelope.to.includes(dev) &&
|
|
69
|
+
!envelope.to.includes('*');
|
|
70
|
+
const title = `${envelope.type} · ${envelope.from.dev}/${envelope.from.agent}`;
|
|
71
|
+
const message = envelope.subject;
|
|
72
|
+
try {
|
|
73
|
+
notifier.notify({
|
|
74
|
+
title,
|
|
75
|
+
message,
|
|
76
|
+
sound: sound ? true : false,
|
|
77
|
+
wait: false,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function processMessage(message, project, dev, verbose) {
|
|
85
|
+
const envelope = parseEnvelopeFromMessage(message.content);
|
|
86
|
+
if (!envelope) {
|
|
87
|
+
if (verbose) {
|
|
88
|
+
daemonLog(project, `Message without valid envelope: ${message.id}`, true);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
saveCursor(project, { lastMessageId: message.id });
|
|
93
|
+
if (envelope.from.dev === dev)
|
|
94
|
+
return;
|
|
95
|
+
appendEnvelope(envelope, project);
|
|
96
|
+
if (shouldNotify(envelope, dev)) {
|
|
97
|
+
notifyEnvelope(envelope, dev);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Discord's hard per-request cap on `limit` for channel message fetches. */
|
|
101
|
+
const MAX_FETCH_PAGE = 100;
|
|
102
|
+
/** How far back to read when there is no cursor yet. */
|
|
103
|
+
const COLD_START_HISTORY = 200;
|
|
104
|
+
function sortById(messages) {
|
|
105
|
+
return [...messages].sort((a, b) => (BigInt(a.id) < BigInt(b.id) ? -1 : 1));
|
|
106
|
+
}
|
|
107
|
+
async function backfillHistory(client, channelId, project, dev, verbose) {
|
|
108
|
+
const channel = await client.channels.fetch(channelId);
|
|
109
|
+
if (!channel || !channel.isTextBased() || channel.isDMBased()) {
|
|
110
|
+
throw new Error(`Channel ${channelId} is not a valid text channel`);
|
|
111
|
+
}
|
|
112
|
+
const cursor = loadCursor(project);
|
|
113
|
+
// Discord caps `limit` at 100 per request, so both paths have to paginate.
|
|
114
|
+
// Without this, a cold start silently loses every claim older than the last
|
|
115
|
+
// 100 messages, and a daemon that was down for a while misses the backlog.
|
|
116
|
+
const collected = [];
|
|
117
|
+
if (cursor?.lastMessageId) {
|
|
118
|
+
let after = cursor.lastMessageId;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const page = await channel.messages.fetch({ after, limit: MAX_FETCH_PAGE });
|
|
121
|
+
if (page.size === 0)
|
|
122
|
+
break;
|
|
123
|
+
const asc = sortById([...page.values()]);
|
|
124
|
+
collected.push(...asc);
|
|
125
|
+
after = asc[asc.length - 1].id;
|
|
126
|
+
if (page.size < MAX_FETCH_PAGE)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
let before;
|
|
132
|
+
while (collected.length < COLD_START_HISTORY) {
|
|
133
|
+
const page = await channel.messages.fetch({
|
|
134
|
+
limit: Math.min(MAX_FETCH_PAGE, COLD_START_HISTORY - collected.length),
|
|
135
|
+
...(before ? { before } : {}),
|
|
136
|
+
});
|
|
137
|
+
if (page.size === 0)
|
|
138
|
+
break;
|
|
139
|
+
const asc = sortById([...page.values()]);
|
|
140
|
+
collected.push(...asc);
|
|
141
|
+
before = asc[0].id;
|
|
142
|
+
if (page.size < MAX_FETCH_PAGE)
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const sorted = sortById(collected);
|
|
147
|
+
for (const msg of sorted) {
|
|
148
|
+
processMessage(msg, project, dev, verbose);
|
|
149
|
+
}
|
|
150
|
+
if (sorted.length > 0) {
|
|
151
|
+
saveCursor(project, { lastMessageId: sorted[sorted.length - 1].id });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function runClientForToken(group, config, verbose) {
|
|
155
|
+
const client = new Client({
|
|
156
|
+
intents: [
|
|
157
|
+
GatewayIntentBits.Guilds,
|
|
158
|
+
GatewayIntentBits.GuildMessages,
|
|
159
|
+
GatewayIntentBits.MessageContent,
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
const channelIds = new Set(group.channels.map((c) => c.channelId));
|
|
163
|
+
const projectByChannel = new Map(group.channels.map((c) => [c.channelId, c.project]));
|
|
164
|
+
let reconnectAttempt = 0;
|
|
165
|
+
client.on('ready', async () => {
|
|
166
|
+
reconnectAttempt = 0;
|
|
167
|
+
for (const binding of group.channels) {
|
|
168
|
+
daemonLog(binding.project, 'Daemon connected', verbose);
|
|
169
|
+
try {
|
|
170
|
+
await backfillHistory(client, binding.channelId, binding.project, config.identity.dev, verbose);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
daemonLog(binding.project, `Backfill error: ${String(err)}`, verbose);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
client.on('messageCreate', (message) => {
|
|
178
|
+
if (!channelIds.has(message.channelId))
|
|
179
|
+
return;
|
|
180
|
+
const project = projectByChannel.get(message.channelId);
|
|
181
|
+
if (!project)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
processMessage(message, project, config.identity.dev, verbose);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
daemonLog(project, `Error processing message: ${String(err)}`, verbose);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
client.on('error', (err) => {
|
|
191
|
+
for (const binding of group.channels) {
|
|
192
|
+
daemonLog(binding.project, `Client error: ${String(err)}`, verbose);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const connectWithBackoff = async () => {
|
|
196
|
+
for (;;) {
|
|
197
|
+
try {
|
|
198
|
+
await client.login(group.token);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
reconnectAttempt++;
|
|
203
|
+
const delay = Math.min(60_000, 1000 * 2 ** reconnectAttempt);
|
|
204
|
+
for (const binding of group.channels) {
|
|
205
|
+
daemonLog(binding.project, `Login failed (${String(err)}), retrying in ${delay}ms`, verbose);
|
|
206
|
+
}
|
|
207
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
await connectWithBackoff();
|
|
212
|
+
return client;
|
|
213
|
+
}
|
|
214
|
+
export async function runDaemon(options = {}) {
|
|
215
|
+
const verbose = options.verbose ?? false;
|
|
216
|
+
const config = loadConfig();
|
|
217
|
+
const groups = collectTokenGroups(config);
|
|
218
|
+
if (groups.length === 0) {
|
|
219
|
+
throw new Error('No projects with a configured token. Run "ai-comms secret set <project>".');
|
|
220
|
+
}
|
|
221
|
+
mkdirSync(getConfigDir(), { recursive: true });
|
|
222
|
+
const projects = [...new Set(groups.flatMap((g) => g.channels.map((c) => c.project)))];
|
|
223
|
+
for (const project of projects) {
|
|
224
|
+
writeDaemonPid(project);
|
|
225
|
+
}
|
|
226
|
+
const clients = [];
|
|
227
|
+
for (const group of groups) {
|
|
228
|
+
clients.push(await runClientForToken(group, config, verbose));
|
|
229
|
+
}
|
|
230
|
+
const shutdown = () => {
|
|
231
|
+
for (const project of projects) {
|
|
232
|
+
removeDaemonPid(project);
|
|
233
|
+
}
|
|
234
|
+
for (const client of clients) {
|
|
235
|
+
client.destroy();
|
|
236
|
+
}
|
|
237
|
+
process.exit(0);
|
|
238
|
+
};
|
|
239
|
+
process.on('SIGINT', shutdown);
|
|
240
|
+
process.on('SIGTERM', shutdown);
|
|
241
|
+
await new Promise(() => {
|
|
242
|
+
// keep alive
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.js","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Envelope } from './envelope.js';
|
|
2
|
+
export declare const REQUIRED_PERMISSIONS: {
|
|
3
|
+
readonly VIEW_CHANNEL: bigint;
|
|
4
|
+
readonly SEND_MESSAGES: bigint;
|
|
5
|
+
readonly READ_MESSAGE_HISTORY: bigint;
|
|
6
|
+
};
|
|
7
|
+
export declare const REQUIRED_PERMISSION_BITS: bigint;
|
|
8
|
+
export declare const PERMISSION_NAMES: Record<string, bigint>;
|
|
9
|
+
export interface DiscordMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class DiscordApiError extends Error {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly retryAfter?: number | undefined;
|
|
16
|
+
constructor(message: string, status: number, retryAfter?: number | undefined);
|
|
17
|
+
}
|
|
18
|
+
export declare function discordFetch(url: string, token: string, init: RequestInit, maxAttempts?: number): Promise<Response>;
|
|
19
|
+
export declare function sendEnvelope(envelope: Envelope, channelId: string, token: string): Promise<{
|
|
20
|
+
id: string;
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function fetchMessagesAfter(channelId: string, token: string, afterMessageId: string, limit?: number): Promise<DiscordMessage[]>;
|
|
24
|
+
export declare function fetchRecentMessages(channelId: string, token: string, limit?: number): Promise<DiscordMessage[]>;
|
|
25
|
+
export declare function getBotUser(token: string): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
username: string;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function getChannel(channelId: string, token: string): Promise<{
|
|
30
|
+
id: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
type: number;
|
|
33
|
+
guild_id?: string;
|
|
34
|
+
permission_overwrites?: PermissionOverwrite[];
|
|
35
|
+
}>;
|
|
36
|
+
interface PermissionOverwrite {
|
|
37
|
+
id: string;
|
|
38
|
+
type: number;
|
|
39
|
+
allow: string;
|
|
40
|
+
deny: string;
|
|
41
|
+
}
|
|
42
|
+
interface GuildRole {
|
|
43
|
+
id: string;
|
|
44
|
+
permissions: string;
|
|
45
|
+
position: number;
|
|
46
|
+
}
|
|
47
|
+
export declare function computeEffectivePermissions(guildId: string, everyonePermissions: bigint, memberRoleIds: string[], roles: GuildRole[], overwrites: PermissionOverwrite[], memberId: string): bigint;
|
|
48
|
+
export declare function checkBotPermissions(channelId: string, token: string): Promise<{
|
|
49
|
+
ok: boolean;
|
|
50
|
+
missing: string[];
|
|
51
|
+
}>;
|
|
52
|
+
export {};
|
package/dist/discord.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { renderEnvelope } from './envelope.js';
|
|
2
|
+
const DISCORD_API = 'https://discord.com/api/v10';
|
|
3
|
+
export const REQUIRED_PERMISSIONS = {
|
|
4
|
+
VIEW_CHANNEL: 1n << 10n,
|
|
5
|
+
SEND_MESSAGES: 1n << 11n,
|
|
6
|
+
READ_MESSAGE_HISTORY: 1n << 16n,
|
|
7
|
+
};
|
|
8
|
+
export const REQUIRED_PERMISSION_BITS = REQUIRED_PERMISSIONS.VIEW_CHANNEL |
|
|
9
|
+
REQUIRED_PERMISSIONS.SEND_MESSAGES |
|
|
10
|
+
REQUIRED_PERMISSIONS.READ_MESSAGE_HISTORY;
|
|
11
|
+
export const PERMISSION_NAMES = {
|
|
12
|
+
VIEW_CHANNEL: REQUIRED_PERMISSIONS.VIEW_CHANNEL,
|
|
13
|
+
SEND_MESSAGES: REQUIRED_PERMISSIONS.SEND_MESSAGES,
|
|
14
|
+
READ_MESSAGE_HISTORY: REQUIRED_PERMISSIONS.READ_MESSAGE_HISTORY,
|
|
15
|
+
};
|
|
16
|
+
export class DiscordApiError extends Error {
|
|
17
|
+
status;
|
|
18
|
+
retryAfter;
|
|
19
|
+
constructor(message, status, retryAfter) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.retryAfter = retryAfter;
|
|
23
|
+
this.name = 'DiscordApiError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function parseRetryAfter(headers) {
|
|
27
|
+
const resetAfter = headers.get('X-RateLimit-Reset-After');
|
|
28
|
+
if (resetAfter) {
|
|
29
|
+
const n = Number.parseFloat(resetAfter);
|
|
30
|
+
if (Number.isFinite(n))
|
|
31
|
+
return Math.ceil(n * 1000);
|
|
32
|
+
}
|
|
33
|
+
const retryAfter = headers.get('Retry-After');
|
|
34
|
+
if (retryAfter) {
|
|
35
|
+
const n = Number.parseFloat(retryAfter);
|
|
36
|
+
if (Number.isFinite(n))
|
|
37
|
+
return Math.ceil(n * 1000);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function sleep(ms) {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
|
+
}
|
|
44
|
+
export async function discordFetch(url, token, init, maxAttempts = 3) {
|
|
45
|
+
let attempt = 0;
|
|
46
|
+
while (attempt < maxAttempts) {
|
|
47
|
+
attempt++;
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
...init,
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bot ${token}`,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
...(init.headers ?? {}),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
if (response.status === 429) {
|
|
57
|
+
const retryMs = parseRetryAfter(response.headers) ?? 1000 * attempt;
|
|
58
|
+
if (attempt >= maxAttempts) {
|
|
59
|
+
throw new DiscordApiError('Discord rate limit exceeded', 429, retryMs);
|
|
60
|
+
}
|
|
61
|
+
await sleep(retryMs);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return response;
|
|
65
|
+
}
|
|
66
|
+
throw new DiscordApiError('Failed after retries', 429);
|
|
67
|
+
}
|
|
68
|
+
export async function sendEnvelope(envelope, channelId, token) {
|
|
69
|
+
const { content, truncated } = renderEnvelope(envelope);
|
|
70
|
+
const url = `${DISCORD_API}/channels/${channelId}/messages`;
|
|
71
|
+
const response = await discordFetch(url, token, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify({ content }),
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
throw new DiscordApiError(`Failed to send message (${response.status}): ${text.slice(0, 200)}`, response.status);
|
|
78
|
+
}
|
|
79
|
+
const data = (await response.json());
|
|
80
|
+
return { id: data.id, truncated };
|
|
81
|
+
}
|
|
82
|
+
export async function fetchMessagesAfter(channelId, token, afterMessageId, limit = 100) {
|
|
83
|
+
const collected = [];
|
|
84
|
+
let after = afterMessageId;
|
|
85
|
+
while (collected.length < limit) {
|
|
86
|
+
const batchLimit = Math.min(100, limit - collected.length);
|
|
87
|
+
const url = new URL(`${DISCORD_API}/channels/${channelId}/messages`);
|
|
88
|
+
url.searchParams.set('after', after);
|
|
89
|
+
url.searchParams.set('limit', String(batchLimit));
|
|
90
|
+
const response = await discordFetch(url.toString(), token, { method: 'GET' });
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
throw new DiscordApiError(`Failed to read history (${response.status}): ${text.slice(0, 200)}`, response.status);
|
|
94
|
+
}
|
|
95
|
+
const batch = (await response.json());
|
|
96
|
+
if (batch.length === 0)
|
|
97
|
+
break;
|
|
98
|
+
collected.push(...batch);
|
|
99
|
+
after = batch[0].id;
|
|
100
|
+
if (batch.length < batchLimit)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return collected.sort((a, b) => (BigInt(a.id) < BigInt(b.id) ? -1 : 1));
|
|
104
|
+
}
|
|
105
|
+
export async function fetchRecentMessages(channelId, token, limit = 200) {
|
|
106
|
+
const url = new URL(`${DISCORD_API}/channels/${channelId}/messages`);
|
|
107
|
+
url.searchParams.set('limit', String(Math.min(limit, 100)));
|
|
108
|
+
const collected = [];
|
|
109
|
+
let before;
|
|
110
|
+
while (collected.length < limit) {
|
|
111
|
+
const pageUrl = new URL(url);
|
|
112
|
+
if (before)
|
|
113
|
+
pageUrl.searchParams.set('before', before);
|
|
114
|
+
const batchLimit = Math.min(100, limit - collected.length);
|
|
115
|
+
pageUrl.searchParams.set('limit', String(batchLimit));
|
|
116
|
+
const response = await discordFetch(pageUrl.toString(), token, { method: 'GET' });
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const text = await response.text();
|
|
119
|
+
throw new DiscordApiError(`Failed to read history (${response.status}): ${text.slice(0, 200)}`, response.status);
|
|
120
|
+
}
|
|
121
|
+
const batch = (await response.json());
|
|
122
|
+
if (batch.length === 0)
|
|
123
|
+
break;
|
|
124
|
+
collected.push(...batch);
|
|
125
|
+
before = batch[batch.length - 1].id;
|
|
126
|
+
if (batch.length < batchLimit)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return collected.sort((a, b) => (BigInt(a.id) < BigInt(b.id) ? -1 : 1));
|
|
130
|
+
}
|
|
131
|
+
export async function getBotUser(token) {
|
|
132
|
+
const response = await discordFetch(`${DISCORD_API}/users/@me`, token, { method: 'GET' });
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new DiscordApiError('Invalid token or no access', response.status);
|
|
135
|
+
}
|
|
136
|
+
return (await response.json());
|
|
137
|
+
}
|
|
138
|
+
export async function getChannel(channelId, token) {
|
|
139
|
+
const response = await discordFetch(`${DISCORD_API}/channels/${channelId}`, token, { method: 'GET' });
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new DiscordApiError('Channel inaccessible or invalid ID', response.status);
|
|
142
|
+
}
|
|
143
|
+
return (await response.json());
|
|
144
|
+
}
|
|
145
|
+
export function computeEffectivePermissions(guildId, everyonePermissions, memberRoleIds, roles, overwrites, memberId) {
|
|
146
|
+
const roleMap = new Map(roles.map((r) => [r.id, r]));
|
|
147
|
+
const memberRoles = memberRoleIds
|
|
148
|
+
.map((id) => roleMap.get(id))
|
|
149
|
+
.filter((r) => Boolean(r))
|
|
150
|
+
.sort((a, b) => b.position - a.position);
|
|
151
|
+
let perms = everyonePermissions;
|
|
152
|
+
for (const role of memberRoles) {
|
|
153
|
+
perms |= BigInt(role.permissions || '0');
|
|
154
|
+
}
|
|
155
|
+
if ((perms & (1n << 3n)) !== 0n) {
|
|
156
|
+
return (1n << 31n) - 1n;
|
|
157
|
+
}
|
|
158
|
+
const applyOverwrite = (overwrite) => {
|
|
159
|
+
const allow = BigInt(overwrite.allow || '0');
|
|
160
|
+
const deny = BigInt(overwrite.deny || '0');
|
|
161
|
+
perms = (perms & ~deny) | allow;
|
|
162
|
+
};
|
|
163
|
+
const everyoneOverwrite = overwrites.find((o) => o.type === 0 && o.id === guildId);
|
|
164
|
+
if (everyoneOverwrite)
|
|
165
|
+
applyOverwrite(everyoneOverwrite);
|
|
166
|
+
for (const role of memberRoles) {
|
|
167
|
+
const ow = overwrites.find((o) => o.type === 0 && o.id === role.id);
|
|
168
|
+
if (ow)
|
|
169
|
+
applyOverwrite(ow);
|
|
170
|
+
}
|
|
171
|
+
const memberOverwrite = overwrites.find((o) => o.type === 1 && o.id === memberId);
|
|
172
|
+
if (memberOverwrite)
|
|
173
|
+
applyOverwrite(memberOverwrite);
|
|
174
|
+
return perms;
|
|
175
|
+
}
|
|
176
|
+
export async function checkBotPermissions(channelId, token) {
|
|
177
|
+
const channel = await getChannel(channelId, token);
|
|
178
|
+
if (!channel.guild_id) {
|
|
179
|
+
return { ok: true, missing: [] };
|
|
180
|
+
}
|
|
181
|
+
const bot = await getBotUser(token);
|
|
182
|
+
const memberResp = await discordFetch(`${DISCORD_API}/guilds/${channel.guild_id}/members/${bot.id}`, token, { method: 'GET' });
|
|
183
|
+
if (!memberResp.ok) {
|
|
184
|
+
return { ok: false, missing: ['guild membership'] };
|
|
185
|
+
}
|
|
186
|
+
const member = (await memberResp.json());
|
|
187
|
+
const guildResp = await discordFetch(`${DISCORD_API}/guilds/${channel.guild_id}`, token, { method: 'GET' });
|
|
188
|
+
if (!guildResp.ok) {
|
|
189
|
+
return { ok: false, missing: ['guild access'] };
|
|
190
|
+
}
|
|
191
|
+
const guild = (await guildResp.json());
|
|
192
|
+
const everyoneRole = guild.roles.find((r) => r.id === guild.id);
|
|
193
|
+
const everyonePerms = BigInt(everyoneRole?.permissions || '0');
|
|
194
|
+
const effective = computeEffectivePermissions(guild.id, everyonePerms, member.roles, guild.roles, channel.permission_overwrites ?? [], bot.id);
|
|
195
|
+
const missing = [];
|
|
196
|
+
for (const [name, bit] of Object.entries(PERMISSION_NAMES)) {
|
|
197
|
+
if ((effective & bit) === 0n) {
|
|
198
|
+
missing.push(name);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ok: missing.length === 0, missing };
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=discord.js.map
|