@hoverlover/cc-discord 0.4.0 → 0.5.1
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/skills/release/SKILL.md +197 -0
- package/package.json +1 -1
- package/server/catchup.ts +36 -2
- package/server/config.ts +18 -0
- package/server/db.ts +9 -0
- package/server/index.ts +32 -2
- package/server/messages.ts +8 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Release
|
|
3
|
+
description: Analyze commits since last release and publish with intelligent semantic version bumping.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release
|
|
7
|
+
|
|
8
|
+
Analyze all commits since the last version tag, determine the appropriate semantic version bump (major/minor/patch), and publish to npm.
|
|
9
|
+
|
|
10
|
+
## Version Bump Rules
|
|
11
|
+
|
|
12
|
+
Semantic versioning: `MAJOR.MINOR.PATCH`
|
|
13
|
+
|
|
14
|
+
| Change Type | Bump | Indicators |
|
|
15
|
+
|-------------|------|------------|
|
|
16
|
+
| **Breaking** | MAJOR | `BREAKING CHANGE:`, `!:` suffix, API removals, incompatible changes |
|
|
17
|
+
| **Feature** | MINOR | `feat:`, `feature:`, `add:`, new functionality, enhancements |
|
|
18
|
+
| **Bugfix** | PATCH | `fix:`, `bugfix:`, `patch:`, corrections, typos, small improvements |
|
|
19
|
+
|
|
20
|
+
The highest-impact change determines the bump (MAJOR > MINOR > PATCH).
|
|
21
|
+
|
|
22
|
+
## Instructions
|
|
23
|
+
|
|
24
|
+
### Step 1: Gather Release Context
|
|
25
|
+
|
|
26
|
+
Run these commands in parallel:
|
|
27
|
+
|
|
28
|
+
1. Get the last version tag:
|
|
29
|
+
```bash
|
|
30
|
+
git describe --tags --abbrev=0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. Get commits since last tag:
|
|
34
|
+
```bash
|
|
35
|
+
git log $(git describe --tags --abbrev=0)..HEAD --oneline
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Check npm login status:
|
|
39
|
+
```bash
|
|
40
|
+
npm whoami 2>/dev/null || echo "NOT_LOGGED_IN"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Step 2: Verify npm Authentication
|
|
44
|
+
|
|
45
|
+
Check the result of `npm whoami`:
|
|
46
|
+
|
|
47
|
+
**If logged in** (returns a username): Proceed to Step 3.
|
|
48
|
+
|
|
49
|
+
**If not logged in** (returns "NOT_LOGGED_IN" or error): Use browser-based authentication:
|
|
50
|
+
|
|
51
|
+
1. Connect to Chrome (with retry logic):
|
|
52
|
+
- Call `mcp__claude-in-chrome__tabs_context_mcp` with `createIfEmpty: true`
|
|
53
|
+
- If connection fails, wait 2 seconds and retry (up to 3 times)
|
|
54
|
+
- If connected, create a new tab using `mcp__claude-in-chrome__tabs_create_mcp`
|
|
55
|
+
- Note whether Chrome is available for later steps
|
|
56
|
+
|
|
57
|
+
2. Start `npm login` in the terminal with a pseudo-terminal to get the login URL:
|
|
58
|
+
```bash
|
|
59
|
+
script -q /dev/null npm login --auth-type=web 2>&1
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. The command will output a URL like `https://www.npmjs.com/login?next=/login/cli/...`
|
|
63
|
+
|
|
64
|
+
4. Open the URL:
|
|
65
|
+
- **If Chrome connected:** Navigate the browser tab to that URL using `mcp__claude-in-chrome__navigate`
|
|
66
|
+
- **If Chrome unavailable:** Display the URL and ask user to open it manually
|
|
67
|
+
|
|
68
|
+
5. Inform the user: "Please complete the npm login in the browser window."
|
|
69
|
+
|
|
70
|
+
6. Wait for the login command to complete (it polls for authentication)
|
|
71
|
+
|
|
72
|
+
7. Verify login succeeded:
|
|
73
|
+
```bash
|
|
74
|
+
npm whoami
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
8. If still not logged in, ask user if they want to retry or abort
|
|
78
|
+
|
|
79
|
+
### Step 3: Analyze Commits
|
|
80
|
+
|
|
81
|
+
For each commit since the last tag, classify it:
|
|
82
|
+
|
|
83
|
+
**Conventional Commit Patterns (primary):**
|
|
84
|
+
- `fix:`, `fix(scope):` → PATCH
|
|
85
|
+
- `feat:`, `feat(scope):` → MINOR
|
|
86
|
+
- `BREAKING CHANGE:` in body or `!:` → MAJOR
|
|
87
|
+
- `docs:`, `chore:`, `style:`, `refactor:`, `test:` → PATCH (maintenance)
|
|
88
|
+
|
|
89
|
+
**Content Analysis (for non-conventional commits):**
|
|
90
|
+
- Look at the commit message semantics
|
|
91
|
+
- "Add", "Implement", "Introduce" → likely MINOR
|
|
92
|
+
- "Fix", "Correct", "Repair", "Resolve" → likely PATCH
|
|
93
|
+
- "Remove", "Delete API", "Breaking" → likely MAJOR
|
|
94
|
+
- "Update", "Bump", "Improve" → likely PATCH
|
|
95
|
+
- Release commits ("Bump installer to...") → skip (don't count)
|
|
96
|
+
|
|
97
|
+
**When uncertain:** Default to PATCH unless the change clearly adds new functionality.
|
|
98
|
+
|
|
99
|
+
### Step 4: Present Analysis
|
|
100
|
+
|
|
101
|
+
Show the user:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Analyzing commits since vX.Y.Z...
|
|
105
|
+
|
|
106
|
+
Commit Analysis:
|
|
107
|
+
[hash] [message] → [classification] ([bump type])
|
|
108
|
+
[hash] [message] → [classification] ([bump type])
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
Summary:
|
|
112
|
+
Breaking changes: N
|
|
113
|
+
New features: N
|
|
114
|
+
Bugfixes/maintenance: N
|
|
115
|
+
|
|
116
|
+
Highest impact: [MAJOR|MINOR|PATCH]
|
|
117
|
+
Proposed version: vX.Y.Z → vA.B.C
|
|
118
|
+
|
|
119
|
+
Proceed with release?
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Wait for user confirmation before proceeding.
|
|
123
|
+
|
|
124
|
+
### Step 5: Execute Release
|
|
125
|
+
|
|
126
|
+
#### 5a. Connect to Chrome Browser
|
|
127
|
+
|
|
128
|
+
Before starting the release, establish a Chrome connection for browser-based npm authentication:
|
|
129
|
+
|
|
130
|
+
1. Call `mcp__claude-in-chrome__tabs_context_mcp` with `createIfEmpty: true`
|
|
131
|
+
2. If the connection fails (extension not connected error):
|
|
132
|
+
- Wait 2 seconds and retry
|
|
133
|
+
- Retry up to 3 times total
|
|
134
|
+
- If all retries fail, inform the user: "Chrome browser extension is not available. You'll need to open the npm auth URL manually when prompted."
|
|
135
|
+
3. If connected, create a new tab using `mcp__claude-in-chrome__tabs_create_mcp`
|
|
136
|
+
4. Store the tab ID for later use
|
|
137
|
+
|
|
138
|
+
#### 5b. Run Release Script
|
|
139
|
+
|
|
140
|
+
Run the release script in background mode to capture the auth URL:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
script -q /dev/null ./scripts/release.sh [patch|minor|major] 2>&1
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The script will:
|
|
147
|
+
1. Validate agent configurations
|
|
148
|
+
2. Bump version in package.json files
|
|
149
|
+
3. Commit and tag the release
|
|
150
|
+
4. Push to git
|
|
151
|
+
5. Publish to npm with browser-based 2FA
|
|
152
|
+
|
|
153
|
+
#### 5c. Handle npm Authentication
|
|
154
|
+
|
|
155
|
+
Monitor the script output for the auth URL. When you see:
|
|
156
|
+
```
|
|
157
|
+
Authenticate your account at:
|
|
158
|
+
https://www.npmjs.com/auth/cli/[unique-id]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**If Chrome is connected:**
|
|
162
|
+
1. Navigate the browser tab to that URL using `mcp__claude-in-chrome__navigate`
|
|
163
|
+
2. Inform the user: "Please complete the npm authentication in the browser."
|
|
164
|
+
|
|
165
|
+
**If Chrome is not available:**
|
|
166
|
+
1. Display the full URL to the user
|
|
167
|
+
2. Inform them: "Please open this URL in your browser to authenticate."
|
|
168
|
+
|
|
169
|
+
Wait for the script to complete (npm polls for authentication).
|
|
170
|
+
|
|
171
|
+
### Step 6: Report Results
|
|
172
|
+
|
|
173
|
+
After successful release, show:
|
|
174
|
+
- New version number
|
|
175
|
+
- npm package URL
|
|
176
|
+
- Git tag created
|
|
177
|
+
|
|
178
|
+
## Edge Cases
|
|
179
|
+
|
|
180
|
+
**No commits since last tag:**
|
|
181
|
+
- Inform user there's nothing to release
|
|
182
|
+
- Ask if they want to force a patch bump anyway
|
|
183
|
+
|
|
184
|
+
**All commits are release/chore commits:**
|
|
185
|
+
- Default to PATCH bump
|
|
186
|
+
- Note that only maintenance commits were found
|
|
187
|
+
|
|
188
|
+
**Mixed signals in a commit:**
|
|
189
|
+
- If a commit adds a feature but also fixes a bug, classify by primary intent
|
|
190
|
+
- When truly ambiguous, ask the user
|
|
191
|
+
|
|
192
|
+
## Safety Rules
|
|
193
|
+
|
|
194
|
+
- Always show analysis and get confirmation before releasing
|
|
195
|
+
- Verify npm login before attempting to publish (use browser auth if needed)
|
|
196
|
+
- If any step fails, stop and report the error
|
|
197
|
+
- Don't push or publish without explicit user approval
|
package/package.json
CHANGED
package/server/catchup.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Client } from "discord.js";
|
|
7
|
-
import { CATCHUP_MESSAGE_LIMIT, IGNORED_CHANNEL_IDS, ALLOWED_CHANNEL_IDS, isAllowedUser } from "./config.ts";
|
|
7
|
+
import { CATCHUP_MESSAGE_LIMIT, IGNORED_CHANNEL_IDS, ALLOWED_CHANNEL_IDS, TRACE_THREAD_NAME, isAllowedUser } from "./config.ts";
|
|
8
|
+
import { isTraceThread } from "./db.ts";
|
|
8
9
|
import { persistInboundDiscordMessage, persistOutboundDiscordMessage } from "./messages.ts";
|
|
9
10
|
import { startTypingIndicator } from "./typing.ts";
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ export async function catchUpMissedMessages(client: Client) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
let channelsScanned = 0;
|
|
19
|
+
let threadsCaughtUp = 0;
|
|
18
20
|
let messagesCaughtUp = 0;
|
|
19
21
|
|
|
20
22
|
for (const [, guild] of client.guilds.cache) {
|
|
@@ -47,8 +49,40 @@ export async function catchUpMissedMessages(client: Client) {
|
|
|
47
49
|
} catch (err: unknown) {
|
|
48
50
|
console.error(`[Catchup] Failed to fetch messages for #${channel.name} (${channel.id}):`, (err as Error).message);
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
// Catch up active threads in this channel
|
|
54
|
+
if ("threads" in channel && channel.threads) {
|
|
55
|
+
try {
|
|
56
|
+
const activeThreads = await (channel as any).threads.fetchActive();
|
|
57
|
+
for (const [, thread] of activeThreads.threads) {
|
|
58
|
+
if (thread.name === TRACE_THREAD_NAME || isTraceThread(thread.id)) continue;
|
|
59
|
+
try {
|
|
60
|
+
const threadMessages = await thread.messages.fetch({ limit: CATCHUP_MESSAGE_LIMIT });
|
|
61
|
+
threadsCaughtUp++;
|
|
62
|
+
const sorted = [...threadMessages.values()]
|
|
63
|
+
.filter((m: any) => !m.author.bot && isAllowedUser(m.author.id))
|
|
64
|
+
.sort((a: any, b: any) => a.createdTimestamp - b.createdTimestamp);
|
|
65
|
+
let threadHasNew = false;
|
|
66
|
+
for (const msg of sorted) {
|
|
67
|
+
const isNew = await persistInboundDiscordMessage(msg);
|
|
68
|
+
if (isNew) {
|
|
69
|
+
messagesCaughtUp++;
|
|
70
|
+
threadHasNew = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (threadHasNew) {
|
|
74
|
+
startTypingIndicator(client, thread.id, persistOutboundDiscordMessage);
|
|
75
|
+
}
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
console.error(`[Catchup] Failed for thread "${thread.name}" (${thread.id}):`, (err as Error).message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (err: unknown) {
|
|
81
|
+
console.error(`[Catchup] Failed to fetch threads for #${(channel as any).name}:`, (err as Error).message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
50
84
|
}
|
|
51
85
|
}
|
|
52
86
|
|
|
53
|
-
console.log(`[Catchup] Done — scanned ${channelsScanned} channel(s), caught up ${messagesCaughtUp} message(s)`);
|
|
87
|
+
console.log(`[Catchup] Done — scanned ${channelsScanned} channel(s), ${threadsCaughtUp} thread(s), caught up ${messagesCaughtUp} message(s)`);
|
|
54
88
|
}
|
package/server/config.ts
CHANGED
|
@@ -94,6 +94,24 @@ export function isAllowedChannel(channelId: string): boolean {
|
|
|
94
94
|
return true;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Thread-aware channel check for incoming messages.
|
|
99
|
+
* Allows threads whose parent channel is in the allowlist.
|
|
100
|
+
*/
|
|
101
|
+
export function isAllowedChannelForMessage(message: { channelId: string; channel?: any }): boolean {
|
|
102
|
+
if (!message.channelId) return false;
|
|
103
|
+
if (IGNORED_CHANNEL_IDS.has(message.channelId)) return false;
|
|
104
|
+
if (ALLOWED_CHANNEL_IDS.length === 0) return true;
|
|
105
|
+
if (ALLOWED_CHANNEL_IDS.includes(message.channelId)) return true;
|
|
106
|
+
// Thread: check parent channel
|
|
107
|
+
const ch = message.channel;
|
|
108
|
+
if (ch?.isThread?.() && ch.parentId) {
|
|
109
|
+
if (IGNORED_CHANNEL_IDS.has(ch.parentId)) return false;
|
|
110
|
+
return ALLOWED_CHANNEL_IDS.includes(ch.parentId);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
export function isAllowedUser(userId: string | undefined): boolean {
|
|
98
116
|
if (!userId) return false;
|
|
99
117
|
if (ALLOWED_DISCORD_USER_IDS.length === 0) return true;
|
package/server/db.ts
CHANGED
|
@@ -133,6 +133,15 @@ export function getCurrentAgentActivity(sessionId: string, defaultAgentId: strin
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
export function isTraceThread(threadId: string): boolean {
|
|
137
|
+
try {
|
|
138
|
+
const row = db.prepare("SELECT 1 FROM trace_threads WHERE thread_id = ?").get(threadId);
|
|
139
|
+
return !!row;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
// ── Trace thread helpers ────────────────────────────────────────────
|
|
137
146
|
|
|
138
147
|
export function getTraceThreadId(channelId: string): string | null {
|
package/server/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DISCORD_SESSION_ID,
|
|
16
16
|
IGNORED_CHANNEL_IDS,
|
|
17
17
|
isAllowedChannel,
|
|
18
|
+
isAllowedChannelForMessage,
|
|
18
19
|
isAllowedUser,
|
|
19
20
|
MESSAGE_ROUTING_MODE,
|
|
20
21
|
RELAY_ALLOW_NO_AUTH,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
TYPING_MAX_MS,
|
|
27
28
|
validateConfig,
|
|
28
29
|
} from "./config.ts";
|
|
29
|
-
import { clearChannelModel, db, getAgentHealthAll, getChannelModel, setChannelModel } from "./db.ts";
|
|
30
|
+
import { clearChannelModel, db, getAgentHealthAll, getChannelModel, isTraceThread, setChannelModel } from "./db.ts";
|
|
30
31
|
import { memoryStore } from "./memory.ts";
|
|
31
32
|
import { persistInboundDiscordMessage, persistOutboundDiscordMessage } from "./messages.ts";
|
|
32
33
|
import { startTraceFlushLoop, stopTraceFlushLoop } from "./trace-thread.ts";
|
|
@@ -104,7 +105,8 @@ client.once("clientReady", async () => {
|
|
|
104
105
|
client.on("messageCreate", async (message) => {
|
|
105
106
|
if (!message) return;
|
|
106
107
|
if (message.author?.bot) return;
|
|
107
|
-
if (!
|
|
108
|
+
if (!isAllowedChannelForMessage(message)) return;
|
|
109
|
+
if (message.channel?.isThread?.() && isTraceThread(message.channelId)) return;
|
|
108
110
|
if (!isAllowedUser(message.author?.id)) {
|
|
109
111
|
console.log(`[Relay] Ignoring message from unauthorized user ${message.author?.id}`);
|
|
110
112
|
return;
|
|
@@ -196,6 +198,34 @@ app.get("/api/channels", async (req: Request, res: Response) => {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
// Optionally include active threads in allowed channels
|
|
202
|
+
if (req.query.include_threads === "true") {
|
|
203
|
+
for (const ch of [...channels]) {
|
|
204
|
+
try {
|
|
205
|
+
const parentChannel = await client.channels.fetch(ch.id);
|
|
206
|
+
if (parentChannel && "threads" in parentChannel) {
|
|
207
|
+
const activeThreads = await (parentChannel as any).threads.fetchActive();
|
|
208
|
+
for (const [, thread] of activeThreads.threads) {
|
|
209
|
+
if (isTraceThread(thread.id)) continue;
|
|
210
|
+
channels.push({
|
|
211
|
+
id: thread.id,
|
|
212
|
+
name: thread.name,
|
|
213
|
+
guildId: ch.guildId,
|
|
214
|
+
guildName: ch.guildName,
|
|
215
|
+
type: thread.type,
|
|
216
|
+
isThread: true,
|
|
217
|
+
parentChannelId: ch.id,
|
|
218
|
+
parentChannelName: ch.name,
|
|
219
|
+
model: getChannelModel(thread.id),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
/* skip */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
res.json({ success: true, channels });
|
|
200
230
|
} catch (err: unknown) {
|
|
201
231
|
console.error("[Relay] /api/channels failed:", err);
|
package/server/messages.ts
CHANGED
|
@@ -23,7 +23,14 @@ export async function formatInboundMessage(message: any) {
|
|
|
23
23
|
fullText = "[No text content]";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Add thread context if message is from a thread
|
|
27
|
+
let prefix = author;
|
|
28
|
+
if (message.channel?.isThread?.()) {
|
|
29
|
+
const threadName = message.channel.name || "thread";
|
|
30
|
+
prefix = `${author} [thread: ${threadName}]`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `${prefix}: ${fullText}`;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export async function persistInboundDiscordMessage(message: any): Promise<boolean> {
|