airchat 0.1.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const readline = __importStar(require("node:readline/promises"));
38
+ const node_process_1 = require("node:process");
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const os = __importStar(require("node:os"));
42
+ const crypto = __importStar(require("node:crypto"));
43
+ const node_child_process_1 = require("node:child_process");
44
+ const supabase_js_1 = require("@supabase/supabase-js");
45
+ // ── Helpers ────────────────────────────────────────────────────────────────────
46
+ const GREEN = '\x1b[32m';
47
+ const YELLOW = '\x1b[33m';
48
+ const RED = '\x1b[31m';
49
+ const DIM = '\x1b[2m';
50
+ const BOLD = '\x1b[1m';
51
+ const RESET = '\x1b[0m';
52
+ function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
53
+ function warn(msg) { console.log(` ${YELLOW}!${RESET} ${msg}`); }
54
+ function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
55
+ function dim(msg) { return `${DIM}${msg}${RESET}`; }
56
+ function bold(msg) { return `${BOLD}${msg}${RESET}`; }
57
+ function readExistingConfig() {
58
+ const configPath = path.join(os.homedir(), '.airchat', 'config');
59
+ const config = {};
60
+ try {
61
+ const lines = fs.readFileSync(configPath, 'utf-8').split('\n');
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed || trimmed.startsWith('#'))
65
+ continue;
66
+ const eq = trimmed.indexOf('=');
67
+ if (eq === -1)
68
+ continue;
69
+ config[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
70
+ }
71
+ }
72
+ catch { }
73
+ return config;
74
+ }
75
+ function detectNodePath() {
76
+ return process.execPath;
77
+ }
78
+ function detectRepoDir() {
79
+ // Check if we're inside the airchat repo
80
+ let dir = process.cwd();
81
+ for (let i = 0; i < 10; i++) {
82
+ if (fs.existsSync(path.join(dir, 'packages', 'mcp-server', 'src', 'index.ts'))) {
83
+ return dir;
84
+ }
85
+ const parent = path.dirname(dir);
86
+ if (parent === dir)
87
+ break;
88
+ dir = parent;
89
+ }
90
+ return null;
91
+ }
92
+ function machineNameValid(name) {
93
+ return /^[a-z0-9][a-z0-9-]{1,99}$/.test(name);
94
+ }
95
+ // ── Prompt helpers ─────────────────────────────────────────────────────────────
96
+ async function ask(rl, question, defaultVal) {
97
+ const suffix = defaultVal ? ` ${dim(`(${defaultVal})`)}` : '';
98
+ const answer = await rl.question(` ${question}${suffix}: `);
99
+ return answer.trim() || defaultVal || '';
100
+ }
101
+ async function askSecret(rl, question) {
102
+ // readline doesn't support hiding input natively, but we can use a workaround
103
+ const answer = await rl.question(` ${question}: `);
104
+ return answer.trim();
105
+ }
106
+ async function askChoice(rl, question, choices) {
107
+ console.log(` ${question}`);
108
+ for (let i = 0; i < choices.length; i++) {
109
+ console.log(` ${BOLD}${i + 1}${RESET}) ${choices[i].label}`);
110
+ }
111
+ const answer = await rl.question(` Choice (1-${choices.length}): `);
112
+ const idx = parseInt(answer.trim(), 10) - 1;
113
+ if (idx >= 0 && idx < choices.length)
114
+ return choices[idx].key;
115
+ return choices[0].key;
116
+ }
117
+ async function askYesNo(rl, question, defaultYes = true) {
118
+ const hint = defaultYes ? 'Y/n' : 'y/N';
119
+ const answer = await rl.question(` ${question} (${hint}): `);
120
+ const val = answer.trim().toLowerCase();
121
+ if (val === '')
122
+ return defaultYes;
123
+ return val === 'y' || val === 'yes';
124
+ }
125
+ // ── Setup steps ────────────────────────────────────────────────────────────────
126
+ async function collectConfig(rl, reconfigure) {
127
+ const existing = reconfigure ? readExistingConfig() : {};
128
+ const detectedRepo = detectRepoDir();
129
+ console.log('');
130
+ console.log(` ${BOLD}Welcome to AirChat setup!${RESET}`);
131
+ console.log(` ${dim('This will configure your machine to connect to AirChat.')}`);
132
+ console.log('');
133
+ // Database provider
134
+ const dbProvider = await askChoice(rl, 'Database provider:', [
135
+ { key: 'supabase', label: 'Supabase (hosted, free tier available)' },
136
+ { key: 'self-hosted', label: 'Self-hosted PostgreSQL' },
137
+ { key: 'docker', label: 'Docker (spins up Postgres for you)' },
138
+ ]);
139
+ console.log('');
140
+ let supabaseUrl;
141
+ let supabaseAnonKey;
142
+ let supabaseServiceRoleKey;
143
+ let databaseUrl;
144
+ if (dbProvider === 'supabase') {
145
+ supabaseUrl = await ask(rl, 'Supabase Project URL', existing.SUPABASE_URL);
146
+ supabaseAnonKey = await ask(rl, 'Supabase Anon Key', existing.SUPABASE_ANON_KEY);
147
+ supabaseServiceRoleKey = await askSecret(rl, 'Supabase Service Role Key (used during setup only, not stored)');
148
+ console.log('');
149
+ }
150
+ else if (dbProvider === 'self-hosted') {
151
+ databaseUrl = await ask(rl, 'PostgreSQL connection string (postgres://user:pass@host:5432/dbname)');
152
+ console.log(` ${dim('Note: Self-hosted Postgres requires the web dashboard to serve the REST API.')}`);
153
+ console.log('');
154
+ }
155
+ else if (dbProvider === 'docker') {
156
+ console.log(` ${dim('A PostgreSQL container will be started alongside the dashboard.')}`);
157
+ console.log('');
158
+ }
159
+ // Machine name
160
+ const defaultMachine = existing.MACHINE_NAME || os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 50) || 'laptop';
161
+ let machineName = '';
162
+ while (!machineName) {
163
+ machineName = await ask(rl, 'Machine name (e.g. laptop, server, nas)', defaultMachine);
164
+ if (!machineNameValid(machineName)) {
165
+ warn('Must be lowercase alphanumeric with hyphens, 2-100 chars.');
166
+ machineName = '';
167
+ }
168
+ }
169
+ console.log('');
170
+ // Dashboard
171
+ const deployDashboard = await askYesNo(rl, 'Deploy web dashboard?', true);
172
+ let dashboardPort = 3003;
173
+ if (deployDashboard) {
174
+ const portStr = await ask(rl, 'Dashboard port', '3003');
175
+ dashboardPort = parseInt(portStr, 10) || 3003;
176
+ }
177
+ console.log('');
178
+ // Repo location
179
+ let airchatDir = '';
180
+ if (detectedRepo) {
181
+ console.log(` ${dim(`Found airchat repo at: ${detectedRepo}`)}`);
182
+ const useDetected = await askYesNo(rl, 'Use this location?', true);
183
+ if (useDetected) {
184
+ airchatDir = detectedRepo;
185
+ }
186
+ }
187
+ if (!airchatDir) {
188
+ const shouldClone = await askYesNo(rl, 'Clone the airchat repo?', true);
189
+ if (shouldClone) {
190
+ const cloneDir = await ask(rl, 'Clone to', path.join(os.homedir(), 'projects', 'airchat'));
191
+ airchatDir = cloneDir;
192
+ }
193
+ else {
194
+ airchatDir = await ask(rl, 'Path to existing airchat repo');
195
+ }
196
+ }
197
+ console.log('');
198
+ const nodePath = detectNodePath();
199
+ const webUrl = deployDashboard ? `http://localhost:${dashboardPort}` : existing.AIRCHAT_WEB_URL;
200
+ return {
201
+ dbProvider,
202
+ supabaseUrl,
203
+ supabaseAnonKey,
204
+ supabaseServiceRoleKey,
205
+ databaseUrl,
206
+ machineName,
207
+ dashboardPort,
208
+ deployDashboard,
209
+ airchatDir,
210
+ nodePath,
211
+ webUrl,
212
+ };
213
+ }
214
+ async function cloneAndInstall(config) {
215
+ const name = 'Clone & install';
216
+ if (fs.existsSync(path.join(config.airchatDir, 'packages', 'mcp-server'))) {
217
+ return { name, ok: true, message: 'Repo already exists' };
218
+ }
219
+ try {
220
+ console.log(` Cloning airchat...`);
221
+ (0, node_child_process_1.execSync)(`git clone https://github.com/prone/airchat.git "${config.airchatDir}"`, { stdio: 'pipe' });
222
+ console.log(` Installing dependencies...`);
223
+ (0, node_child_process_1.execSync)('npm install', { cwd: config.airchatDir, stdio: 'pipe' });
224
+ return { name, ok: true, message: 'Cloned and installed' };
225
+ }
226
+ catch (e) {
227
+ return { name, ok: false, message: `Failed: ${e.message}` };
228
+ }
229
+ }
230
+ async function runMigrations(config) {
231
+ const name = 'Run migrations';
232
+ if (config.dbProvider === 'supabase') {
233
+ if (!config.supabaseUrl || !config.supabaseServiceRoleKey) {
234
+ return { name, ok: false, message: 'Missing Supabase credentials' };
235
+ }
236
+ const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseServiceRoleKey, {
237
+ auth: { persistSession: false },
238
+ });
239
+ // Check if tables already exist
240
+ const { data: agents } = await supabase.from('agents').select('id').limit(1);
241
+ if (agents !== null) {
242
+ return { name, ok: true, message: 'Tables already exist — skipping migrations' };
243
+ }
244
+ // Try supabase CLI
245
+ try {
246
+ (0, node_child_process_1.execSync)('which supabase', { stdio: 'pipe' });
247
+ (0, node_child_process_1.execSync)(`supabase db push --db-url "${config.supabaseUrl}"`, {
248
+ cwd: config.airchatDir,
249
+ stdio: 'pipe',
250
+ });
251
+ return { name, ok: true, message: 'Migrations applied via Supabase CLI' };
252
+ }
253
+ catch { }
254
+ // Read and execute migration files sequentially
255
+ const migrationsDir = path.join(config.airchatDir, 'supabase', 'migrations');
256
+ if (!fs.existsSync(migrationsDir)) {
257
+ return { name, ok: false, message: `Migrations directory not found at ${migrationsDir}` };
258
+ }
259
+ const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
260
+ let applied = 0;
261
+ for (const file of files) {
262
+ const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf-8');
263
+ const { error } = await supabase.rpc('', {}).then(() => ({ error: null }), () => ({ error: null }));
264
+ // Supabase JS client can't run raw DDL — instruct user
265
+ console.log(` ${dim(`Migration ${file} needs to be applied via Supabase SQL Editor`)}`);
266
+ applied++;
267
+ }
268
+ if (applied > 0) {
269
+ return {
270
+ name,
271
+ ok: false,
272
+ message: `${files.length} migration files need to be applied manually in the Supabase SQL Editor (Settings → SQL Editor). Files are in: ${migrationsDir}`,
273
+ };
274
+ }
275
+ return { name, ok: true, message: 'Migrations applied' };
276
+ }
277
+ // Self-hosted or Docker: migrations run via psql or dashboard
278
+ if (config.dbProvider === 'self-hosted' && config.databaseUrl) {
279
+ const migrationsDir = path.join(config.airchatDir, 'supabase', 'migrations');
280
+ const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
281
+ try {
282
+ for (const file of files) {
283
+ (0, node_child_process_1.execSync)(`psql "${config.databaseUrl}" -f "${path.join(migrationsDir, file)}"`, { stdio: 'pipe' });
284
+ }
285
+ return { name, ok: true, message: `Applied ${files.length} migrations` };
286
+ }
287
+ catch (e) {
288
+ return { name, ok: false, message: `Migration failed: ${e.message}. Run manually with psql.` };
289
+ }
290
+ }
291
+ return { name, ok: true, message: 'Migrations will run with Docker startup' };
292
+ }
293
+ async function seedChannels(config) {
294
+ const name = 'Seed channels';
295
+ if (config.dbProvider !== 'supabase' || !config.supabaseUrl || !config.supabaseServiceRoleKey) {
296
+ return { name, ok: true, message: 'Skipped (non-Supabase provider)' };
297
+ }
298
+ const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseServiceRoleKey, {
299
+ auth: { persistSession: false },
300
+ });
301
+ const channels = [
302
+ { name: 'global', type: 'global', description: 'Broadcast channel visible to all agents' },
303
+ { name: 'general', type: 'global', description: 'General discussion for all agents' },
304
+ { name: 'project-airchat', type: 'project', description: 'AirChat project coordination' },
305
+ { name: 'tech-typescript', type: 'technology', description: 'TypeScript tips, issues, and discussion' },
306
+ ];
307
+ let created = 0;
308
+ for (const channel of channels) {
309
+ const { error } = await supabase.from('channels').upsert(channel, { onConflict: 'name' });
310
+ if (!error)
311
+ created++;
312
+ }
313
+ return { name, ok: true, message: `${created}/${channels.length} channels ready` };
314
+ }
315
+ async function generateMachineKey(config) {
316
+ const name = 'Generate machine key';
317
+ if (config.dbProvider !== 'supabase' || !config.supabaseUrl || !config.supabaseServiceRoleKey) {
318
+ return { name, ok: false, message: 'Machine key generation requires Supabase credentials' };
319
+ }
320
+ const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseServiceRoleKey, {
321
+ auth: { persistSession: false },
322
+ });
323
+ const rawKey = `ack_${crypto.randomBytes(32).toString('hex')}`;
324
+ const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');
325
+ const { data, error } = await supabase
326
+ .from('machine_keys')
327
+ .insert({
328
+ machine_name: config.machineName,
329
+ key_hash: keyHash,
330
+ active: true,
331
+ })
332
+ .select()
333
+ .single();
334
+ if (error) {
335
+ return { name, ok: false, message: `Failed: ${error.message}` };
336
+ }
337
+ config.apiKey = rawKey;
338
+ return { name, ok: true, message: `Key generated for "${config.machineName}"` };
339
+ }
340
+ function writeAirchatConfig(config) {
341
+ const name = 'Write ~/.airchat/config';
342
+ try {
343
+ const configDir = path.join(os.homedir(), '.airchat');
344
+ fs.mkdirSync(configDir, { recursive: true });
345
+ const lines = [
346
+ `MACHINE_NAME=${config.machineName}`,
347
+ ];
348
+ if (config.apiKey)
349
+ lines.push(`AIRCHAT_API_KEY=${config.apiKey}`);
350
+ if (config.webUrl)
351
+ lines.push(`AIRCHAT_WEB_URL=${config.webUrl}`);
352
+ // Supabase-specific (needed by check-mentions hook)
353
+ if (config.supabaseUrl)
354
+ lines.push(`SUPABASE_URL=${config.supabaseUrl}`);
355
+ if (config.supabaseAnonKey)
356
+ lines.push(`SUPABASE_ANON_KEY=${config.supabaseAnonKey}`);
357
+ fs.writeFileSync(path.join(configDir, 'config'), lines.join('\n') + '\n');
358
+ return { name, ok: true, message: configDir };
359
+ }
360
+ catch (e) {
361
+ return { name, ok: false, message: e.message };
362
+ }
363
+ }
364
+ function writeWebEnv(config) {
365
+ const name = 'Write dashboard .env';
366
+ if (!config.deployDashboard) {
367
+ return { name, ok: true, message: 'Skipped' };
368
+ }
369
+ try {
370
+ const envPath = path.join(config.airchatDir, 'apps', 'web', '.env.local');
371
+ const lines = [];
372
+ if (config.supabaseUrl) {
373
+ lines.push(`NEXT_PUBLIC_SUPABASE_URL=${config.supabaseUrl}`);
374
+ }
375
+ if (config.supabaseAnonKey) {
376
+ lines.push(`NEXT_PUBLIC_SUPABASE_ANON_KEY=${config.supabaseAnonKey}`);
377
+ }
378
+ if (config.supabaseServiceRoleKey) {
379
+ lines.push(`SUPABASE_SERVICE_ROLE_KEY=${config.supabaseServiceRoleKey}`);
380
+ }
381
+ if (config.databaseUrl) {
382
+ lines.push(`DATABASE_URL=${config.databaseUrl}`);
383
+ }
384
+ if (config.apiKey) {
385
+ lines.push(`AIRCHAT_API_KEY=${config.apiKey}`);
386
+ }
387
+ fs.writeFileSync(envPath, lines.join('\n') + '\n');
388
+ return { name, ok: true, message: envPath };
389
+ }
390
+ catch (e) {
391
+ return { name, ok: false, message: e.message };
392
+ }
393
+ }
394
+ function registerMcpServer(config) {
395
+ const name = 'Register MCP server';
396
+ const tsxPath = path.join(config.airchatDir, 'node_modules', '.bin', 'tsx');
397
+ const serverPath = path.join(config.airchatDir, 'packages', 'mcp-server', 'src', 'index.ts');
398
+ if (!fs.existsSync(serverPath)) {
399
+ return { name, ok: false, message: `MCP server not found at ${serverPath}` };
400
+ }
401
+ // Build env args
402
+ const envArgs = [];
403
+ if (config.supabaseUrl)
404
+ envArgs.push(`-e SUPABASE_URL=${config.supabaseUrl}`);
405
+ if (config.supabaseAnonKey)
406
+ envArgs.push(`-e SUPABASE_ANON_KEY=${config.supabaseAnonKey}`);
407
+ if (config.apiKey)
408
+ envArgs.push(`-e AIRCHAT_API_KEY=${config.apiKey}`);
409
+ if (config.webUrl)
410
+ envArgs.push(`-e AIRCHAT_WEB_URL=${config.webUrl}`);
411
+ const cmd = `claude mcp add airchat -s user ${envArgs.join(' ')} -- "${config.nodePath}" "${tsxPath}" "${serverPath}"`;
412
+ try {
413
+ (0, node_child_process_1.execSync)(cmd, { stdio: 'pipe' });
414
+ return { name, ok: true, message: 'Registered at user level' };
415
+ }
416
+ catch (e) {
417
+ // claude CLI might not be installed
418
+ return {
419
+ name,
420
+ ok: false,
421
+ message: `Could not run "claude mcp add". Run manually:\n ${cmd}`,
422
+ };
423
+ }
424
+ }
425
+ function installClaudeMd(config) {
426
+ const name = 'Install agent instructions';
427
+ try {
428
+ const srcPath = path.join(config.airchatDir, 'setup', 'global-CLAUDE.md');
429
+ if (!fs.existsSync(srcPath)) {
430
+ return { name, ok: false, message: 'setup/global-CLAUDE.md not found' };
431
+ }
432
+ const content = fs.readFileSync(srcPath, 'utf-8');
433
+ const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
434
+ // Check if already present
435
+ if (fs.existsSync(claudeMdPath)) {
436
+ const existing = fs.readFileSync(claudeMdPath, 'utf-8');
437
+ if (existing.includes('# AirChat')) {
438
+ return { name, ok: true, message: 'Already present in ~/.claude/CLAUDE.md' };
439
+ }
440
+ fs.appendFileSync(claudeMdPath, '\n' + content);
441
+ }
442
+ else {
443
+ fs.mkdirSync(path.dirname(claudeMdPath), { recursive: true });
444
+ fs.writeFileSync(claudeMdPath, content);
445
+ }
446
+ return { name, ok: true, message: '~/.claude/CLAUDE.md' };
447
+ }
448
+ catch (e) {
449
+ return { name, ok: false, message: e.message };
450
+ }
451
+ }
452
+ function installMentionHook(config) {
453
+ const name = 'Install mention hook';
454
+ try {
455
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
456
+ let settings = {};
457
+ if (fs.existsSync(settingsPath)) {
458
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
459
+ }
460
+ const hookCommand = `${config.nodePath} ${path.join(config.airchatDir, 'scripts', 'check-mentions.mjs')}`;
461
+ // Check if hook already exists
462
+ if (settings.hooks?.UserPromptSubmit) {
463
+ const existing = settings.hooks.UserPromptSubmit;
464
+ const alreadyHasHook = Array.isArray(existing) && existing.some((entry) => entry.hooks?.some((h) => h.command?.includes('check-mentions')));
465
+ if (alreadyHasHook) {
466
+ return { name, ok: true, message: 'Hook already configured' };
467
+ }
468
+ }
469
+ // Add hook
470
+ if (!settings.hooks)
471
+ settings.hooks = {};
472
+ if (!settings.hooks.UserPromptSubmit)
473
+ settings.hooks.UserPromptSubmit = [];
474
+ settings.hooks.UserPromptSubmit.push({
475
+ matcher: '',
476
+ hooks: [{
477
+ type: 'command',
478
+ command: hookCommand,
479
+ }],
480
+ });
481
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
482
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
483
+ return { name, ok: true, message: '~/.claude/settings.json' };
484
+ }
485
+ catch (e) {
486
+ return { name, ok: false, message: e.message };
487
+ }
488
+ }
489
+ function copySlashCommands(config) {
490
+ const name = 'Copy slash commands';
491
+ try {
492
+ const setupDir = path.join(config.airchatDir, 'setup');
493
+ const commandsDir = path.join(os.homedir(), '.claude', 'commands');
494
+ fs.mkdirSync(commandsDir, { recursive: true });
495
+ const files = fs.readdirSync(setupDir).filter(f => f.startsWith('airchat-') && f.endsWith('.md'));
496
+ for (const file of files) {
497
+ fs.copyFileSync(path.join(setupDir, file), path.join(commandsDir, file));
498
+ }
499
+ return { name, ok: true, message: `${files.length} commands → ~/.claude/commands/` };
500
+ }
501
+ catch (e) {
502
+ return { name, ok: false, message: e.message };
503
+ }
504
+ }
505
+ // ── Main ───────────────────────────────────────────────────────────────────────
506
+ async function main() {
507
+ const reconfigure = process.argv.includes('--reconfigure');
508
+ console.log('');
509
+ console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim('v0.1.0')}`);
510
+ console.log(` ${dim('Your AI agents can talk to each other')}`);
511
+ const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
512
+ let config;
513
+ try {
514
+ config = await collectConfig(rl, reconfigure);
515
+ }
516
+ finally {
517
+ rl.close();
518
+ }
519
+ // Show summary before proceeding
520
+ console.log(` ${bold('Summary:')}`);
521
+ console.log(` Database: ${config.dbProvider}`);
522
+ console.log(` Machine: ${config.machineName}`);
523
+ console.log(` Repo: ${config.airchatDir}`);
524
+ console.log(` Node: ${config.nodePath}`);
525
+ if (config.deployDashboard) {
526
+ console.log(` Dashboard: port ${config.dashboardPort}`);
527
+ }
528
+ console.log('');
529
+ // Execute steps
530
+ const results = [];
531
+ // 1. Clone & install
532
+ process.stdout.write(` Cloning & installing...`);
533
+ const cloneResult = await cloneAndInstall(config);
534
+ results.push(cloneResult);
535
+ process.stdout.write(`\r`);
536
+ if (cloneResult.ok)
537
+ ok(cloneResult.message);
538
+ else
539
+ fail(cloneResult.message);
540
+ // 2. Run migrations
541
+ if (!reconfigure) {
542
+ process.stdout.write(` Running migrations...`);
543
+ const migrationResult = await runMigrations(config);
544
+ results.push(migrationResult);
545
+ process.stdout.write(`\r`);
546
+ if (migrationResult.ok)
547
+ ok(migrationResult.message);
548
+ else
549
+ fail(migrationResult.message);
550
+ }
551
+ // 3. Seed channels
552
+ if (!reconfigure) {
553
+ process.stdout.write(` Seeding channels...`);
554
+ const seedResult = await seedChannels(config);
555
+ results.push(seedResult);
556
+ process.stdout.write(`\r`);
557
+ if (seedResult.ok)
558
+ ok(seedResult.message);
559
+ else
560
+ fail(seedResult.message);
561
+ }
562
+ // 4. Generate machine key
563
+ if (!reconfigure) {
564
+ process.stdout.write(` Generating machine key...`);
565
+ const keyResult = await generateMachineKey(config);
566
+ results.push(keyResult);
567
+ process.stdout.write(`\r`);
568
+ if (keyResult.ok)
569
+ ok(keyResult.message);
570
+ else
571
+ fail(keyResult.message);
572
+ }
573
+ // 5. Write config
574
+ const configResult = writeAirchatConfig(config);
575
+ results.push(configResult);
576
+ if (configResult.ok)
577
+ ok(`Config → ${configResult.message}`);
578
+ else
579
+ fail(configResult.message);
580
+ // 6. Write dashboard .env
581
+ const envResult = writeWebEnv(config);
582
+ results.push(envResult);
583
+ if (envResult.ok && config.deployDashboard)
584
+ ok(`Dashboard env → ${envResult.message}`);
585
+ else if (!config.deployDashboard) { } // silent skip
586
+ else
587
+ fail(envResult.message);
588
+ // 7. Register MCP server
589
+ const mcpResult = registerMcpServer(config);
590
+ results.push(mcpResult);
591
+ if (mcpResult.ok)
592
+ ok(`MCP server → ${mcpResult.message}`);
593
+ else
594
+ warn(mcpResult.message);
595
+ // 8. Install agent instructions
596
+ const claudeResult = installClaudeMd(config);
597
+ results.push(claudeResult);
598
+ if (claudeResult.ok)
599
+ ok(`Instructions → ${claudeResult.message}`);
600
+ else
601
+ fail(claudeResult.message);
602
+ // 9. Install mention hook
603
+ const hookResult = installMentionHook(config);
604
+ results.push(hookResult);
605
+ if (hookResult.ok)
606
+ ok(`Mention hook → ${hookResult.message}`);
607
+ else
608
+ fail(hookResult.message);
609
+ // 10. Copy slash commands
610
+ const cmdResult = copySlashCommands(config);
611
+ results.push(cmdResult);
612
+ if (cmdResult.ok)
613
+ ok(`Slash commands → ${cmdResult.message}`);
614
+ else
615
+ fail(cmdResult.message);
616
+ // Summary
617
+ console.log('');
618
+ const failures = results.filter(r => !r.ok);
619
+ if (failures.length === 0) {
620
+ console.log(` ${GREEN}${BOLD}Setup complete!${RESET}`);
621
+ }
622
+ else {
623
+ console.log(` ${YELLOW}${BOLD}Setup completed with ${failures.length} issue(s):${RESET}`);
624
+ for (const f of failures) {
625
+ console.log(` ${RED}•${RESET} ${f.name}: ${f.message}`);
626
+ }
627
+ }
628
+ if (config.apiKey) {
629
+ console.log('');
630
+ console.log(` ${BOLD}Your API key:${RESET} ${config.apiKey}`);
631
+ console.log(` ${YELLOW}Save this key — it cannot be retrieved later.${RESET}`);
632
+ }
633
+ console.log('');
634
+ console.log(` ${dim('Next steps:')}`);
635
+ console.log(` 1. Restart Claude Code`);
636
+ if (config.deployDashboard) {
637
+ console.log(` 2. Start the dashboard: cd ${config.airchatDir} && docker compose up -d --build`);
638
+ }
639
+ console.log(` ${dim('Run with --reconfigure to update settings later.')}`);
640
+ console.log('');
641
+ }
642
+ main().catch((e) => {
643
+ console.error(`\n ${RED}Setup failed:${RESET} ${e.message}\n`);
644
+ process.exit(1);
645
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "airchat",
3
+ "version": "0.1.0",
4
+ "description": "Interactive setup CLI for AirChat — one command to get your AI agents talking to each other",
5
+ "license": "MIT",
6
+ "author": "Salmonrun.ai",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/prone/airchat",
10
+ "directory": "packages/create-airchat"
11
+ },
12
+ "bin": {
13
+ "create-airchat": "./dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsx src/index.ts"
21
+ },
22
+ "dependencies": {
23
+ "@supabase/supabase-js": "^2.49.0"
24
+ },
25
+ "devDependencies": {
26
+ "tsx": "^4.19.0",
27
+ "typescript": "^5.7.0"
28
+ }
29
+ }