@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.
- package/config.sample.json +8 -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 +55 -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.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/index.js +182 -14
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/scripts/init.ts +322 -117
- 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/slack.ts +190 -0
- package/templates/admin/AGENTS.md +5 -5
- package/templates/agents/AGENTS.md +1 -1
package/scripts/init.ts
CHANGED
|
@@ -9,12 +9,34 @@
|
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { heading, success, warn, fail, info, dim, blank, printCheck } from './lib/output.js';
|
|
12
|
-
import { askRequired, askSecret, confirm, choose, closePrompts } from './lib/prompts.js';
|
|
12
|
+
import { askRequired, askSecret, confirm, choose, pressEnter, closePrompts } from './lib/prompts.js';
|
|
13
13
|
import { runAllPrereqs, checkNodeVersion } from './lib/prerequisites.js';
|
|
14
14
|
import { pingServer, validateBotToken, checkChannelAccess, getChannelInfo } from './lib/mattermost.js';
|
|
15
|
-
import { buildConfig, writeConfig, configExists, getConfigPath, getConfigDir, type BotEntry, type ChannelEntry, type ConfigDefaults } from './lib/config-gen.js';
|
|
15
|
+
import { buildConfig, writeConfig, configExists, getConfigPath, getConfigDir, readExistingConfig, mergeConfig, type BotEntry, type ChannelEntry, type ConfigDefaults } from './lib/config-gen.js';
|
|
16
|
+
import { generateManifestUrl, validateSlackToken, validateAppToken, resolveSlackUser } from './lib/slack.js';
|
|
16
17
|
import { detectPlatform, getServiceStatus } from './lib/service.js';
|
|
17
18
|
|
|
19
|
+
async function promptAllowlist(botName: string): Promise<{ mode: 'allowlist'; users: string[] } | undefined> {
|
|
20
|
+
info(`\nAccess control: who can use "${botName}"?`);
|
|
21
|
+
dim('By default, only users you list here can interact with this bot.\n');
|
|
22
|
+
|
|
23
|
+
let yourHandle = await askRequired('Your username (you\'ll be added to the allowlist)');
|
|
24
|
+
yourHandle = yourHandle.replace(/^@/, '').trim();
|
|
25
|
+
const users = [yourHandle];
|
|
26
|
+
|
|
27
|
+
const extra = await confirm('Add more users to the allowlist?', false);
|
|
28
|
+
if (extra) {
|
|
29
|
+
const moreRaw = await askRequired('Additional usernames (comma-separated)');
|
|
30
|
+
for (const u of moreRaw.split(',')) {
|
|
31
|
+
const trimmed = u.replace(/^@/, '').trim();
|
|
32
|
+
if (trimmed.length > 0) users.push(trimmed);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
success(`Allowlist: ${users.join(', ')}`);
|
|
37
|
+
return { mode: 'allowlist', users };
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
async function main() {
|
|
19
41
|
const isCli = process.env.COPILOT_BRIDGE_CLI === '1';
|
|
20
42
|
console.log();
|
|
@@ -47,164 +69,344 @@ async function main() {
|
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
// --- Check for existing config ---
|
|
50
|
-
|
|
72
|
+
let existingConfig = configExists() ? readExistingConfig() : null;
|
|
73
|
+
let mergeMode = false;
|
|
74
|
+
|
|
75
|
+
if (existingConfig) {
|
|
51
76
|
blank();
|
|
52
77
|
warn(`Existing config found at ${getConfigPath()}`);
|
|
53
|
-
|
|
78
|
+
const existingPlatforms = Object.keys(existingConfig.platforms ?? {});
|
|
79
|
+
info(`Current platforms: ${existingPlatforms.join(', ') || 'none'}`);
|
|
80
|
+
|
|
81
|
+
const action = await choose('What would you like to do?', [
|
|
82
|
+
'Add a platform (keep existing config)',
|
|
83
|
+
'Start fresh (overwrite)',
|
|
84
|
+
'Cancel',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
if (action === 2) {
|
|
54
88
|
info(isCli
|
|
55
89
|
? 'Run "copilot-bridge check" to validate your existing config.'
|
|
56
90
|
: 'Run "npm run check" to validate your existing config.');
|
|
57
91
|
closePrompts();
|
|
58
92
|
process.exit(0);
|
|
59
93
|
}
|
|
94
|
+
mergeMode = action === 0;
|
|
95
|
+
if (!mergeMode) existingConfig = null;
|
|
60
96
|
}
|
|
61
97
|
|
|
62
|
-
// --- Step 2:
|
|
63
|
-
heading('Step 2:
|
|
64
|
-
info('Connect to your Mattermost instance. You\'ll need the URL and a bot token.');
|
|
65
|
-
dim('Create bot accounts in Mattermost: System Console → Integrations → Bot Accounts\n');
|
|
98
|
+
// --- Step 2: Platform selection ---
|
|
99
|
+
heading('Step 2: Platform');
|
|
66
100
|
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
if (!
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
let useMattermost = false;
|
|
102
|
+
let useSlack = false;
|
|
103
|
+
|
|
104
|
+
if (mergeMode && existingConfig) {
|
|
105
|
+
// In merge mode, only offer platforms not yet configured
|
|
106
|
+
const hasMattermost = !!existingConfig.platforms?.mattermost;
|
|
107
|
+
const hasSlack = !!existingConfig.platforms?.slack;
|
|
108
|
+
const available: string[] = [];
|
|
109
|
+
if (!hasMattermost) available.push('Mattermost');
|
|
110
|
+
if (!hasSlack) available.push('Slack');
|
|
111
|
+
|
|
112
|
+
if (available.length === 0) {
|
|
113
|
+
info('Both platforms are already configured.');
|
|
114
|
+
closePrompts();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
} else if (available.length === 1) {
|
|
117
|
+
info(`Adding ${available[0]} to your existing config.`);
|
|
118
|
+
useMattermost = available[0] === 'Mattermost';
|
|
119
|
+
useSlack = available[0] === 'Slack';
|
|
120
|
+
} else {
|
|
121
|
+
const idx = await choose('Which platform to add?', [...available, 'Both']);
|
|
122
|
+
if (idx === available.length) {
|
|
123
|
+
useMattermost = true;
|
|
124
|
+
useSlack = true;
|
|
125
|
+
} else {
|
|
126
|
+
useMattermost = available[idx] === 'Mattermost';
|
|
127
|
+
useSlack = available[idx] === 'Slack';
|
|
128
|
+
}
|
|
79
129
|
}
|
|
130
|
+
} else {
|
|
131
|
+
const platformChoice = await choose('Which chat platform(s)?', [
|
|
132
|
+
'Mattermost',
|
|
133
|
+
'Slack',
|
|
134
|
+
'Both',
|
|
135
|
+
]);
|
|
136
|
+
useMattermost = platformChoice === 0 || platformChoice === 2;
|
|
137
|
+
useSlack = platformChoice === 1 || platformChoice === 2;
|
|
80
138
|
}
|
|
81
139
|
|
|
82
|
-
// --- Step 3:
|
|
83
|
-
|
|
84
|
-
|
|
140
|
+
// --- Step 3: Platform-specific setup ---
|
|
141
|
+
let mmUrl = '';
|
|
85
142
|
const bots: BotEntry[] = [];
|
|
86
|
-
|
|
143
|
+
const slackBots: BotEntry[] = [];
|
|
144
|
+
const channels: ChannelEntry[] = [];
|
|
145
|
+
|
|
146
|
+
// ── Mattermost ──────────────────────────────────────────
|
|
147
|
+
if (useMattermost) {
|
|
148
|
+
heading('Step 3a: Mattermost Connection');
|
|
149
|
+
info('Connect to your Mattermost instance. You\'ll need the URL and a bot token.');
|
|
150
|
+
dim('Create bot accounts in Mattermost: System Console → Integrations → Bot Accounts\n');
|
|
151
|
+
|
|
152
|
+
while (true) {
|
|
153
|
+
mmUrl = await askRequired('Mattermost URL (e.g., https://chat.example.com)');
|
|
154
|
+
mmUrl = mmUrl.replace(/\/+$/, '');
|
|
155
|
+
if (!mmUrl.startsWith('http')) mmUrl = `https://${mmUrl}`;
|
|
156
|
+
|
|
157
|
+
const ping = await pingServer(mmUrl);
|
|
158
|
+
printCheck(ping);
|
|
159
|
+
if (ping.status === 'pass' || ping.status === 'warn') break;
|
|
160
|
+
if (!await confirm('Try a different URL?')) {
|
|
161
|
+
warn('Continuing with unverified URL.');
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
heading('Mattermost Bots');
|
|
167
|
+
let addMore = true;
|
|
168
|
+
while (addMore) {
|
|
169
|
+
if (bots.length === 0) {
|
|
170
|
+
info('Enter the bot token from your Mattermost bot account.');
|
|
171
|
+
dim('You can add more bots later if you want multiple identities.\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const token = await askSecret(`Bot token${bots.length > 0 ? ' (for next bot)' : ''}`);
|
|
175
|
+
const validation = await validateBotToken(mmUrl, token);
|
|
176
|
+
printCheck(validation.result);
|
|
177
|
+
|
|
178
|
+
if (validation.result.status === 'pass' && validation.bot) {
|
|
179
|
+
const isAdmin = validation.bot.roles?.includes('system_admin')
|
|
180
|
+
|| await confirm(`Is "${validation.bot.username}" an admin bot?`, false);
|
|
181
|
+
|
|
182
|
+
bots.push({
|
|
183
|
+
name: validation.bot.username,
|
|
184
|
+
token,
|
|
185
|
+
admin: !!isAdmin,
|
|
186
|
+
});
|
|
187
|
+
success(`Added bot "${validation.bot.username}"${isAdmin ? ' (admin)' : ''}`);
|
|
188
|
+
const access = await promptAllowlist(validation.bot.username);
|
|
189
|
+
if (access) bots[bots.length - 1].access = access;
|
|
190
|
+
} else {
|
|
191
|
+
warn(isCli
|
|
192
|
+
? 'Token validation failed. The token was still added — verify it later with "copilot-bridge check".'
|
|
193
|
+
: 'Token validation failed. The token was still added — verify it later with "npm run check".');
|
|
194
|
+
let name = await askRequired('Bot username (for config)');
|
|
195
|
+
name = name.replace(/^@/, '');
|
|
196
|
+
bots.push({ name, token, admin: false });
|
|
197
|
+
const access = await promptAllowlist(name);
|
|
198
|
+
if (access) bots[bots.length - 1].access = access;
|
|
199
|
+
}
|
|
87
200
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
dim('You can add more bots later if you want multiple identities.\n');
|
|
201
|
+
if (bots.length >= 1) {
|
|
202
|
+
addMore = await confirm('Add another bot?', false);
|
|
203
|
+
}
|
|
92
204
|
}
|
|
93
205
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
206
|
+
// Mattermost channels
|
|
207
|
+
heading('Mattermost Channels');
|
|
208
|
+
info('Direct messages work automatically — no config needed.');
|
|
209
|
+
info('Group channels need their channel ID and a working directory.\n');
|
|
97
210
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
211
|
+
let addChannels = await confirm('Configure group channels now?', false);
|
|
212
|
+
while (addChannels) {
|
|
213
|
+
const channelId = await askRequired('Channel ID (from Mattermost channel settings → View Info)');
|
|
101
214
|
|
|
102
|
-
bots
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
215
|
+
const primaryBot = bots[0];
|
|
216
|
+
const access = await checkChannelAccess(mmUrl, primaryBot.token, channelId);
|
|
217
|
+
printCheck(access);
|
|
218
|
+
|
|
219
|
+
let channelName: string | undefined;
|
|
220
|
+
if (access.status === 'pass') {
|
|
221
|
+
const chInfo = await getChannelInfo(mmUrl, primaryBot.token, channelId);
|
|
222
|
+
channelName = chInfo?.displayName || chInfo?.name;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const workDir = await askRequired('Working directory (absolute path for this channel\'s workspace)');
|
|
226
|
+
if (!fs.existsSync(workDir)) {
|
|
227
|
+
if (await confirm(`Directory "${workDir}" doesn't exist. Create it?`)) {
|
|
228
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
229
|
+
success(`Created ${workDir}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let botName = bots[0].name;
|
|
234
|
+
if (bots.length > 1) {
|
|
235
|
+
const idx = await choose('Which bot for this channel?', bots.map(b => b.name));
|
|
236
|
+
botName = bots[idx].name;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const triggerIdx = await choose('Trigger mode for this channel?', [
|
|
240
|
+
'mention — respond only when @mentioned (recommended)',
|
|
241
|
+
'all — respond to every message',
|
|
242
|
+
]);
|
|
243
|
+
const triggerMode = triggerIdx === 0 ? 'mention' as const : 'all' as const;
|
|
244
|
+
const threadedReplies = await confirm('Reply in threads?', true);
|
|
245
|
+
|
|
246
|
+
channels.push({
|
|
247
|
+
id: channelId,
|
|
248
|
+
name: channelName,
|
|
249
|
+
platform: 'mattermost',
|
|
250
|
+
bot: botName,
|
|
251
|
+
workingDirectory: workDir,
|
|
252
|
+
triggerMode,
|
|
253
|
+
threadedReplies,
|
|
106
254
|
});
|
|
107
|
-
success(`Added
|
|
108
|
-
|
|
109
|
-
warn(isCli
|
|
110
|
-
? 'Token validation failed. The token was still added — verify it later with "copilot-bridge check".'
|
|
111
|
-
: 'Token validation failed. The token was still added — verify it later with "npm run check".');
|
|
112
|
-
let name = await askRequired('Bot username (for config)');
|
|
113
|
-
name = name.replace(/^@/, '');
|
|
114
|
-
bots.push({ name, token, admin: false });
|
|
255
|
+
success(`Added channel${channelName ? ` "${channelName}"` : ''}`);
|
|
256
|
+
addChannels = await confirm('Add another channel?', false);
|
|
115
257
|
}
|
|
116
258
|
|
|
117
|
-
if (
|
|
118
|
-
|
|
259
|
+
if (channels.length === 0) {
|
|
260
|
+
info('No group channels configured. DMs will still work automatically.');
|
|
119
261
|
}
|
|
120
262
|
}
|
|
121
263
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
264
|
+
// ── Slack ───────────────────────────────────────────────
|
|
265
|
+
if (useSlack) {
|
|
266
|
+
heading(useMattermost ? 'Step 3b: Slack Connection' : 'Step 3: Slack Connection');
|
|
267
|
+
info('We\'ll create a Slack app with the right permissions via a manifest URL.');
|
|
268
|
+
blank();
|
|
126
269
|
|
|
127
|
-
|
|
128
|
-
|
|
270
|
+
const botDisplayName = await askRequired('Bot display name for Slack (e.g., copilot)');
|
|
271
|
+
const manifestUrl = generateManifestUrl(botDisplayName);
|
|
129
272
|
|
|
130
|
-
|
|
131
|
-
|
|
273
|
+
info('Open this URL in your browser to create the Slack app:');
|
|
274
|
+
blank();
|
|
275
|
+
console.log(` ${manifestUrl}`);
|
|
276
|
+
blank();
|
|
277
|
+
dim('Steps in Slack:');
|
|
278
|
+
dim(' 1. Click the link above → review the manifest → Create');
|
|
279
|
+
dim(' 2. On the app page, go to "OAuth & Permissions" → Install to Workspace');
|
|
280
|
+
dim(' 3. Copy the "Bot User OAuth Token" (starts with xoxb-)');
|
|
281
|
+
dim(' 4. Go to "Basic Information" → "App-Level Tokens" → Generate Token');
|
|
282
|
+
dim(' Name it anything, add the "connections:write" scope, then Generate');
|
|
283
|
+
dim(' 5. Copy the app-level token (starts with xapp-)');
|
|
284
|
+
blank();
|
|
285
|
+
|
|
286
|
+
await pressEnter('Press Enter when you\'re ready to paste the tokens...');
|
|
132
287
|
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
288
|
+
// Bot token
|
|
289
|
+
const botToken = await askSecret('Bot User OAuth Token (xoxb-...)');
|
|
290
|
+
const tokenResult = await validateSlackToken(botToken);
|
|
291
|
+
if (tokenResult.ok) {
|
|
292
|
+
success(`Authenticated as @${tokenResult.botName} in ${tokenResult.teamName}`);
|
|
293
|
+
} else {
|
|
294
|
+
warn(`Token validation failed: ${tokenResult.error}. Added anyway — verify later.`);
|
|
295
|
+
}
|
|
137
296
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
297
|
+
// App token
|
|
298
|
+
const appToken = await askSecret('App-Level Token (xapp-...)');
|
|
299
|
+
const appResult = await validateAppToken(appToken);
|
|
300
|
+
if (appResult.ok) {
|
|
301
|
+
success('Socket Mode connection verified');
|
|
302
|
+
} else {
|
|
303
|
+
warn(`App token validation failed: ${appResult.error}. Added anyway — verify later.`);
|
|
142
304
|
}
|
|
143
305
|
|
|
144
|
-
const
|
|
306
|
+
const slackBotName = tokenResult.botName ?? botDisplayName;
|
|
307
|
+
const isAdmin = await confirm(`Is "${slackBotName}" an admin bot?`, false);
|
|
145
308
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
309
|
+
slackBots.push({
|
|
310
|
+
name: slackBotName,
|
|
311
|
+
token: botToken,
|
|
312
|
+
appToken,
|
|
313
|
+
admin: isAdmin,
|
|
314
|
+
});
|
|
315
|
+
success(`Added Slack bot "${slackBotName}"${isAdmin ? ' (admin)' : ''}`);
|
|
316
|
+
|
|
317
|
+
// Allowlist — resolve Slack handles to UIDs
|
|
318
|
+
const access = await promptAllowlist(slackBotName);
|
|
319
|
+
if (access) {
|
|
320
|
+
const resolvedUsers: string[] = [];
|
|
321
|
+
for (const handle of access.users) {
|
|
322
|
+
const result = await resolveSlackUser(botToken, handle);
|
|
323
|
+
if (result.userId) {
|
|
324
|
+
success(` Resolved "${handle}" → ${result.userId}${result.displayName ? ` (${result.displayName})` : ''}`);
|
|
325
|
+
resolvedUsers.push(result.userId);
|
|
326
|
+
} else {
|
|
327
|
+
warn(` Could not resolve "${handle}"${result.error ? `: ${result.error}` : ''} — storing as-is`);
|
|
328
|
+
resolvedUsers.push(handle);
|
|
329
|
+
}
|
|
151
330
|
}
|
|
331
|
+
slackBots[slackBots.length - 1].access = { mode: 'allowlist', users: resolvedUsers };
|
|
152
332
|
}
|
|
333
|
+
// Slack channels (DMs auto-discovered, channels optional)
|
|
334
|
+
blank();
|
|
335
|
+
info('Slack DMs work automatically. You can optionally configure specific channels.');
|
|
336
|
+
let addSlackChannels = await confirm('Configure Slack channels now?', false);
|
|
337
|
+
while (addSlackChannels) {
|
|
338
|
+
const channelId = await askRequired('Slack channel ID (right-click channel → View channel details → copy ID at bottom)');
|
|
339
|
+
const workDir = await askRequired('Working directory (absolute path for this channel\'s workspace)');
|
|
340
|
+
if (!fs.existsSync(workDir)) {
|
|
341
|
+
if (await confirm(`Directory "${workDir}" doesn't exist. Create it?`)) {
|
|
342
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
343
|
+
success(`Created ${workDir}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
153
346
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
347
|
+
const triggerIdx = await choose('Trigger mode for this channel?', [
|
|
348
|
+
'mention — respond only when @mentioned (recommended)',
|
|
349
|
+
'all — respond to every message',
|
|
350
|
+
]);
|
|
351
|
+
const triggerMode = triggerIdx === 0 ? 'mention' as const : 'all' as const;
|
|
352
|
+
const threadedReplies = await confirm('Reply in threads?', true);
|
|
353
|
+
|
|
354
|
+
channels.push({
|
|
355
|
+
id: channelId,
|
|
356
|
+
platform: 'slack',
|
|
357
|
+
bot: slackBotName,
|
|
358
|
+
workingDirectory: workDir,
|
|
359
|
+
triggerMode,
|
|
360
|
+
threadedReplies,
|
|
361
|
+
});
|
|
362
|
+
success('Added Slack channel');
|
|
363
|
+
addSlackChannels = await confirm('Add another Slack channel?', false);
|
|
159
364
|
}
|
|
365
|
+
}
|
|
160
366
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
name: channelName,
|
|
164
|
-
platform: 'mattermost',
|
|
165
|
-
bot: botName,
|
|
166
|
-
workingDirectory: workDir,
|
|
167
|
-
});
|
|
168
|
-
success(`Added channel${channelName ? ` "${channelName}"` : ''}`);
|
|
367
|
+
// --- Step 4: Defaults (skip in merge mode — existing config has them) ---
|
|
368
|
+
const defaults: ConfigDefaults = {};
|
|
169
369
|
|
|
170
|
-
|
|
171
|
-
|
|
370
|
+
if (!mergeMode) {
|
|
371
|
+
heading('Step 4: Defaults');
|
|
372
|
+
dim('These can be changed later in config.json or via chat commands.\n');
|
|
373
|
+
|
|
374
|
+
const modelChoice = await choose('Default model?', [
|
|
375
|
+
'claude-sonnet-4.6 (recommended)',
|
|
376
|
+
'claude-opus-4.6 (premium)',
|
|
377
|
+
'claude-haiku-4.5 (fast/cheap)',
|
|
378
|
+
'Other (enter manually)',
|
|
379
|
+
]);
|
|
380
|
+
if (modelChoice === 3) {
|
|
381
|
+
defaults.model = await askRequired('Model name');
|
|
382
|
+
} else {
|
|
383
|
+
defaults.model = ['claude-sonnet-4.6', 'claude-opus-4.6', 'claude-haiku-4.5'][modelChoice];
|
|
384
|
+
}
|
|
172
385
|
|
|
173
|
-
|
|
174
|
-
|
|
386
|
+
const triggerChoice = await choose('Default trigger mode (for group channels — DMs always respond)?', [
|
|
387
|
+
'mention — bot responds only when @mentioned (recommended)',
|
|
388
|
+
'all — bot responds to every message in the channel',
|
|
389
|
+
]);
|
|
390
|
+
defaults.triggerMode = triggerChoice === 0 ? 'mention' : 'all';
|
|
391
|
+
defaults.threadedReplies = await confirm('Reply in threads by default?', true);
|
|
392
|
+
defaults.verbose = await confirm('Verbose mode (show tool calls)?', false);
|
|
175
393
|
}
|
|
176
394
|
|
|
177
|
-
// --- Step 5:
|
|
178
|
-
heading('Step 5:
|
|
179
|
-
dim('These can be changed later in config.json or via chat commands.\n');
|
|
395
|
+
// --- Step 5: Generate config ---
|
|
396
|
+
heading('Step 5: Generate Config');
|
|
180
397
|
|
|
181
|
-
|
|
398
|
+
let finalConfig = buildConfig({ mmUrl: mmUrl || undefined, bots, channels, defaults, slackBots });
|
|
182
399
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
'
|
|
187
|
-
'Other (enter manually)',
|
|
188
|
-
]);
|
|
189
|
-
if (modelChoice === 3) {
|
|
190
|
-
defaults.model = await askRequired('Model name');
|
|
191
|
-
} else {
|
|
192
|
-
defaults.model = ['claude-sonnet-4.6', 'claude-opus-4.6', 'claude-haiku-4.5'][modelChoice];
|
|
400
|
+
// Merge with existing config if in merge mode
|
|
401
|
+
if (mergeMode && existingConfig) {
|
|
402
|
+
finalConfig = mergeConfig(existingConfig, finalConfig);
|
|
403
|
+
info('Merged new platform into existing config.');
|
|
193
404
|
}
|
|
194
405
|
|
|
195
|
-
|
|
196
|
-
'
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
defaults.triggerMode = triggerChoice === 0 ? 'mention' : 'all';
|
|
200
|
-
defaults.threadedReplies = await confirm('Reply in threads by default?', true);
|
|
201
|
-
defaults.verbose = await confirm('Verbose mode (show tool calls)?', false);
|
|
202
|
-
|
|
203
|
-
// --- Step 6: Generate config ---
|
|
204
|
-
heading('Step 6: Generate Config');
|
|
205
|
-
|
|
206
|
-
const config = buildConfig({ mmUrl, bots, channels, defaults });
|
|
207
|
-
const configPath = writeConfig(config);
|
|
406
|
+
if (configExists()) {
|
|
407
|
+
dim('Backing up existing config before writing...');
|
|
408
|
+
}
|
|
409
|
+
const configPath = writeConfig(finalConfig);
|
|
208
410
|
success(`Config written to ${configPath}`);
|
|
209
411
|
|
|
210
412
|
// Ensure workspaces dir exists
|
|
@@ -213,8 +415,8 @@ async function main() {
|
|
|
213
415
|
fs.mkdirSync(workspacesDir, { recursive: true });
|
|
214
416
|
}
|
|
215
417
|
|
|
216
|
-
// --- Step
|
|
217
|
-
heading('Step
|
|
418
|
+
// --- Step 6: Service Setup (Optional) ---
|
|
419
|
+
heading('Step 6: Service Setup (Optional)');
|
|
218
420
|
|
|
219
421
|
const osPlatform = detectPlatform();
|
|
220
422
|
if (osPlatform === 'macos') {
|
|
@@ -235,7 +437,10 @@ async function main() {
|
|
|
235
437
|
heading('✅ Setup Complete');
|
|
236
438
|
blank();
|
|
237
439
|
info(`Config: ${configPath}`);
|
|
238
|
-
|
|
440
|
+
const allBotNames = [...bots.map(b => b.name), ...slackBots.map(b => b.name)];
|
|
441
|
+
info(`Bots: ${allBotNames.join(', ')}`);
|
|
442
|
+
const platforms = [useMattermost ? 'Mattermost' : null, useSlack ? 'Slack' : null].filter(Boolean);
|
|
443
|
+
info(`Platforms: ${platforms.join(', ')}`);
|
|
239
444
|
if (channels.length > 0) info(`Channels: ${channels.length} configured`);
|
|
240
445
|
info('DMs: enabled automatically');
|
|
241
446
|
blank();
|
|
@@ -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
|
+
}
|