@chrisromp/copilot-bridge 0.7.0-dev.16 → 0.8.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.
Files changed (38) hide show
  1. package/config.sample.json +8 -1
  2. package/dist/channels/slack/adapter.d.ts +62 -0
  3. package/dist/channels/slack/adapter.d.ts.map +1 -0
  4. package/dist/channels/slack/adapter.js +382 -0
  5. package/dist/channels/slack/adapter.js.map +1 -0
  6. package/dist/channels/slack/mrkdwn.d.ts +22 -0
  7. package/dist/channels/slack/mrkdwn.d.ts.map +1 -0
  8. package/dist/channels/slack/mrkdwn.js +120 -0
  9. package/dist/channels/slack/mrkdwn.js.map +1 -0
  10. package/dist/config.d.ts +5 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +55 -7
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/access-control.d.ts +32 -0
  15. package/dist/core/access-control.d.ts.map +1 -0
  16. package/dist/core/access-control.js +59 -0
  17. package/dist/core/access-control.js.map +1 -0
  18. package/dist/core/command-handler.d.ts.map +1 -1
  19. package/dist/core/command-handler.js +75 -1
  20. package/dist/core/command-handler.js.map +1 -1
  21. package/dist/core/inter-agent.d.ts +9 -2
  22. package/dist/core/inter-agent.d.ts.map +1 -1
  23. package/dist/core/inter-agent.js +87 -22
  24. package/dist/core/inter-agent.js.map +1 -1
  25. package/dist/core/model-fallback.js +1 -1
  26. package/dist/core/model-fallback.js.map +1 -1
  27. package/dist/index.js +182 -14
  28. package/dist/index.js.map +1 -1
  29. package/dist/types.d.ts +9 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/package.json +2 -1
  32. package/scripts/init.ts +322 -117
  33. package/scripts/lib/config-gen.ts +74 -10
  34. package/scripts/lib/prerequisites.ts +17 -5
  35. package/scripts/lib/prompts.ts +4 -0
  36. package/scripts/lib/slack.ts +190 -0
  37. package/templates/admin/AGENTS.md +5 -5
  38. package/templates/agents/AGENTS.md +1 -1
@@ -4,6 +4,9 @@
4
4
  */
5
5
 
6
6
  import { execSync } from 'node:child_process';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
7
10
  import type { CheckResult } from './output.js';
8
11
 
9
12
  export function checkNodeVersion(): CheckResult {
@@ -64,16 +67,25 @@ export function checkGitHubAuth(): CheckResult {
64
67
  return { status: 'pass', label: 'GitHub authenticated', detail: 'via gh CLI' };
65
68
  }
66
69
 
67
- // Check if Copilot CLI has stored credentials
68
- const copilotAuth = tryCommand('copilot auth status 2>&1');
69
- if (copilotAuth && !copilotAuth.includes('not logged in')) {
70
- return { status: 'pass', label: 'GitHub authenticated', detail: 'via Copilot CLI' };
70
+ // Check if Copilot CLI has stored credentials (~/.copilot/config.json)
71
+ try {
72
+ const copilotConfig = join(homedir(), '.copilot', 'config.json');
73
+ if (existsSync(copilotConfig)) {
74
+ const data = JSON.parse(readFileSync(copilotConfig, 'utf-8'));
75
+ const users = data.logged_in_users;
76
+ if (Array.isArray(users) && users.length > 0) {
77
+ const login = users[0].login ?? 'unknown';
78
+ return { status: 'pass', label: 'GitHub authenticated', detail: `via Copilot CLI (${login})` };
79
+ }
80
+ }
81
+ } catch {
82
+ // Fall through
71
83
  }
72
84
 
73
85
  return {
74
86
  status: 'warn',
75
87
  label: 'GitHub authentication',
76
- detail: 'no token found — set COPILOT_GITHUB_TOKEN, run gh auth login, or run copilot auth login',
88
+ detail: 'no token found — set COPILOT_GITHUB_TOKEN, run gh auth login, or run copilot login',
77
89
  };
78
90
  }
79
91
 
@@ -46,6 +46,10 @@ export async function confirm(question: string, defaultYes = true): Promise<bool
46
46
  return answer.toLowerCase().startsWith('y');
47
47
  }
48
48
 
49
+ export async function pressEnter(message = 'Press Enter to continue...'): Promise<void> {
50
+ await ask(message);
51
+ }
52
+
49
53
  export async function choose(question: string, options: string[], defaultIndex = 0): Promise<number> {
50
54
  console.log(`\n${question}`);
51
55
  for (let i = 0; i < options.length; i++) {
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Slack setup helpers for the init wizard.
3
+ * Generates app manifests and validates tokens.
4
+ */
5
+
6
+ /**
7
+ * Generate a Slack App Manifest for copilot-bridge.
8
+ * This pre-configures all required scopes, events, and Socket Mode.
9
+ */
10
+ export function generateManifest(botName: string): object {
11
+ return {
12
+ display_information: {
13
+ name: botName,
14
+ description: 'GitHub Copilot bridge for Slack',
15
+ background_color: '#1a1a2e',
16
+ },
17
+ features: {
18
+ app_home: {
19
+ messages_tab_enabled: true,
20
+ messages_tab_read_only_enabled: false,
21
+ },
22
+ bot_user: {
23
+ display_name: botName,
24
+ always_online: true,
25
+ },
26
+ },
27
+ oauth_config: {
28
+ scopes: {
29
+ bot: [
30
+ 'chat:write',
31
+ 'chat:write.public',
32
+ 'channels:history',
33
+ 'channels:read',
34
+ 'groups:read',
35
+ 'groups:history',
36
+ 'im:history',
37
+ 'im:read',
38
+ 'im:write',
39
+ 'files:read',
40
+ 'files:write',
41
+ 'reactions:read',
42
+ 'reactions:write',
43
+ 'users:read',
44
+ ],
45
+ },
46
+ },
47
+ settings: {
48
+ event_subscriptions: {
49
+ bot_events: [
50
+ 'message.channels',
51
+ 'message.groups',
52
+ 'message.im',
53
+ 'reaction_added',
54
+ 'reaction_removed',
55
+ ],
56
+ },
57
+ interactivity: {
58
+ is_enabled: false,
59
+ },
60
+ org_deploy_enabled: false,
61
+ socket_mode_enabled: true,
62
+ token_rotation_enabled: false,
63
+ },
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Generate a URL that opens Slack's "Create App" page with the manifest pre-filled.
69
+ */
70
+ export function generateManifestUrl(botName: string): string {
71
+ const manifest = generateManifest(botName);
72
+ const encoded = encodeURIComponent(JSON.stringify(manifest));
73
+ return `https://api.slack.com/apps?new_app=1&manifest_json=${encoded}`;
74
+ }
75
+
76
+ /**
77
+ * Validate a Slack bot token by calling auth.test.
78
+ * Returns bot info on success or null on failure.
79
+ */
80
+ export async function validateSlackToken(token: string): Promise<{
81
+ ok: boolean;
82
+ userId?: string;
83
+ botName?: string;
84
+ teamName?: string;
85
+ error?: string;
86
+ }> {
87
+ try {
88
+ const resp = await fetch('https://slack.com/api/auth.test', {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Authorization': `Bearer ${token}`,
92
+ 'Content-Type': 'application/x-www-form-urlencoded',
93
+ },
94
+ });
95
+ if (!resp.ok) return { ok: false, error: `HTTP ${resp.status}` };
96
+
97
+ const data = await resp.json() as any;
98
+ if (!data.ok) return { ok: false, error: data.error };
99
+
100
+ return {
101
+ ok: true,
102
+ userId: data.user_id,
103
+ botName: data.user,
104
+ teamName: data.team,
105
+ };
106
+ } catch (err: any) {
107
+ return { ok: false, error: err.message };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Validate a Slack app-level token by attempting a connections.open.
113
+ * This confirms Socket Mode will work.
114
+ */
115
+ export async function validateAppToken(appToken: string): Promise<{
116
+ ok: boolean;
117
+ error?: string;
118
+ }> {
119
+ try {
120
+ const resp = await fetch('https://slack.com/api/apps.connections.open', {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Authorization': `Bearer ${appToken}`,
124
+ 'Content-Type': 'application/x-www-form-urlencoded',
125
+ },
126
+ });
127
+ if (!resp.ok) return { ok: false, error: `HTTP ${resp.status}` };
128
+
129
+ const data = await resp.json() as any;
130
+ if (!data.ok) return { ok: false, error: data.error };
131
+
132
+ return { ok: true };
133
+ } catch (err: any) {
134
+ return { ok: false, error: err.message };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Resolve a Slack username/handle to a user ID.
140
+ * Uses the users.list API with pagination to find a matching user.
141
+ * Handle is case-insensitive with leading @ stripped.
142
+ */
143
+ export async function resolveSlackUser(botToken: string, handle: string): Promise<{
144
+ userId: string | null;
145
+ displayName?: string;
146
+ error?: string;
147
+ }> {
148
+ const normalized = handle.replace(/^@/, '').toLowerCase();
149
+ let cursor: string | undefined;
150
+
151
+ try {
152
+ do {
153
+ const params = new URLSearchParams({ limit: '200' });
154
+ if (cursor) params.set('cursor', cursor);
155
+
156
+ const resp = await fetch(`https://slack.com/api/users.list?${params}`, {
157
+ headers: { 'Authorization': `Bearer ${botToken}` },
158
+ });
159
+ if (!resp.ok) return { userId: null, error: `HTTP ${resp.status}` };
160
+
161
+ const data = await resp.json() as any;
162
+ if (!data.ok) return { userId: null, error: data.error };
163
+
164
+ for (const member of data.members ?? []) {
165
+ if (member.deleted || member.is_bot) continue;
166
+ const name = (member.name ?? '').toLowerCase();
167
+ // Prefer unique handle (member.name) over display/real name for security
168
+ if (name === normalized) {
169
+ return { userId: member.id, displayName: member.profile?.display_name || member.real_name || member.name };
170
+ }
171
+ }
172
+
173
+ // Second pass: try display_name and real_name (less reliable, not unique)
174
+ for (const member of data.members ?? []) {
175
+ if (member.deleted || member.is_bot) continue;
176
+ const displayName = member.profile?.display_name_normalized?.toLowerCase() ?? '';
177
+ const realName = member.profile?.real_name_normalized?.toLowerCase() ?? '';
178
+ if (displayName === normalized || realName === normalized) {
179
+ return { userId: member.id, displayName: member.profile?.display_name || member.real_name || member.name };
180
+ }
181
+ }
182
+
183
+ cursor = data.response_metadata?.next_cursor || undefined;
184
+ } while (cursor);
185
+
186
+ return { userId: null, error: `User "${handle}" not found` };
187
+ } catch (err: any) {
188
+ return { userId: null, error: err.message };
189
+ }
190
+ }
@@ -1,6 +1,6 @@
1
1
  # Admin Agent — copilot-bridge
2
2
 
3
- You are the **admin agent** for copilot-bridge, a service that bridges GitHub Copilot CLI to messaging platforms (e.g., Mattermost).
3
+ You are the **admin agent** for copilot-bridge, a service that bridges GitHub Copilot CLI to messaging platforms (e.g., Mattermost, Slack).
4
4
 
5
5
  **Source repo**: https://github.com/ChrisRomp/copilot-bridge
6
6
  **Bridge config**: `~/.copilot-bridge/config.json` (resolution: `COPILOT_BRIDGE_CONFIG` env → `~/.copilot-bridge/config.json` → `cwd/config.json`)
@@ -12,7 +12,7 @@ You are a bot — use **it/its** pronouns when referring to yourself or other bo
12
12
 
13
13
  ## How You Communicate
14
14
 
15
- - You receive messages from a chat platform (Mattermost)
15
+ - You receive messages from a chat platform (e.g., Mattermost, Slack)
16
16
  - Your responses are streamed back to the same channel
17
17
  - Slash commands (e.g., `/new`, `/model`, `/verbose`) are intercepted by the bridge — you won't see them
18
18
  - The user may be on mobile; keep responses concise when possible
@@ -181,9 +181,9 @@ A bridge restart is needed for removals to take effect.
181
181
  ## Bridge Architecture (Reference)
182
182
 
183
183
  ```
184
- Mattermost Channel → copilot-bridge → @github/copilot-sdk → Copilot CLI
185
-
186
- └──────────── streaming response (edit-in-place) ←─────────┘
184
+ Chat Platform (Mattermost/Slack) → copilot-bridge → @github/copilot-sdk → Copilot CLI
185
+
186
+ └──────────── streaming response (edit-in-place) ←───────────────────┘
187
187
  ```
188
188
 
189
189
  - Each channel maps to a Copilot session with a working directory, model, and optional agent
@@ -14,7 +14,7 @@ You are a bot — use **it/its** pronouns when referring to yourself or other bo
14
14
 
15
15
  ## How You Communicate
16
16
 
17
- - You receive messages from a chat platform (Mattermost)
17
+ - You receive messages from a chat platform (e.g., Mattermost, Slack)
18
18
  - Your responses are streamed back to the same channel
19
19
  - Slash commands (e.g., `/new`, `/model`, `/verbose`) are intercepted by the bridge — you won't see them
20
20
  - The user may be on mobile; keep responses concise when possible