airchat 0.1.1 → 0.2.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 (2) hide show
  1. package/dist/index.js +106 -72
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -92,6 +92,21 @@ function detectRepoDir() {
92
92
  function machineNameValid(name) {
93
93
  return /^[a-z0-9][a-z0-9-]{1,99}$/.test(name);
94
94
  }
95
+ // ── Ed25519 Keypair Generation ─────────────────────────────────────────────────
96
+ // Inline implementation to avoid importing from @airchat/shared at runtime
97
+ // (the setup CLI may run before the repo is cloned). Uses the same algorithm
98
+ // as generateKeypair() in packages/shared/src/crypto.ts.
99
+ function generateKeypair() {
100
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
101
+ // Export raw 32-byte keys in hex
102
+ const pubRaw = publicKey.export({ type: 'spki', format: 'der' });
103
+ // SPKI DER for Ed25519 is 44 bytes: 12-byte header + 32-byte key
104
+ const pubHex = pubRaw.subarray(pubRaw.length - 32).toString('hex');
105
+ const privRaw = privateKey.export({ type: 'pkcs8', format: 'der' });
106
+ // PKCS8 DER for Ed25519 is 48 bytes: 16-byte header + 32-byte seed
107
+ const privHex = privRaw.subarray(privRaw.length - 32).toString('hex');
108
+ return { publicKey: pubHex, privateKey: privHex };
109
+ }
95
110
  // ── Prompt helpers ─────────────────────────────────────────────────────────────
96
111
  async function ask(rl, question, defaultVal) {
97
112
  const suffix = defaultVal ? ` ${dim(`(${defaultVal})`)}` : '';
@@ -138,12 +153,10 @@ async function collectConfig(rl, reconfigure) {
138
153
  ]);
139
154
  console.log('');
140
155
  let supabaseUrl;
141
- let supabaseAnonKey;
142
156
  let supabaseServiceRoleKey;
143
157
  let databaseUrl;
144
158
  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);
159
+ supabaseUrl = await ask(rl, 'Supabase Project URL');
147
160
  supabaseServiceRoleKey = await askSecret(rl, 'Supabase Service Role Key (used during setup only, not stored)');
148
161
  console.log('');
149
162
  }
@@ -200,7 +213,6 @@ async function collectConfig(rl, reconfigure) {
200
213
  return {
201
214
  dbProvider,
202
215
  supabaseUrl,
203
- supabaseAnonKey,
204
216
  supabaseServiceRoleKey,
205
217
  databaseUrl,
206
218
  machineName,
@@ -312,49 +324,77 @@ async function seedChannels(config) {
312
324
  }
313
325
  return { name, ok: true, message: `${created}/${channels.length} channels ready` };
314
326
  }
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' };
327
+ async function generateAndRegisterKeypair(config, reconfigure) {
328
+ const name = 'Generate Ed25519 keypair';
329
+ const configDir = path.join(os.homedir(), '.airchat');
330
+ const privateKeyPath = path.join(configDir, 'machine.key');
331
+ const publicKeyPath = path.join(configDir, 'machine.pub');
332
+ // Check for existing keypair (skip generation unless reconfiguring)
333
+ if (!reconfigure && fs.existsSync(privateKeyPath) && fs.existsSync(publicKeyPath)) {
334
+ const existingPub = fs.readFileSync(publicKeyPath, 'utf-8').trim();
335
+ const existingPriv = fs.readFileSync(privateKeyPath, 'utf-8').trim();
336
+ if (existingPub.length === 64 && existingPriv.length === 64) {
337
+ config.keypair = { publicKey: existingPub, privateKey: existingPriv };
338
+ return { name, ok: true, message: 'Existing keypair found — reusing' };
339
+ }
319
340
  }
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}"` };
341
+ // Generate new Ed25519 keypair
342
+ const keypair = generateKeypair();
343
+ config.keypair = keypair;
344
+ // Ensure ~/.airchat/ exists with correct permissions
345
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
346
+ // Write private key (chmod 600)
347
+ fs.writeFileSync(privateKeyPath, keypair.privateKey + '\n', { mode: 0o600 });
348
+ // Write public key
349
+ fs.writeFileSync(publicKeyPath, keypair.publicKey + '\n', { mode: 0o644 });
350
+ // Ensure directory permissions are correct (mkdirSync mode may not apply if dir exists)
351
+ fs.chmodSync(configDir, 0o700);
352
+ fs.chmodSync(privateKeyPath, 0o600);
353
+ // Register public key with the server (requires service role access)
354
+ if (config.dbProvider === 'supabase' && config.supabaseUrl && config.supabaseServiceRoleKey) {
355
+ const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseServiceRoleKey, {
356
+ auth: { persistSession: false },
357
+ });
358
+ // Upsert: if machine_name already exists, update the public key (key rotation)
359
+ const { error } = await supabase
360
+ .from('machine_keys')
361
+ .upsert({
362
+ machine_name: config.machineName,
363
+ public_key: keypair.publicKey,
364
+ active: true,
365
+ }, { onConflict: 'machine_name' })
366
+ .select()
367
+ .single();
368
+ if (error) {
369
+ return { name, ok: false, message: `Keypair generated but registration failed: ${error.message}` };
370
+ }
371
+ return { name, ok: true, message: `Keypair generated and public key registered for "${config.machineName}"` };
372
+ }
373
+ // Non-Supabase: keypair generated but not registered
374
+ return {
375
+ name,
376
+ ok: true,
377
+ message: `Keypair generated. Public key must be registered manually with the server.`,
378
+ };
339
379
  }
340
380
  function writeAirchatConfig(config) {
341
381
  const name = 'Write ~/.airchat/config';
342
382
  try {
343
383
  const configDir = path.join(os.homedir(), '.airchat');
344
- fs.mkdirSync(configDir, { recursive: true });
384
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
385
+ // v2 config: only MACHINE_NAME and AIRCHAT_WEB_URL
345
386
  const lines = [
346
387
  `MACHINE_NAME=${config.machineName}`,
347
388
  ];
348
- if (config.apiKey)
349
- lines.push(`AIRCHAT_API_KEY=${config.apiKey}`);
350
389
  if (config.webUrl)
351
390
  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');
391
+ fs.writeFileSync(path.join(configDir, 'config'), lines.join('\n') + '\n', { mode: 0o600 });
392
+ // Ensure agents directory exists with correct permissions
393
+ const agentsDir = path.join(configDir, 'agents');
394
+ fs.mkdirSync(agentsDir, { recursive: true, mode: 0o700 });
395
+ fs.chmodSync(agentsDir, 0o700);
396
+ // Ensure parent dir permissions are correct
397
+ fs.chmodSync(configDir, 0o700);
358
398
  return { name, ok: true, message: configDir };
359
399
  }
360
400
  catch (e) {
@@ -372,18 +412,12 @@ function writeWebEnv(config) {
372
412
  if (config.supabaseUrl) {
373
413
  lines.push(`NEXT_PUBLIC_SUPABASE_URL=${config.supabaseUrl}`);
374
414
  }
375
- if (config.supabaseAnonKey) {
376
- lines.push(`NEXT_PUBLIC_SUPABASE_ANON_KEY=${config.supabaseAnonKey}`);
377
- }
378
415
  if (config.supabaseServiceRoleKey) {
379
416
  lines.push(`SUPABASE_SERVICE_ROLE_KEY=${config.supabaseServiceRoleKey}`);
380
417
  }
381
418
  if (config.databaseUrl) {
382
419
  lines.push(`DATABASE_URL=${config.databaseUrl}`);
383
420
  }
384
- if (config.apiKey) {
385
- lines.push(`AIRCHAT_API_KEY=${config.apiKey}`);
386
- }
387
421
  fs.writeFileSync(envPath, lines.join('\n') + '\n');
388
422
  return { name, ok: true, message: envPath };
389
423
  }
@@ -398,20 +432,11 @@ function registerMcpServer(config) {
398
432
  if (!fs.existsSync(serverPath)) {
399
433
  return { name, ok: false, message: `MCP server not found at ${serverPath}` };
400
434
  }
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}"`;
435
+ // v2: No env vars passed — the MCP server reads from ~/.airchat/config and ~/.airchat/machine.key directly
436
+ const cmd = `claude mcp add airchat -s user -- "${config.nodePath}" "${tsxPath}" "${serverPath}"`;
412
437
  try {
413
438
  (0, node_child_process_1.execSync)(cmd, { stdio: 'pipe' });
414
- return { name, ok: true, message: 'Registered at user level' };
439
+ return { name, ok: true, message: 'Registered at user level (no env vars — reads from ~/.airchat/)' };
415
440
  }
416
441
  catch (e) {
417
442
  // claude CLI might not be installed
@@ -506,7 +531,7 @@ function copySlashCommands(config) {
506
531
  async function main() {
507
532
  const reconfigure = process.argv.includes('--reconfigure');
508
533
  console.log('');
509
- console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim('v0.1.0')}`);
534
+ console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim('v0.2.0')}`);
510
535
  console.log(` ${dim('Your AI agents can talk to each other')}`);
511
536
  const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
512
537
  let config;
@@ -559,18 +584,16 @@ async function main() {
559
584
  else
560
585
  fail(seedResult.message);
561
586
  }
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
587
+ // 4. Generate Ed25519 keypair and register public key
588
+ process.stdout.write(` Generating keypair...`);
589
+ const keypairResult = await generateAndRegisterKeypair(config, reconfigure);
590
+ results.push(keypairResult);
591
+ process.stdout.write(`\r`);
592
+ if (keypairResult.ok)
593
+ ok(keypairResult.message);
594
+ else
595
+ fail(keypairResult.message);
596
+ // 5. Write config (v2: only MACHINE_NAME and AIRCHAT_WEB_URL)
574
597
  const configResult = writeAirchatConfig(config);
575
598
  results.push(configResult);
576
599
  if (configResult.ok)
@@ -585,7 +608,7 @@ async function main() {
585
608
  else if (!config.deployDashboard) { } // silent skip
586
609
  else
587
610
  fail(envResult.message);
588
- // 7. Register MCP server
611
+ // 7. Register MCP server (no env vars — reads from ~/.airchat/ directly)
589
612
  const mcpResult = registerMcpServer(config);
590
613
  results.push(mcpResult);
591
614
  if (mcpResult.ok)
@@ -625,18 +648,29 @@ async function main() {
625
648
  console.log(` ${RED}•${RESET} ${f.name}: ${f.message}`);
626
649
  }
627
650
  }
628
- if (config.apiKey) {
651
+ // Show keypair info
652
+ if (config.keypair) {
629
653
  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}`);
654
+ console.log(` ${BOLD}Machine keypair:${RESET}`);
655
+ console.log(` Private key: ~/.airchat/machine.key ${dim('(chmod 600)')}`);
656
+ console.log(` Public key: ~/.airchat/machine.pub`);
657
+ console.log(` Public key (hex): ${config.keypair.publicKey}`);
658
+ console.log(` ${dim('The private key never leaves this machine.')}`);
659
+ console.log(` ${dim('Agents will auto-register on startup using the keypair.')}`);
632
660
  }
633
661
  console.log('');
662
+ console.log(` ${dim('Config:')}`);
663
+ console.log(` ~/.airchat/config ${dim('MACHINE_NAME + AIRCHAT_WEB_URL')}`);
664
+ console.log(` ~/.airchat/machine.key ${dim('Ed25519 private key')}`);
665
+ console.log(` ~/.airchat/machine.pub ${dim('Ed25519 public key')}`);
666
+ console.log(` ~/.airchat/agents/ ${dim('Cached derived keys (auto-created)')}`);
667
+ console.log('');
634
668
  console.log(` ${dim('Next steps:')}`);
635
669
  console.log(` 1. Restart Claude Code`);
636
670
  if (config.deployDashboard) {
637
671
  console.log(` 2. Start the dashboard: cd ${config.airchatDir} && docker compose up -d --build`);
638
672
  }
639
- console.log(` ${dim('Run with --reconfigure to update settings later.')}`);
673
+ console.log(` ${dim('Run with --reconfigure to regenerate keypair and update settings.')}`);
640
674
  console.log('');
641
675
  }
642
676
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airchat",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Interactive setup CLI for AirChat — one command to get your AI agents talking to each other",
5
5
  "license": "MIT",
6
6
  "author": "Salmonrun.ai",