@chrisromp/copilot-bridge 0.7.0 → 0.8.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/config.sample.json +9 -1
- package/dist/channels/slack/adapter.d.ts +62 -0
- package/dist/channels/slack/adapter.d.ts.map +1 -0
- package/dist/channels/slack/adapter.js +382 -0
- package/dist/channels/slack/adapter.js.map +1 -0
- package/dist/channels/slack/mrkdwn.d.ts +22 -0
- package/dist/channels/slack/mrkdwn.d.ts.map +1 -0
- package/dist/channels/slack/mrkdwn.js +120 -0
- package/dist/channels/slack/mrkdwn.js.map +1 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +63 -7
- package/dist/config.js.map +1 -1
- package/dist/core/access-control.d.ts +32 -0
- package/dist/core/access-control.d.ts.map +1 -0
- package/dist/core/access-control.js +59 -0
- package/dist/core/access-control.js.map +1 -0
- package/dist/core/command-handler.d.ts +2 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +75 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/inter-agent.d.ts +9 -2
- package/dist/core/inter-agent.d.ts.map +1 -1
- package/dist/core/inter-agent.js +87 -22
- package/dist/core/inter-agent.js.map +1 -1
- package/dist/core/model-fallback.js +1 -1
- package/dist/core/model-fallback.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +51 -13
- package/dist/core/session-manager.js.map +1 -1
- package/dist/index.js +207 -30
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/scripts/check.ts +54 -1
- package/scripts/com.copilot-bridge.plist +5 -2
- package/scripts/init.ts +322 -117
- package/scripts/install-service.ts +32 -1
- package/scripts/lib/config-gen.ts +74 -10
- package/scripts/lib/prerequisites.ts +17 -5
- package/scripts/lib/prompts.ts +4 -0
- package/scripts/lib/service.ts +27 -3
- package/scripts/lib/slack.ts +190 -0
- package/templates/admin/AGENTS.md +5 -5
- package/templates/agents/AGENTS.md +1 -1
|
@@ -12,6 +12,8 @@ export interface BotEntry {
|
|
|
12
12
|
token: string;
|
|
13
13
|
admin: boolean;
|
|
14
14
|
agent?: string;
|
|
15
|
+
appToken?: string; // Slack Socket Mode app-level token
|
|
16
|
+
access?: { mode: 'allowlist' | 'blocklist' | 'open'; users?: string[] };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface ChannelEntry {
|
|
@@ -20,6 +22,8 @@ export interface ChannelEntry {
|
|
|
20
22
|
platform: string;
|
|
21
23
|
bot: string;
|
|
22
24
|
workingDirectory: string;
|
|
25
|
+
triggerMode?: 'all' | 'mention';
|
|
26
|
+
threadedReplies?: boolean;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export interface ConfigDefaults {
|
|
@@ -31,9 +35,12 @@ export interface ConfigDefaults {
|
|
|
31
35
|
|
|
32
36
|
export interface GeneratedConfig {
|
|
33
37
|
platforms: {
|
|
34
|
-
mattermost
|
|
38
|
+
mattermost?: {
|
|
35
39
|
url: string;
|
|
36
|
-
bots?: Record<string, { token: string; admin?: boolean; agent?: string }>;
|
|
40
|
+
bots?: Record<string, { token: string; admin?: boolean; agent?: string; access?: { mode: string; users: string[] } }>;
|
|
41
|
+
};
|
|
42
|
+
slack?: {
|
|
43
|
+
bots?: Record<string, { token: string; appToken: string; admin?: boolean; agent?: string; access?: { mode: string; users: string[] } }>;
|
|
37
44
|
};
|
|
38
45
|
};
|
|
39
46
|
channels: Array<{
|
|
@@ -42,33 +49,51 @@ export interface GeneratedConfig {
|
|
|
42
49
|
platform: string;
|
|
43
50
|
bot?: string;
|
|
44
51
|
workingDirectory: string;
|
|
52
|
+
triggerMode?: string;
|
|
53
|
+
threadedReplies?: boolean;
|
|
45
54
|
}>;
|
|
46
55
|
defaults?: ConfigDefaults;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export function buildConfig(opts: {
|
|
50
|
-
mmUrl
|
|
59
|
+
mmUrl?: string;
|
|
51
60
|
bots: BotEntry[];
|
|
52
61
|
channels: ChannelEntry[];
|
|
53
62
|
defaults?: ConfigDefaults;
|
|
63
|
+
slackBots?: BotEntry[];
|
|
54
64
|
}): GeneratedConfig {
|
|
55
65
|
const config: GeneratedConfig = {
|
|
56
|
-
platforms: {
|
|
57
|
-
mattermost: {
|
|
58
|
-
url: opts.mmUrl,
|
|
59
|
-
},
|
|
60
|
-
},
|
|
66
|
+
platforms: {},
|
|
61
67
|
channels: [],
|
|
62
68
|
};
|
|
63
69
|
|
|
64
|
-
//
|
|
65
|
-
if (opts.bots.length > 0) {
|
|
70
|
+
// Mattermost platform
|
|
71
|
+
if (opts.mmUrl && opts.bots.length > 0) {
|
|
72
|
+
config.platforms.mattermost = { url: opts.mmUrl };
|
|
66
73
|
config.platforms.mattermost.bots = {};
|
|
67
74
|
for (const bot of opts.bots) {
|
|
68
75
|
config.platforms.mattermost.bots[bot.name] = {
|
|
69
76
|
token: bot.token,
|
|
70
77
|
...(bot.admin ? { admin: true } : {}),
|
|
71
78
|
...(bot.agent ? { agent: bot.agent } : {}),
|
|
79
|
+
...(bot.access ? { access: bot.access } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Slack platform
|
|
85
|
+
if (opts.slackBots && opts.slackBots.length > 0) {
|
|
86
|
+
config.platforms.slack = { bots: {} };
|
|
87
|
+
for (const bot of opts.slackBots) {
|
|
88
|
+
if (!bot.appToken) {
|
|
89
|
+
throw new Error(`Slack bot "${bot.name}" is missing required appToken`);
|
|
90
|
+
}
|
|
91
|
+
config.platforms.slack!.bots![bot.name] = {
|
|
92
|
+
token: bot.token,
|
|
93
|
+
appToken: bot.appToken,
|
|
94
|
+
...(bot.admin ? { admin: true } : {}),
|
|
95
|
+
...(bot.agent ? { agent: bot.agent } : {}),
|
|
96
|
+
...(bot.access ? { access: bot.access } : {}),
|
|
72
97
|
};
|
|
73
98
|
}
|
|
74
99
|
}
|
|
@@ -80,6 +105,8 @@ export function buildConfig(opts: {
|
|
|
80
105
|
platform: ch.platform,
|
|
81
106
|
bot: ch.bot,
|
|
82
107
|
workingDirectory: ch.workingDirectory,
|
|
108
|
+
...(ch.triggerMode ? { triggerMode: ch.triggerMode } : {}),
|
|
109
|
+
...(ch.threadedReplies !== undefined ? { threadedReplies: ch.threadedReplies } : {}),
|
|
83
110
|
});
|
|
84
111
|
}
|
|
85
112
|
|
|
@@ -111,6 +138,14 @@ export function writeConfig(config: GeneratedConfig): string {
|
|
|
111
138
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
112
139
|
|
|
113
140
|
const configPath = getConfigPath();
|
|
141
|
+
|
|
142
|
+
// Back up existing config before overwriting
|
|
143
|
+
if (fs.existsSync(configPath)) {
|
|
144
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
145
|
+
const backupPath = `${configPath}.${timestamp}.bak`;
|
|
146
|
+
fs.copyFileSync(configPath, backupPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
114
149
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
115
150
|
return configPath;
|
|
116
151
|
}
|
|
@@ -124,3 +159,32 @@ export function readExistingConfig(): GeneratedConfig | null {
|
|
|
124
159
|
return null;
|
|
125
160
|
}
|
|
126
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Merge a new platform's config into an existing config.
|
|
165
|
+
* Preserves all existing platforms, channels, and defaults.
|
|
166
|
+
*/
|
|
167
|
+
export function mergeConfig(existing: GeneratedConfig, addition: GeneratedConfig): GeneratedConfig {
|
|
168
|
+
const merged: GeneratedConfig = {
|
|
169
|
+
platforms: { ...existing.platforms },
|
|
170
|
+
channels: [...(existing.channels ?? [])],
|
|
171
|
+
defaults: existing.defaults ?? addition.defaults,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Merge new platforms (don't overwrite existing ones)
|
|
175
|
+
for (const [name, config] of Object.entries(addition.platforms)) {
|
|
176
|
+
if (!merged.platforms[name as keyof typeof merged.platforms]) {
|
|
177
|
+
(merged.platforms as any)[name] = config;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Append new channels (skip duplicates by id)
|
|
182
|
+
const existingIds = new Set(merged.channels.map(c => c.id));
|
|
183
|
+
for (const ch of addition.channels ?? []) {
|
|
184
|
+
if (!existingIds.has(ch.id)) {
|
|
185
|
+
merged.channels.push(ch);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return merged;
|
|
190
|
+
}
|
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
88
|
+
detail: 'no token found — set COPILOT_GITHUB_TOKEN, run gh auth login, or run copilot login',
|
|
77
89
|
};
|
|
78
90
|
}
|
|
79
91
|
|
package/scripts/lib/prompts.ts
CHANGED
|
@@ -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++) {
|
package/scripts/lib/service.ts
CHANGED
|
@@ -38,6 +38,10 @@ export function getSystemPath(): string {
|
|
|
38
38
|
return `${nodeBinDir}:${basePath}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function getLogPath(homePath: string): string {
|
|
42
|
+
return path.join(homePath, '.copilot-bridge', 'copilot-bridge.log');
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
// --- launchd (macOS) ---
|
|
42
46
|
|
|
43
47
|
export interface LaunchdConfig {
|
|
@@ -49,6 +53,7 @@ export interface LaunchdConfig {
|
|
|
49
53
|
export function generateLaunchdPlist(config: LaunchdConfig): string {
|
|
50
54
|
const nodePath = getNodePath();
|
|
51
55
|
const tsxPath = path.join(config.bridgePath, 'node_modules', '.bin', 'tsx');
|
|
56
|
+
const logPath = getLogPath(config.homePath);
|
|
52
57
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
53
58
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
54
59
|
<plist version="1.0">
|
|
@@ -83,11 +88,14 @@ export function generateLaunchdPlist(config: LaunchdConfig): string {
|
|
|
83
88
|
<key>ThrottleInterval</key>
|
|
84
89
|
<integer>10</integer>
|
|
85
90
|
|
|
91
|
+
<key>Umask</key>
|
|
92
|
+
<integer>63</integer>
|
|
93
|
+
|
|
86
94
|
<key>StandardOutPath</key>
|
|
87
|
-
<string
|
|
95
|
+
<string>${logPath}</string>
|
|
88
96
|
|
|
89
97
|
<key>StandardErrorPath</key>
|
|
90
|
-
<string
|
|
98
|
+
<string>${logPath}</string>
|
|
91
99
|
</dict>
|
|
92
100
|
</plist>`;
|
|
93
101
|
}
|
|
@@ -111,6 +119,21 @@ export function installLaunchd(plistContent: string): { installed: boolean; path
|
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
export function generateNewsyslogConfig(logPath: string, user: string): string {
|
|
123
|
+
let group = 'staff';
|
|
124
|
+
try { group = execSync('id -gn', { encoding: 'utf-8' }).trim(); } catch { /* default */ }
|
|
125
|
+
// N=no signal, C=create new file after rotation, Z=gzip compress
|
|
126
|
+
// Rotates at 10 MB, keeps 3 archives
|
|
127
|
+
return `# Copilot Bridge log rotation — installed by copilot-bridge install-service
|
|
128
|
+
# logfilename owner:group mode count size(KB) when flags
|
|
129
|
+
${logPath} ${user}:${group} 600 3 10240 * NCZ
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getNewsyslogInstallPath(): string {
|
|
134
|
+
return '/etc/newsyslog.d/copilot-bridge.conf';
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
// --- systemd (Linux) ---
|
|
115
138
|
|
|
116
139
|
export interface SystemdConfig {
|
|
@@ -129,12 +152,13 @@ After=network.target
|
|
|
129
152
|
[Service]
|
|
130
153
|
Type=simple
|
|
131
154
|
User=${config.user}
|
|
132
|
-
ExecStart
|
|
155
|
+
ExecStart="${nodePath}" "${tsxPath}" "${config.bridgePath}/dist/index.js"
|
|
133
156
|
WorkingDirectory=${config.bridgePath}
|
|
134
157
|
Environment=HOME=${config.homePath}
|
|
135
158
|
Environment=PATH=${getSystemPath()}
|
|
136
159
|
Restart=always
|
|
137
160
|
RestartSec=10
|
|
161
|
+
UMask=0077
|
|
138
162
|
|
|
139
163
|
[Install]
|
|
140
164
|
WantedBy=multi-user.target`;
|
|
@@ -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
|
|
185
|
-
|
|
186
|
-
|
|
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
|