airchat 0.1.0 → 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.
- package/dist/index.js +106 -72
- package/package.json +2 -2
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'
|
|
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
|
|
316
|
-
const name = 'Generate
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
//
|
|
402
|
-
const
|
|
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.
|
|
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
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
651
|
+
// Show keypair info
|
|
652
|
+
if (config.keypair) {
|
|
629
653
|
console.log('');
|
|
630
|
-
console.log(` ${BOLD}
|
|
631
|
-
console.log(`
|
|
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
|
|
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.
|
|
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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"directory": "packages/create-airchat"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"
|
|
13
|
+
"airchat": "dist/index.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|