@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoverlover/cc-discord",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Discord <-> Claude Code relay: use your Claude subscription to power per-channel AI bots",
5
5
  "type": "module",
6
6
  "bin": {
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 (!isAllowedChannel(message.channelId)) return;
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);
@@ -23,7 +23,14 @@ export async function formatInboundMessage(message: any) {
23
23
  fullText = "[No text content]";
24
24
  }
25
25
 
26
- return `${author}: ${fullText}`;
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> {