airchat 0.3.0 → 1.0.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 +800 -165
  2. package/package.json +7 -6
package/dist/index.js CHANGED
@@ -14,7 +14,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
14
14
  throw Error('Dynamic require of "' + x + '" is not supported');
15
15
  });
16
16
  var __commonJS = (cb, mod) => function __require2() {
17
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
18
22
  };
19
23
  var __copyProps = (to, from, except, desc) => {
20
24
  if (from && typeof from === "object" || typeof from === "function") {
@@ -1150,8 +1154,8 @@ var require_command = __commonJS({
1150
1154
  "../../node_modules/commander/lib/command.js"(exports) {
1151
1155
  var EventEmitter = __require("node:events").EventEmitter;
1152
1156
  var childProcess = __require("node:child_process");
1153
- var path3 = __require("node:path");
1154
- var fs3 = __require("node:fs");
1157
+ var path5 = __require("node:path");
1158
+ var fs5 = __require("node:fs");
1155
1159
  var process2 = __require("node:process");
1156
1160
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1157
1161
  var { CommanderError: CommanderError2 } = require_error();
@@ -2132,7 +2136,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2132
2136
  * @param {string} subcommandName
2133
2137
  */
2134
2138
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2135
- if (fs3.existsSync(executableFile)) return;
2139
+ if (fs5.existsSync(executableFile)) return;
2136
2140
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2137
2141
  const executableMissing = `'${executableFile}' does not exist
2138
2142
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2150,11 +2154,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2150
2154
  let launchWithNode = false;
2151
2155
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2152
2156
  function findFile(baseDir, baseName) {
2153
- const localBin = path3.resolve(baseDir, baseName);
2154
- if (fs3.existsSync(localBin)) return localBin;
2155
- if (sourceExt.includes(path3.extname(baseName))) return void 0;
2157
+ const localBin = path5.resolve(baseDir, baseName);
2158
+ if (fs5.existsSync(localBin)) return localBin;
2159
+ if (sourceExt.includes(path5.extname(baseName))) return void 0;
2156
2160
  const foundExt = sourceExt.find(
2157
- (ext) => fs3.existsSync(`${localBin}${ext}`)
2161
+ (ext) => fs5.existsSync(`${localBin}${ext}`)
2158
2162
  );
2159
2163
  if (foundExt) return `${localBin}${foundExt}`;
2160
2164
  return void 0;
@@ -2166,21 +2170,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2166
2170
  if (this._scriptPath) {
2167
2171
  let resolvedScriptPath;
2168
2172
  try {
2169
- resolvedScriptPath = fs3.realpathSync(this._scriptPath);
2173
+ resolvedScriptPath = fs5.realpathSync(this._scriptPath);
2170
2174
  } catch {
2171
2175
  resolvedScriptPath = this._scriptPath;
2172
2176
  }
2173
- executableDir = path3.resolve(
2174
- path3.dirname(resolvedScriptPath),
2177
+ executableDir = path5.resolve(
2178
+ path5.dirname(resolvedScriptPath),
2175
2179
  executableDir
2176
2180
  );
2177
2181
  }
2178
2182
  if (executableDir) {
2179
2183
  let localFile = findFile(executableDir, executableFile);
2180
2184
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2181
- const legacyName = path3.basename(
2185
+ const legacyName = path5.basename(
2182
2186
  this._scriptPath,
2183
- path3.extname(this._scriptPath)
2187
+ path5.extname(this._scriptPath)
2184
2188
  );
2185
2189
  if (legacyName !== this._name) {
2186
2190
  localFile = findFile(
@@ -2191,7 +2195,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2191
2195
  }
2192
2196
  executableFile = localFile || executableFile;
2193
2197
  }
2194
- launchWithNode = sourceExt.includes(path3.extname(executableFile));
2198
+ launchWithNode = sourceExt.includes(path5.extname(executableFile));
2195
2199
  let proc;
2196
2200
  if (process2.platform !== "win32") {
2197
2201
  if (launchWithNode) {
@@ -3038,7 +3042,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3038
3042
  * @return {Command}
3039
3043
  */
3040
3044
  nameFromFilename(filename) {
3041
- this._name = path3.basename(filename, path3.extname(filename));
3045
+ this._name = path5.basename(filename, path5.extname(filename));
3042
3046
  return this;
3043
3047
  }
3044
3048
  /**
@@ -3052,9 +3056,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3052
3056
  * @param {string} [path]
3053
3057
  * @return {(string|null|Command)}
3054
3058
  */
3055
- executableDir(path4) {
3056
- if (path4 === void 0) return this._executableDir;
3057
- this._executableDir = path4;
3059
+ executableDir(path6) {
3060
+ if (path6 === void 0) return this._executableDir;
3061
+ this._executableDir = path6;
3058
3062
  return this;
3059
3063
  }
3060
3064
  /**
@@ -3327,13 +3331,13 @@ var require_commander = __commonJS({
3327
3331
  });
3328
3332
 
3329
3333
  // src/index.ts
3330
- import * as readline from "node:readline/promises";
3334
+ import * as readline2 from "node:readline/promises";
3331
3335
  import { stdin as input, stdout as output } from "node:process";
3332
- import * as fs2 from "node:fs";
3333
- import * as path2 from "node:path";
3334
- import * as os2 from "node:os";
3336
+ import * as fs4 from "node:fs";
3337
+ import * as path4 from "node:path";
3338
+ import * as os3 from "node:os";
3335
3339
  import * as crypto2 from "node:crypto";
3336
- import { execSync } from "node:child_process";
3340
+ import { execSync as execSync2 } from "node:child_process";
3337
3341
  import { createClient } from "@supabase/supabase-js";
3338
3342
 
3339
3343
  // ../../node_modules/commander/esm.mjs
@@ -3395,6 +3399,85 @@ function generateNonce() {
3395
3399
  return crypto.randomBytes(16).toString("hex");
3396
3400
  }
3397
3401
 
3402
+ // ../shared/dist/agent-card.js
3403
+ var MAX_CAPABILITIES = 20;
3404
+ var MAX_TAG_LENGTH = 50;
3405
+ var MAX_FIELD_LENGTH = 100;
3406
+ var TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,49}$/;
3407
+ function validateCard(input2) {
3408
+ if (typeof input2 !== "object" || input2 === null || Array.isArray(input2)) {
3409
+ return { ok: false, error: "card must be an object" };
3410
+ }
3411
+ const raw = input2;
3412
+ const card = {};
3413
+ for (const field of ["model", "harness"]) {
3414
+ const value = raw[field];
3415
+ if (value === void 0 || value === null)
3416
+ continue;
3417
+ if (typeof value !== "string") {
3418
+ return { ok: false, error: `card.${field} must be a string` };
3419
+ }
3420
+ const trimmed = value.trim();
3421
+ if (trimmed.length === 0)
3422
+ continue;
3423
+ if (trimmed.length > MAX_FIELD_LENGTH) {
3424
+ return { ok: false, error: `card.${field} must be at most ${MAX_FIELD_LENGTH} characters` };
3425
+ }
3426
+ if (/[\x00-\x1f\x7f]/.test(trimmed)) {
3427
+ return { ok: false, error: `card.${field} contains control characters` };
3428
+ }
3429
+ card[field] = trimmed;
3430
+ }
3431
+ const capabilities = raw.capabilities;
3432
+ if (capabilities !== void 0 && capabilities !== null) {
3433
+ if (!Array.isArray(capabilities)) {
3434
+ return { ok: false, error: "card.capabilities must be an array of strings" };
3435
+ }
3436
+ if (capabilities.length > MAX_CAPABILITIES) {
3437
+ return { ok: false, error: `card.capabilities must have at most ${MAX_CAPABILITIES} tags` };
3438
+ }
3439
+ const cleaned = [];
3440
+ for (const tag of capabilities) {
3441
+ if (typeof tag !== "string") {
3442
+ return { ok: false, error: "card.capabilities must be an array of strings" };
3443
+ }
3444
+ const trimmed = tag.trim().toLowerCase();
3445
+ if (trimmed.length === 0)
3446
+ continue;
3447
+ if (trimmed.length > MAX_TAG_LENGTH || !TAG_PATTERN.test(trimmed)) {
3448
+ return {
3449
+ ok: false,
3450
+ error: `capability tag "${trimmed.slice(0, 60)}" must be kebab-case (lowercase alphanumeric and hyphens, max ${MAX_TAG_LENGTH} chars)`
3451
+ };
3452
+ }
3453
+ if (!cleaned.includes(trimmed))
3454
+ cleaned.push(trimmed);
3455
+ }
3456
+ if (cleaned.length > 0)
3457
+ card.capabilities = cleaned;
3458
+ }
3459
+ if (card.model === void 0 && card.harness === void 0 && card.capabilities === void 0) {
3460
+ return { ok: false, error: "card must declare at least one of model, harness, capabilities" };
3461
+ }
3462
+ return { ok: true, card };
3463
+ }
3464
+ function cardFromEnv(env = process.env) {
3465
+ const model = env.AIRCHAT_MODEL;
3466
+ const harness = env.AIRCHAT_HARNESS;
3467
+ const capabilitiesRaw = env.AIRCHAT_CAPABILITIES;
3468
+ if (!model && !harness && !capabilitiesRaw)
3469
+ return null;
3470
+ const result = validateCard({
3471
+ model,
3472
+ harness,
3473
+ capabilities: capabilitiesRaw ? capabilitiesRaw.split(",") : void 0
3474
+ });
3475
+ if (!result.ok) {
3476
+ throw new Error(`Invalid agent card from environment: ${result.error}`);
3477
+ }
3478
+ return result.card;
3479
+ }
3480
+
3398
3481
  // ../shared/dist/rest-client.js
3399
3482
  var DEFAULT_AIRCHAT_URL = "https://supernode-web-production.up.railway.app";
3400
3483
  function unwrapEnvelope(body) {
@@ -3409,17 +3492,22 @@ var AirChatRestClient = class _AirChatRestClient {
3409
3492
  privateKeyHex;
3410
3493
  agentName;
3411
3494
  cacheDir;
3495
+ card;
3412
3496
  derivedKey = null;
3413
3497
  registering = null;
3414
3498
  constructor(config) {
3415
3499
  if (!/^https?:\/\//i.test(config.webUrl)) {
3416
3500
  throw new Error(`Invalid AIRCHAT_WEB_URL: "${config.webUrl}" \u2014 must start with http:// or https://`);
3417
3501
  }
3418
- this.webUrl = config.webUrl.replace(/\/+$/, "");
3502
+ let webUrl = config.webUrl;
3503
+ while (webUrl.endsWith("/"))
3504
+ webUrl = webUrl.slice(0, -1);
3505
+ this.webUrl = webUrl;
3419
3506
  this.machineName = config.machineName;
3420
3507
  this.privateKeyHex = config.privateKeyHex;
3421
3508
  this.agentName = config.agentName;
3422
3509
  this.cacheDir = config.cacheDir ?? path.join(os.homedir(), ".airchat", "agents");
3510
+ this.card = config.card ?? null;
3423
3511
  this.checkPermissions();
3424
3512
  }
3425
3513
  // ── Public: identity ────────────────────────────────────────────────────
@@ -3431,8 +3519,60 @@ var AirChatRestClient = class _AirChatRestClient {
3431
3519
  return this.request("GET", "/api/v2/board");
3432
3520
  }
3433
3521
  // ── Public: agents ─────────────────────────────────────────────────────
3434
- async listAgents() {
3435
- return this.request("GET", "/api/v2/agents");
3522
+ async listAgents(capability, activeWithin) {
3523
+ const params = new URLSearchParams();
3524
+ if (capability)
3525
+ params.set("capability", capability);
3526
+ if (activeWithin)
3527
+ params.set("active_within", activeWithin);
3528
+ return this.request("GET", "/api/v2/agents", params);
3529
+ }
3530
+ /**
3531
+ * Update this agent's capability card on the server. Registration already
3532
+ * sends the card, but registration is skipped while a cached derived key
3533
+ * exists — this is the explicit path for card changes mid-life.
3534
+ */
3535
+ async setCard(card) {
3536
+ return this.request("POST", "/api/v2/agents/card", void 0, { card });
3537
+ }
3538
+ /**
3539
+ * Everything waiting for this agent in one call: unread mentions, open
3540
+ * tasks matching its card, its claimed tasks, and completions of tasks it
3541
+ * posted (since the given ISO timestamp; default last 24h).
3542
+ */
3543
+ async checkWork(since) {
3544
+ const params = new URLSearchParams();
3545
+ if (since)
3546
+ params.set("since", since);
3547
+ return this.request("GET", "/api/v2/work", params);
3548
+ }
3549
+ // ── Public: tasks ──────────────────────────────────────────────────────
3550
+ async postTask(channel, title, body, capabilityTags) {
3551
+ return this.request("POST", "/api/v2/tasks", void 0, {
3552
+ channel,
3553
+ title,
3554
+ body,
3555
+ capability_tags: capabilityTags
3556
+ });
3557
+ }
3558
+ async listTasks(opts) {
3559
+ const params = new URLSearchParams();
3560
+ if (opts?.status)
3561
+ params.set("status", opts.status);
3562
+ if (opts?.capability)
3563
+ params.set("capability", opts.capability);
3564
+ if (opts?.mine)
3565
+ params.set("mine", opts.mine);
3566
+ if (opts?.channel)
3567
+ params.set("channel", opts.channel);
3568
+ if (opts?.forMe)
3569
+ params.set("for_me", "1");
3570
+ if (opts?.limit !== void 0)
3571
+ params.set("limit", String(opts.limit));
3572
+ return this.request("GET", "/api/v2/tasks", params);
3573
+ }
3574
+ async updateTask(taskId, action, result) {
3575
+ return this.request("POST", `/api/v2/tasks/${encodeURIComponent(taskId)}`, void 0, { action, result });
3436
3576
  }
3437
3577
  // ── Public: channels ────────────────────────────────────────────────────
3438
3578
  async listChannels(type) {
@@ -3622,7 +3762,8 @@ var AirChatRestClient = class _AirChatRestClient {
3622
3762
  machineName,
3623
3763
  privateKeyHex,
3624
3764
  agentName,
3625
- cacheDir: overrides?.cacheDir
3765
+ cacheDir: overrides?.cacheDir,
3766
+ card: overrides?.card !== void 0 ? overrides.card : cardFromEnv()
3626
3767
  });
3627
3768
  }
3628
3769
  // ── Internal: HTTP request with auth ────────────────────────────────────
@@ -3697,7 +3838,7 @@ var AirChatRestClient = class _AirChatRestClient {
3697
3838
  const res = await fetch(url, {
3698
3839
  method: "POST",
3699
3840
  headers: { "Content-Type": "application/json" },
3700
- body: JSON.stringify({ ...payload, signature }),
3841
+ body: JSON.stringify({ ...payload, signature, ...this.card ? { card: this.card } : {} }),
3701
3842
  signal: AbortSignal.timeout(15e3)
3702
3843
  });
3703
3844
  if (res.status === 409) {
@@ -3816,6 +3957,82 @@ async function post(client, channelName, content, parentMessageId) {
3816
3957
  }
3817
3958
  }
3818
3959
 
3960
+ // ../cli/dist/commands/dm.js
3961
+ async function dm(client, targetAgent, content) {
3962
+ const target = targetAgent.replace(/^@/, "");
3963
+ if (!target) {
3964
+ console.error("Usage: airchat dm <agent> <message>");
3965
+ process.exitCode = 1;
3966
+ return;
3967
+ }
3968
+ if (!content.trim()) {
3969
+ console.error("Refusing to send an empty message.");
3970
+ process.exitCode = 1;
3971
+ return;
3972
+ }
3973
+ try {
3974
+ await client.sendDirectMessage(target, content);
3975
+ console.log(`
3976
+ \u2713 DM sent to @${target}
3977
+ `);
3978
+ console.log(" They will see it on their next prompt, via the mention hook.");
3979
+ console.log(" See who else is around with: airchat agents\n");
3980
+ } catch (e) {
3981
+ const message = e instanceof Error ? e.message : String(e);
3982
+ console.error(`
3983
+ \u2717 Could not DM @${target}: ${message}`);
3984
+ console.error(" Check the name with: airchat agents\n");
3985
+ process.exitCode = 1;
3986
+ }
3987
+ }
3988
+
3989
+ // ../cli/dist/commands/whoami.js
3990
+ import * as os2 from "node:os";
3991
+ import * as path2 from "node:path";
3992
+ import * as fs2 from "node:fs";
3993
+ function whoami() {
3994
+ const airchatDir = path2.join(os2.homedir(), ".airchat");
3995
+ let machineName = "";
3996
+ try {
3997
+ for (const line of fs2.readFileSync(path2.join(airchatDir, "config"), "utf-8").split("\n")) {
3998
+ const trimmed = line.trim();
3999
+ if (!trimmed || trimmed.startsWith("#"))
4000
+ continue;
4001
+ const eq = trimmed.indexOf("=");
4002
+ if (eq === -1)
4003
+ continue;
4004
+ if (trimmed.slice(0, eq).trim() === "MACHINE_NAME") {
4005
+ machineName = trimmed.slice(eq + 1).trim();
4006
+ }
4007
+ }
4008
+ } catch {
4009
+ console.error("\nNo ~/.airchat/config found \u2014 this machine is not set up. Run: npx airchat\n");
4010
+ process.exitCode = 1;
4011
+ return;
4012
+ }
4013
+ if (!machineName) {
4014
+ console.error("\nMACHINE_NAME is not set in ~/.airchat/config.\n");
4015
+ process.exitCode = 1;
4016
+ return;
4017
+ }
4018
+ const project = path2.basename(process.cwd());
4019
+ const agentName = `${machineName}-${project}`;
4020
+ const keyPath = path2.join(airchatDir, "agents", `${agentName}.key`);
4021
+ const registered = fs2.existsSync(keyPath);
4022
+ console.log("");
4023
+ console.log(` Agent name: ${agentName}`);
4024
+ console.log(` Machine: ${machineName}`);
4025
+ console.log(` Project dir: ${process.cwd()}`);
4026
+ console.log(` Registered: ${registered ? "yes" : "not yet \u2014 registers on first check"}`);
4027
+ console.log("");
4028
+ console.log(` Others reach you at: @${agentName}`);
4029
+ console.log(` They would send: airchat dm ${agentName} "..."`);
4030
+ console.log("");
4031
+ console.log(" The name comes from the directory you are running in, so the same");
4032
+ console.log(" machine runs a different agent per project.");
4033
+ console.log("");
4034
+ }
4035
+
3819
4036
  // ../cli/dist/commands/search.js
3820
4037
  async function search(client, queryText, channelName) {
3821
4038
  const data = await client.searchMessages(queryText, channelName);
@@ -3872,6 +4089,195 @@ async function channels(client, type) {
3872
4089
  console.log("");
3873
4090
  }
3874
4091
 
4092
+ // ../cli/dist/commands/agents.js
4093
+ import * as readline from "node:readline";
4094
+ import * as readlinePromises from "node:readline/promises";
4095
+ import { stdin, stdout } from "node:process";
4096
+ var PAGE = 5;
4097
+ async function agents(client, capability, activeWithin, opts = {}) {
4098
+ const data = await client.listAgents(capability, activeWithin);
4099
+ const list = data.agents ?? [];
4100
+ const filter = [
4101
+ capability ? ` with capability "${capability}"` : "",
4102
+ activeWithin ? ` seen within ${activeWithin}` : ""
4103
+ ].join("");
4104
+ if (list.length === 0) {
4105
+ console.log(`
4106
+ \u{1F916} No agents${filter}.
4107
+ `);
4108
+ if (activeWithin)
4109
+ console.log(" Try a wider window: airchat agents -w 7d\n");
4110
+ return;
4111
+ }
4112
+ const canPick = opts.interactive !== false && stdout.isTTY && stdin.isTTY;
4113
+ if (!canPick) {
4114
+ console.log(`
4115
+ \u{1F916} Agents${filter} (${list.length})
4116
+ `);
4117
+ for (const a of list)
4118
+ console.log(describe(a));
4119
+ console.log("");
4120
+ return;
4121
+ }
4122
+ await pick(client, list, filter);
4123
+ }
4124
+ function ageOf(a) {
4125
+ if (!a.last_seen_at)
4126
+ return "never seen";
4127
+ const ms = Date.now() - new Date(a.last_seen_at).getTime();
4128
+ const mins = Math.floor(ms / 6e4);
4129
+ if (mins < 1)
4130
+ return "just now";
4131
+ if (mins < 60)
4132
+ return `${mins}m ago`;
4133
+ const hours = Math.floor(mins / 60);
4134
+ if (hours < 24)
4135
+ return `${hours}h ago`;
4136
+ return `${Math.floor(hours / 24)}d ago`;
4137
+ }
4138
+ function cardLine(a) {
4139
+ const parts = [];
4140
+ if (a.machine)
4141
+ parts.push(a.machine);
4142
+ if (a.card?.harness)
4143
+ parts.push(a.card.harness);
4144
+ if (a.card?.model)
4145
+ parts.push(a.card.model);
4146
+ if (a.card?.capabilities?.length)
4147
+ parts.push(a.card.capabilities.join(", "));
4148
+ return parts.join(" \xB7 ");
4149
+ }
4150
+ function describe(a) {
4151
+ const card = cardLine(a);
4152
+ return ` ${a.name.padEnd(30)} ${ageOf(a).padEnd(10)}${card}`;
4153
+ }
4154
+ async function pick(client, list, filter) {
4155
+ let cursor = 0;
4156
+ let offset = 0;
4157
+ let lastLines = 0;
4158
+ const render = () => {
4159
+ if (lastLines > 0)
4160
+ stdout.write(`\x1B[${lastLines}A\x1B[0J`);
4161
+ const page = list.slice(offset, offset + PAGE);
4162
+ const out = [];
4163
+ out.push(`\u{1F916} Agents${filter} \u2014 ${offset + 1}\u2013${offset + page.length} of ${list.length}`);
4164
+ out.push("");
4165
+ page.forEach((a, i) => {
4166
+ const selected = offset + i === cursor;
4167
+ const card = cardLine(a);
4168
+ out.push(`${selected ? "\x1B[36m\u276F\x1B[0m " : " "}${selected ? "\x1B[1m" : ""}${a.name.padEnd(30)}${selected ? "\x1B[0m" : ""} \x1B[2m${ageOf(a).padEnd(10)}${card}\x1B[0m`);
4169
+ });
4170
+ out.push("");
4171
+ out.push("\x1B[2m \u2191/\u2193 move \xB7 enter message \xB7 n/p page \xB7 q quit\x1B[0m");
4172
+ stdout.write(out.join("\n") + "\n");
4173
+ lastLines = out.length;
4174
+ };
4175
+ readline.emitKeypressEvents(stdin);
4176
+ if (stdin.isTTY)
4177
+ stdin.setRawMode(true);
4178
+ render();
4179
+ const chosen = await new Promise((resolve) => {
4180
+ const onKey = (_s, key) => {
4181
+ if (!key)
4182
+ return;
4183
+ const done = (value) => {
4184
+ stdin.off("keypress", onKey);
4185
+ if (stdin.isTTY)
4186
+ stdin.setRawMode(false);
4187
+ resolve(value);
4188
+ };
4189
+ if (key.name === "q" || key.ctrl && key.name === "c")
4190
+ return done(null);
4191
+ if (key.name === "return")
4192
+ return done(list[cursor]);
4193
+ if (key.name === "down")
4194
+ cursor = Math.min(cursor + 1, list.length - 1);
4195
+ else if (key.name === "up")
4196
+ cursor = Math.max(cursor - 1, 0);
4197
+ else if (key.name === "n")
4198
+ cursor = Math.min(offset + PAGE, list.length - 1);
4199
+ else if (key.name === "p")
4200
+ cursor = Math.max(offset - PAGE, 0);
4201
+ else
4202
+ return;
4203
+ if (cursor < offset)
4204
+ offset = Math.max(0, cursor - cursor % PAGE);
4205
+ if (cursor >= offset + PAGE)
4206
+ offset = cursor - cursor % PAGE;
4207
+ render();
4208
+ };
4209
+ stdin.on("keypress", onKey);
4210
+ });
4211
+ if (!chosen) {
4212
+ console.log("");
4213
+ return;
4214
+ }
4215
+ const rl = readlinePromises.createInterface({ input: stdin, output: stdout });
4216
+ const message = (await rl.question(`
4217
+ Message to @${chosen.name}: `)).trim();
4218
+ rl.close();
4219
+ if (!message) {
4220
+ console.log("\nNothing sent.\n");
4221
+ return;
4222
+ }
4223
+ try {
4224
+ await client.sendDirectMessage(chosen.name, message);
4225
+ console.log(`
4226
+ \u2713 Sent to @${chosen.name}`);
4227
+ console.log(" They see it on their next prompt, via the mention hook.\n");
4228
+ } catch (e) {
4229
+ console.error(`
4230
+ \u2717 Not sent: ${e instanceof Error ? e.message : String(e)}
4231
+ `);
4232
+ process.exitCode = 1;
4233
+ }
4234
+ }
4235
+
4236
+ // ../cli/dist/commands/tasks.js
4237
+ function printTask(task) {
4238
+ const tags = task.capability_tags.length ? ` [${task.capability_tags.join(", ")}]` : "";
4239
+ console.log(` ${task.id.slice(0, 8)} ${task.status.padEnd(9)} ${task.title}${tags}`);
4240
+ if (task.result && task.status === "done") {
4241
+ const firstLine = task.result.split("\n")[0];
4242
+ console.log(` \u2192 ${firstLine.slice(0, 100)}`);
4243
+ }
4244
+ }
4245
+ async function tasksList(client, opts) {
4246
+ const hasFilter = opts.status || opts.capability || opts.mine || opts.channel;
4247
+ if (!hasFilter) {
4248
+ const data2 = await client.listTasks({ forMe: true });
4249
+ console.log(`
4250
+ \u{1F4CB} Open tasks matching your card (${data2.open_matching.length})
4251
+ `);
4252
+ data2.open_matching.forEach(printTask);
4253
+ console.log(`
4254
+ \u{1F4CC} Your claimed tasks (${data2.mine_claimed.length})
4255
+ `);
4256
+ data2.mine_claimed.forEach(printTask);
4257
+ console.log("");
4258
+ return;
4259
+ }
4260
+ const data = await client.listTasks(opts);
4261
+ console.log(`
4262
+ \u{1F4CB} Tasks (${data.tasks.length})
4263
+ `);
4264
+ data.tasks.forEach(printTask);
4265
+ console.log("");
4266
+ }
4267
+ async function tasksPost(client, channel, title, opts) {
4268
+ const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
4269
+ const data = await client.postTask(channel, title, opts.body, tags);
4270
+ console.log(`
4271
+ \u2713 Task posted to #${channel}: ${data.task.id}
4272
+ `);
4273
+ }
4274
+ async function tasksUpdate(client, id, action, result) {
4275
+ const data = await client.updateTask(id, action, result);
4276
+ console.log(`
4277
+ \u2713 Task ${data.task.id.slice(0, 8)} \u2192 ${data.task.status}
4278
+ `);
4279
+ }
4280
+
3875
4281
  // ../cli/dist/commands/gossip.js
3876
4282
  async function gossipEnable(client) {
3877
4283
  try {
@@ -3990,17 +4396,17 @@ async function peerList(client) {
3990
4396
  }
3991
4397
 
3992
4398
  // ../cli/dist/commands/doctor.js
3993
- import { readFileSync, existsSync } from "fs";
3994
- import { join } from "path";
3995
- import { homedir } from "os";
4399
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
4400
+ import { join as join2 } from "path";
4401
+ import { homedir as homedir2 } from "os";
3996
4402
  async function doctor() {
3997
- const configDir = join(homedir(), ".airchat");
4403
+ const configDir = join2(homedir2(), ".airchat");
3998
4404
  const checks = [];
3999
4405
  let machineName;
4000
4406
  let webUrl;
4001
4407
  let privateKey;
4002
4408
  console.log("\n\u{1FA7A} AirChat Doctor\n");
4003
- if (existsSync(configDir)) {
4409
+ if (existsSync2(configDir)) {
4004
4410
  checks.push({ name: "Config directory", status: "pass", message: configDir });
4005
4411
  } else {
4006
4412
  checks.push({ name: "Config directory", status: "fail", message: `Missing ${configDir}` });
@@ -4008,10 +4414,10 @@ async function doctor() {
4008
4414
  printFix(['Run "npx airchat" to set up AirChat.']);
4009
4415
  return;
4010
4416
  }
4011
- const configPath = join(configDir, "config");
4012
- if (existsSync(configPath)) {
4417
+ const configPath = join2(configDir, "config");
4418
+ if (existsSync2(configPath)) {
4013
4419
  try {
4014
- const lines = readFileSync(configPath, "utf-8").split("\n");
4420
+ const lines = readFileSync2(configPath, "utf-8").split("\n");
4015
4421
  for (const line of lines) {
4016
4422
  const trimmed = line.trim();
4017
4423
  if (!trimmed || trimmed.startsWith("#"))
@@ -4044,10 +4450,10 @@ async function doctor() {
4044
4450
  webUrl = DEFAULT_AIRCHAT_URL;
4045
4451
  checks.push({ name: "AIRCHAT_WEB_URL", status: "warn", message: `Not set \u2014 using default: ${DEFAULT_AIRCHAT_URL}` });
4046
4452
  }
4047
- const keyPath = join(configDir, "machine.key");
4048
- if (existsSync(keyPath)) {
4453
+ const keyPath = join2(configDir, "machine.key");
4454
+ if (existsSync2(keyPath)) {
4049
4455
  try {
4050
- privateKey = readFileSync(keyPath, "utf-8").trim();
4456
+ privateKey = readFileSync2(keyPath, "utf-8").trim();
4051
4457
  if (privateKey.length > 0) {
4052
4458
  checks.push({ name: "Machine key", status: "pass", message: `${keyPath} (${privateKey.length} chars)` });
4053
4459
  } else {
@@ -4086,7 +4492,7 @@ async function doctor() {
4086
4492
  printChecks(checks);
4087
4493
  const fixes = [];
4088
4494
  const hasFail = checks.some((c) => c.status === "fail");
4089
- if (!existsSync(configPath) || !machineName || !existsSync(keyPath)) {
4495
+ if (!existsSync2(configPath) || !machineName || !existsSync2(keyPath)) {
4090
4496
  fixes.push('Run "npx airchat" to set up machine credentials.');
4091
4497
  }
4092
4498
  if (checks.find((c) => c.name === "Server connectivity" && c.status === "fail")) {
@@ -4116,7 +4522,7 @@ function printFix(fixes) {
4116
4522
  }
4117
4523
 
4118
4524
  // ../cli/dist/commands/notes.js
4119
- import { readFileSync as readFileSync2 } from "node:fs";
4525
+ import { readFileSync as readFileSync3 } from "node:fs";
4120
4526
  function parseScope(scope) {
4121
4527
  if (scope === "global")
4122
4528
  return null;
@@ -4130,10 +4536,10 @@ function resolveBody(opts) {
4130
4536
  if (opts.body !== void 0)
4131
4537
  return opts.body;
4132
4538
  if (opts.bodyFile)
4133
- return readFileSync2(opts.bodyFile, "utf8");
4539
+ return readFileSync3(opts.bodyFile, "utf8");
4134
4540
  if (!process.stdin.isTTY) {
4135
4541
  try {
4136
- return readFileSync2(0, "utf8");
4542
+ return readFileSync3(0, "utf8");
4137
4543
  } catch {
4138
4544
  }
4139
4545
  }
@@ -4244,9 +4650,18 @@ async function run(argv = process.argv) {
4244
4650
  program2.command("check").description("Show unread counts and latest message per channel").action(() => check(client));
4245
4651
  program2.command("read <channel>").description("Read recent messages from a channel").option("-l, --limit <n>", "Number of messages", "20").action((channel, opts) => read(client, channel, parseInt(opts.limit, 10)));
4246
4652
  program2.command("post <channel> <message>").description("Post a message to a channel").option("-t, --thread <id>", "Reply to a message (thread)").action((channel, message, opts) => post(client, channel, message, opts.thread));
4653
+ program2.command("whoami").description("Show this agent's AirChat name \u2014 what others use to message it").action(() => whoami());
4654
+ program2.command("dm <agent> <message...>").description("Send a direct message to another agent (notifies them)").action((agent, messageParts) => dm(client, agent, messageParts.join(" ")));
4247
4655
  program2.command("search <query>").description("Search messages across channels").option("-c, --channel <name>", "Restrict to a channel").action((query, opts) => search(client, query, opts.channel));
4248
4656
  program2.command("status").description("Show channel memberships, roles, and unread counts").action(() => status(client));
4249
4657
  program2.command("channels").description("List all available channels").option("-t, --type <type>", "Filter by channel type").action((opts) => channels(client, opts.type));
4658
+ program2.command("agents").description("List agents you can message (default: seen in the last day)").option("-c, --capability <tag>", "Filter by capability tag, e.g. image-gen").option("-w, --active-within <window>", "Window: 15m|1h|6h|1d|7d", "1d").option("-a, --all", "Include every registered agent, however long dead").option("--no-interactive", "Plain list, no picker (implied when not a terminal)").action((opts) => agents(client, opts.capability, opts.all ? void 0 : opts.activeWithin, { all: opts.all, interactive: opts.interactive }));
4659
+ const tasks = program2.command("tasks").description("Capability-tagged task queue");
4660
+ tasks.command("list", { isDefault: true }).description("List tasks (no filters: open tasks matching your card + your claims)").option("-s, --status <status>", "open | claimed | done | cancelled").option("-c, --capability <tag>", "Filter by capability tag").option("-m, --mine <side>", "created | claimed").option("--channel <name>", "Filter by channel").action((opts) => tasksList(client, opts));
4661
+ tasks.command("post <channel> <title>").description("Post a task for another agent to claim").option("-b, --body <text>", "Task details").option("-t, --tags <tags>", "Comma-separated capability tags, e.g. image-gen,vision").action((channel, title, opts) => tasksPost(client, channel, title, opts));
4662
+ tasks.command("claim <id>").description("Claim an open task (atomic \u2014 one winner)").action((id) => tasksUpdate(client, id, "claim"));
4663
+ tasks.command("complete <id> <result>").description("Complete a task you claimed; the result is posted to the channel").action((id, result) => tasksUpdate(client, id, "complete", result));
4664
+ tasks.command("cancel <id>").description("Cancel a task you created").action((id) => tasksUpdate(client, id, "cancel"));
4250
4665
  program2.command("notes [channel]").description("List notes in a channel, or across all channels + global if omitted").option("-s, --search <query>", "Full-text search within notes").option("--stubs", "Include unfilled stubs").option("-l, --limit <n>", "Max notes to list").action((channel, opts) => notesList(client, channel, opts));
4251
4666
  program2.command("note <scope> <slug>").description('Read a note. <scope> is a channel name or "global"').option("-r, --revision <n>", "Read a specific past revision").action((scope, slug, opts) => noteRead(client, scope, slug, opts));
4252
4667
  program2.command("write-note <scope> <slug>").description("Create or update a note. Body from --body, --body-file, or stdin").option("-t, --title <title>", "Note title (defaults to the slug)").option("-b, --body <text>", "Note body (Markdown)").option("-f, --body-file <path>", "Read the body from a file").option("--protect", "Mark the note protected (creator-only writes)").option("--expected-revision <n>", "Optimistic concurrency: fail if the note moved on").action((scope, slug, opts) => noteWrite(client, scope, slug, opts));
@@ -4263,7 +4678,194 @@ async function run(argv = process.argv) {
4263
4678
  await program2.parseAsync(argv);
4264
4679
  }
4265
4680
 
4681
+ // src/harnesses.ts
4682
+ import * as fs3 from "node:fs";
4683
+ import * as path3 from "node:path";
4684
+ import { execSync } from "node:child_process";
4685
+ function binaryExists(bin) {
4686
+ try {
4687
+ execSync(process.platform === "win32" ? `where ${bin}` : `command -v ${bin}`, { stdio: "pipe" });
4688
+ return true;
4689
+ } catch {
4690
+ return false;
4691
+ }
4692
+ }
4693
+ function fwd(p) {
4694
+ return p.replace(/\\/g, "/");
4695
+ }
4696
+ function launchParts(launch) {
4697
+ return {
4698
+ command: fwd(launch.nodePath),
4699
+ args: [fwd(launch.tsxPath), fwd(launch.serverPath)]
4700
+ };
4701
+ }
4702
+ function readIfExists(filePath) {
4703
+ try {
4704
+ return fs3.readFileSync(filePath, "utf-8");
4705
+ } catch (e) {
4706
+ if (e?.code === "ENOENT") return null;
4707
+ throw e;
4708
+ }
4709
+ }
4710
+ function mergeJsonFile(filePath, mutate, manualSnippet) {
4711
+ let config = {};
4712
+ const raw = readIfExists(filePath);
4713
+ if (raw !== null && raw.trim()) {
4714
+ try {
4715
+ config = JSON.parse(raw);
4716
+ } catch {
4717
+ return {
4718
+ ok: false,
4719
+ message: `${filePath} exists but could not be parsed (comments or trailing commas?). Left untouched \u2014 add the entry manually:`,
4720
+ manualSnippet
4721
+ };
4722
+ }
4723
+ }
4724
+ mutate(config);
4725
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
4726
+ fs3.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
4727
+ return { ok: true, message: filePath };
4728
+ }
4729
+ function tryCliRegister(cmd, manualSnippet) {
4730
+ try {
4731
+ execSync(cmd, { stdio: "pipe" });
4732
+ return { ok: true, message: "Registered via CLI" };
4733
+ } catch {
4734
+ return {
4735
+ ok: false,
4736
+ message: `Could not run "${cmd.split(" ").slice(0, 3).join(" ")} \u2026". Add manually:`,
4737
+ manualSnippet
4738
+ };
4739
+ }
4740
+ }
4741
+ var HARNESSES = [
4742
+ {
4743
+ key: "claude",
4744
+ label: "Claude Code",
4745
+ instructionsPath: (home) => path3.join(home, ".claude", "CLAUDE.md"),
4746
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".claude")) || hasBinary("claude"),
4747
+ registerMcp(launch) {
4748
+ const { command, args } = launchParts(launch);
4749
+ const cmd = `claude mcp add airchat -s user -- "${command}" ${args.map((a) => `"${a}"`).join(" ")}`;
4750
+ return tryCliRegister(cmd, cmd);
4751
+ }
4752
+ },
4753
+ {
4754
+ key: "codex",
4755
+ label: "Codex CLI",
4756
+ instructionsPath: (home) => path3.join(home, ".codex", "AGENTS.md"),
4757
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".codex")) || hasBinary("codex"),
4758
+ registerMcp(launch) {
4759
+ const { command, args } = launchParts(launch);
4760
+ const cmd = `codex mcp add airchat -- "${command}" ${args.map((a) => `"${a}"`).join(" ")}`;
4761
+ const toml = [
4762
+ "# ~/.codex/config.toml",
4763
+ "[mcp_servers.airchat]",
4764
+ `command = "${command}"`,
4765
+ `args = [${args.map((a) => `"${a}"`).join(", ")}]`
4766
+ ].join("\n");
4767
+ return tryCliRegister(cmd, toml);
4768
+ }
4769
+ },
4770
+ {
4771
+ key: "antigravity",
4772
+ label: "Antigravity CLI (formerly Gemini CLI)",
4773
+ // Both GEMINI.md and AGENTS.md are honored; GEMINI.md is the long-standing
4774
+ // global location so existing setups keep working.
4775
+ instructionsPath: (home) => path3.join(home, ".gemini", "GEMINI.md"),
4776
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".gemini", "config")) || hasBinary("agy") || hasBinary("gemini"),
4777
+ registerMcp(launch, home) {
4778
+ const { command, args } = launchParts(launch);
4779
+ const filePath = path3.join(home, ".gemini", "config", "mcp_config.json");
4780
+ const snippet = JSON.stringify({ mcpServers: { airchat: { command, args } } }, null, 2);
4781
+ return mergeJsonFile(
4782
+ filePath,
4783
+ (config) => {
4784
+ config.mcpServers = config.mcpServers ?? {};
4785
+ config.mcpServers.airchat = { command, args };
4786
+ },
4787
+ `// ${filePath}
4788
+ ${snippet}`
4789
+ );
4790
+ }
4791
+ },
4792
+ {
4793
+ key: "cursor",
4794
+ label: "Cursor",
4795
+ // Cursor's global rules live in app settings, not a file; project-level
4796
+ // AGENTS.md is out of scope for a global installer.
4797
+ instructionsPath: null,
4798
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".cursor")) || hasBinary("cursor-agent"),
4799
+ registerMcp(launch, home) {
4800
+ const { command, args } = launchParts(launch);
4801
+ const filePath = path3.join(home, ".cursor", "mcp.json");
4802
+ const entry = { type: "stdio", command, args };
4803
+ const snippet = JSON.stringify({ mcpServers: { airchat: entry } }, null, 2);
4804
+ return mergeJsonFile(
4805
+ filePath,
4806
+ (config) => {
4807
+ config.mcpServers = config.mcpServers ?? {};
4808
+ config.mcpServers.airchat = entry;
4809
+ },
4810
+ `// ${filePath}
4811
+ ${snippet}`
4812
+ );
4813
+ }
4814
+ },
4815
+ {
4816
+ key: "opencode",
4817
+ label: "OpenCode",
4818
+ instructionsPath: (home) => path3.join(home, ".config", "opencode", "AGENTS.md"),
4819
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".config", "opencode")) || hasBinary("opencode"),
4820
+ registerMcp(launch, home) {
4821
+ const { command, args } = launchParts(launch);
4822
+ const filePath = path3.join(home, ".config", "opencode", "opencode.json");
4823
+ const entry = { type: "local", command: [command, ...args], enabled: true };
4824
+ const snippet = JSON.stringify({ mcp: { airchat: entry } }, null, 2);
4825
+ return mergeJsonFile(
4826
+ filePath,
4827
+ (config) => {
4828
+ config.mcp = config.mcp ?? {};
4829
+ config.mcp.airchat = entry;
4830
+ },
4831
+ `// ${filePath}
4832
+ ${snippet}`
4833
+ );
4834
+ }
4835
+ }
4836
+ ];
4837
+ function detectHarnesses(home, hasBinary = binaryExists) {
4838
+ return HARNESSES.filter((h) => h.detect(home, hasBinary));
4839
+ }
4840
+ function genericSnippet(launch) {
4841
+ const { command, args } = launchParts(launch);
4842
+ return [
4843
+ "AirChat is a standard stdio MCP server. For any other MCP client, register:",
4844
+ ` command: ${command}`,
4845
+ ` args: ${args.join(" ")}`,
4846
+ " env: none (reads ~/.airchat/config and ~/.airchat/machine.key)"
4847
+ ].join("\n");
4848
+ }
4849
+ function installInstructions(filePath, content) {
4850
+ try {
4851
+ const existing = readIfExists(filePath);
4852
+ if (existing !== null) {
4853
+ if (existing.includes("# AirChat")) {
4854
+ return { ok: true, message: `Already present in ${filePath}` };
4855
+ }
4856
+ fs3.appendFileSync(filePath, "\n" + content);
4857
+ } else {
4858
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
4859
+ fs3.writeFileSync(filePath, content);
4860
+ }
4861
+ return { ok: true, message: filePath };
4862
+ } catch (e) {
4863
+ return { ok: false, message: e.message };
4864
+ }
4865
+ }
4866
+
4266
4867
  // src/index.ts
4868
+ var AIRCHAT_VERSION = true ? "1.0.0" : "dev";
4267
4869
  var DEFAULT_AIRCHAT_URL2 = "https://supernode-web-production.up.railway.app";
4268
4870
  var GREEN = "\x1B[32m";
4269
4871
  var YELLOW = "\x1B[33m";
@@ -4287,10 +4889,10 @@ function bold(msg) {
4287
4889
  return `${BOLD}${msg}${RESET}`;
4288
4890
  }
4289
4891
  function readExistingConfig() {
4290
- const configPath = path2.join(os2.homedir(), ".airchat", "config");
4892
+ const configPath = path4.join(os3.homedir(), ".airchat", "config");
4291
4893
  const config = {};
4292
4894
  try {
4293
- const lines = fs2.readFileSync(configPath, "utf-8").split("\n");
4895
+ const lines = fs4.readFileSync(configPath, "utf-8").split("\n");
4294
4896
  for (const line of lines) {
4295
4897
  const trimmed = line.trim();
4296
4898
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -4308,10 +4910,10 @@ function detectNodePath() {
4308
4910
  function detectRepoDir() {
4309
4911
  let dir = process.cwd();
4310
4912
  for (let i = 0; i < 10; i++) {
4311
- if (fs2.existsSync(path2.join(dir, "packages", "mcp-server", "src", "index.ts"))) {
4913
+ if (fs4.existsSync(path4.join(dir, "packages", "mcp-server", "src", "index.ts"))) {
4312
4914
  return dir;
4313
4915
  }
4314
- const parent = path2.dirname(dir);
4916
+ const parent = path4.dirname(dir);
4315
4917
  if (parent === dir) break;
4316
4918
  dir = parent;
4317
4919
  }
@@ -4382,7 +4984,7 @@ async function collectConfig(rl, reconfigure) {
4382
4984
  console.log(` ${dim("A PostgreSQL container will be started alongside the dashboard.")}`);
4383
4985
  console.log("");
4384
4986
  }
4385
- const defaultMachine = existing.MACHINE_NAME || os2.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "laptop";
4987
+ const defaultMachine = existing.MACHINE_NAME || os3.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "laptop";
4386
4988
  let machineName = "";
4387
4989
  while (!machineName) {
4388
4990
  machineName = await ask(rl, "Machine name (e.g. laptop, server, nas)", defaultMachine);
@@ -4392,6 +4994,21 @@ async function collectConfig(rl, reconfigure) {
4392
4994
  }
4393
4995
  }
4394
4996
  console.log("");
4997
+ const detected = detectHarnesses(os3.homedir());
4998
+ const detectedKeys = new Set(detected.map((h) => h.key));
4999
+ console.log(" Which AI harnesses should connect to AirChat?");
5000
+ HARNESSES.forEach((h, i) => {
5001
+ const mark = detectedKeys.has(h.key) ? ` ${GREEN}(detected)${RESET}` : "";
5002
+ console.log(` ${BOLD}${i + 1}${RESET}) ${h.label}${mark}`);
5003
+ });
5004
+ console.log(` ${BOLD}${HARNESSES.length + 1}${RESET}) Other MCP client ${dim("(prints a generic snippet)")}`);
5005
+ const fallbackSel = detected.length ? detected.map((h) => h.key) : ["claude"];
5006
+ const selRaw = await rl.question(
5007
+ ` Select, comma-separated ${dim(`(Enter = ${fallbackSel.join(", ")})`)}: `
5008
+ );
5009
+ let harnesses = selRaw.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((idx) => idx >= 0 && idx <= HARNESSES.length).map((idx) => idx === HARNESSES.length ? "other" : HARNESSES[idx].key);
5010
+ if (harnesses.length === 0) harnesses = fallbackSel;
5011
+ console.log("");
4395
5012
  const deployDashboard = await askYesNo(rl, "Deploy web dashboard?", true);
4396
5013
  let dashboardPort = 3003;
4397
5014
  if (deployDashboard) {
@@ -4428,7 +5045,7 @@ async function collectConfig(rl, reconfigure) {
4428
5045
  if (!airchatDir) {
4429
5046
  const shouldClone = await askYesNo(rl, "Clone the airchat repo?", true);
4430
5047
  if (shouldClone) {
4431
- const cloneDir = await ask(rl, "Clone to", path2.join(os2.homedir(), "projects", "airchat"));
5048
+ const cloneDir = await ask(rl, "Clone to", path4.join(os3.homedir(), "projects", "airchat"));
4432
5049
  airchatDir = cloneDir;
4433
5050
  } else {
4434
5051
  airchatDir = await ask(rl, "Path to existing airchat repo");
@@ -4455,6 +5072,7 @@ async function collectConfig(rl, reconfigure) {
4455
5072
  supabaseServiceRoleKey,
4456
5073
  databaseUrl,
4457
5074
  machineName,
5075
+ harnesses,
4458
5076
  dashboardPort,
4459
5077
  deployDashboard,
4460
5078
  airchatDir,
@@ -4468,14 +5086,14 @@ async function collectConfig(rl, reconfigure) {
4468
5086
  }
4469
5087
  async function cloneAndInstall(config) {
4470
5088
  const name = "Clone & install";
4471
- if (fs2.existsSync(path2.join(config.airchatDir, "packages", "mcp-server"))) {
5089
+ if (fs4.existsSync(path4.join(config.airchatDir, "packages", "mcp-server"))) {
4472
5090
  return { name, ok: true, message: "Repo already exists" };
4473
5091
  }
4474
5092
  try {
4475
5093
  console.log(` Cloning airchat...`);
4476
- execSync(`git clone https://github.com/prone/airchat.git "${config.airchatDir}"`, { stdio: "pipe" });
5094
+ execSync2(`git clone https://github.com/prone/airchat.git "${config.airchatDir}"`, { stdio: "pipe" });
4477
5095
  console.log(` Installing dependencies...`);
4478
- execSync("npm install", { cwd: config.airchatDir, stdio: "pipe" });
5096
+ execSync2("npm install", { cwd: config.airchatDir, stdio: "pipe" });
4479
5097
  return { name, ok: true, message: "Cloned and installed" };
4480
5098
  } catch (e) {
4481
5099
  return { name, ok: false, message: `Failed: ${e.message}` };
@@ -4490,27 +5108,27 @@ async function runMigrations(config) {
4490
5108
  const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
4491
5109
  auth: { persistSession: false }
4492
5110
  });
4493
- const { data: agents } = await supabase.from("agents").select("id").limit(1);
4494
- if (agents !== null) {
5111
+ const { data: agents2 } = await supabase.from("agents").select("id").limit(1);
5112
+ if (agents2 !== null) {
4495
5113
  return { name, ok: true, message: "Tables already exist \u2014 skipping migrations" };
4496
5114
  }
4497
5115
  try {
4498
- execSync("which supabase", { stdio: "pipe" });
4499
- execSync(`supabase db push --db-url "${config.supabaseUrl}"`, {
5116
+ execSync2("which supabase", { stdio: "pipe" });
5117
+ execSync2(`supabase db push --db-url "${config.supabaseUrl}"`, {
4500
5118
  cwd: config.airchatDir,
4501
5119
  stdio: "pipe"
4502
5120
  });
4503
5121
  return { name, ok: true, message: "Migrations applied via Supabase CLI" };
4504
5122
  } catch {
4505
5123
  }
4506
- const migrationsDir = path2.join(config.airchatDir, "supabase", "migrations");
4507
- if (!fs2.existsSync(migrationsDir)) {
5124
+ const migrationsDir = path4.join(config.airchatDir, "supabase", "migrations");
5125
+ if (!fs4.existsSync(migrationsDir)) {
4508
5126
  return { name, ok: false, message: `Migrations directory not found at ${migrationsDir}` };
4509
5127
  }
4510
- const files = fs2.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
5128
+ const files = fs4.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
4511
5129
  let applied = 0;
4512
5130
  for (const file of files) {
4513
- const sql = fs2.readFileSync(path2.join(migrationsDir, file), "utf-8");
5131
+ const sql = fs4.readFileSync(path4.join(migrationsDir, file), "utf-8");
4514
5132
  const { error } = await supabase.rpc("", {}).then(
4515
5133
  () => ({ error: null }),
4516
5134
  () => ({ error: null })
@@ -4528,11 +5146,11 @@ async function runMigrations(config) {
4528
5146
  return { name, ok: true, message: "Migrations applied" };
4529
5147
  }
4530
5148
  if (config.dbProvider === "self-hosted" && config.databaseUrl) {
4531
- const migrationsDir = path2.join(config.airchatDir, "supabase", "migrations");
4532
- const files = fs2.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
5149
+ const migrationsDir = path4.join(config.airchatDir, "supabase", "migrations");
5150
+ const files = fs4.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
4533
5151
  try {
4534
5152
  for (const file of files) {
4535
- execSync(`psql "${config.databaseUrl}" -f "${path2.join(migrationsDir, file)}"`, { stdio: "pipe" });
5153
+ execSync2(`psql "${config.databaseUrl}" -f "${path4.join(migrationsDir, file)}"`, { stdio: "pipe" });
4536
5154
  }
4537
5155
  return { name, ok: true, message: `Applied ${files.length} migrations` };
4538
5156
  } catch (e) {
@@ -4564,12 +5182,12 @@ async function seedChannels(config) {
4564
5182
  }
4565
5183
  async function generateAndRegisterKeypair(config, reconfigure) {
4566
5184
  const name = "Generate Ed25519 keypair";
4567
- const configDir = path2.join(os2.homedir(), ".airchat");
4568
- const privateKeyPath = path2.join(configDir, "machine.key");
4569
- const publicKeyPath = path2.join(configDir, "machine.pub");
4570
- if (!reconfigure && fs2.existsSync(privateKeyPath) && fs2.existsSync(publicKeyPath)) {
4571
- const existingPub = fs2.readFileSync(publicKeyPath, "utf-8").trim();
4572
- const existingPriv = fs2.readFileSync(privateKeyPath, "utf-8").trim();
5185
+ const configDir = path4.join(os3.homedir(), ".airchat");
5186
+ const privateKeyPath = path4.join(configDir, "machine.key");
5187
+ const publicKeyPath = path4.join(configDir, "machine.pub");
5188
+ if (!reconfigure && fs4.existsSync(privateKeyPath) && fs4.existsSync(publicKeyPath)) {
5189
+ const existingPub = fs4.readFileSync(publicKeyPath, "utf-8").trim();
5190
+ const existingPriv = fs4.readFileSync(privateKeyPath, "utf-8").trim();
4573
5191
  if (existingPub.length === 64 && existingPriv.length === 64) {
4574
5192
  config.keypair = { publicKey: existingPub, privateKey: existingPriv };
4575
5193
  return { name, ok: true, message: "Existing keypair found \u2014 reusing" };
@@ -4577,11 +5195,11 @@ async function generateAndRegisterKeypair(config, reconfigure) {
4577
5195
  }
4578
5196
  const keypair = generateKeypair();
4579
5197
  config.keypair = keypair;
4580
- fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
4581
- fs2.writeFileSync(privateKeyPath, keypair.privateKey + "\n", { mode: 384 });
4582
- fs2.writeFileSync(publicKeyPath, keypair.publicKey + "\n", { mode: 420 });
4583
- fs2.chmodSync(configDir, 448);
4584
- fs2.chmodSync(privateKeyPath, 384);
5198
+ fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
5199
+ fs4.writeFileSync(privateKeyPath, keypair.privateKey + "\n", { mode: 384 });
5200
+ fs4.writeFileSync(publicKeyPath, keypair.publicKey + "\n", { mode: 420 });
5201
+ fs4.chmodSync(configDir, 448);
5202
+ fs4.chmodSync(privateKeyPath, 384);
4585
5203
  if (config.dbProvider === "supabase" && config.supabaseUrl && config.supabaseServiceRoleKey) {
4586
5204
  const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
4587
5205
  auth: { persistSession: false }
@@ -4630,8 +5248,8 @@ async function generateAndRegisterKeypair(config, reconfigure) {
4630
5248
  function writeAirchatConfig(config) {
4631
5249
  const name = "Write ~/.airchat/config";
4632
5250
  try {
4633
- const configDir = path2.join(os2.homedir(), ".airchat");
4634
- fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
5251
+ const configDir = path4.join(os3.homedir(), ".airchat");
5252
+ fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
4635
5253
  const lines = [
4636
5254
  `MACHINE_NAME=${config.machineName}`
4637
5255
  ];
@@ -4639,11 +5257,11 @@ function writeAirchatConfig(config) {
4639
5257
  if (config.slackBotToken) lines.push(`SLACK_BOT_TOKEN=${config.slackBotToken}`);
4640
5258
  if (config.slackAppToken) lines.push(`SLACK_APP_TOKEN=${config.slackAppToken}`);
4641
5259
  if (config.slackWebhookUrl) lines.push(`SLACK_WEBHOOK_URL=${config.slackWebhookUrl}`);
4642
- fs2.writeFileSync(path2.join(configDir, "config"), lines.join("\n") + "\n", { mode: 384 });
4643
- const agentsDir = path2.join(configDir, "agents");
4644
- fs2.mkdirSync(agentsDir, { recursive: true, mode: 448 });
4645
- fs2.chmodSync(agentsDir, 448);
4646
- fs2.chmodSync(configDir, 448);
5260
+ fs4.writeFileSync(path4.join(configDir, "config"), lines.join("\n") + "\n", { mode: 384 });
5261
+ const agentsDir = path4.join(configDir, "agents");
5262
+ fs4.mkdirSync(agentsDir, { recursive: true, mode: 448 });
5263
+ fs4.chmodSync(agentsDir, 448);
5264
+ fs4.chmodSync(configDir, 448);
4647
5265
  return { name, ok: true, message: configDir };
4648
5266
  } catch (e) {
4649
5267
  return { name, ok: false, message: e.message };
@@ -4655,7 +5273,7 @@ function writeWebEnv(config) {
4655
5273
  return { name, ok: true, message: "Skipped" };
4656
5274
  }
4657
5275
  try {
4658
- const envPath = path2.join(config.airchatDir, "apps", "web", ".env.local");
5276
+ const envPath = path4.join(config.airchatDir, "apps", "web", ".env.local");
4659
5277
  const lines = [];
4660
5278
  if (config.supabaseUrl) {
4661
5279
  lines.push(`NEXT_PUBLIC_SUPABASE_URL=${config.supabaseUrl}`);
@@ -4666,76 +5284,73 @@ function writeWebEnv(config) {
4666
5284
  if (config.databaseUrl) {
4667
5285
  lines.push(`DATABASE_URL=${config.databaseUrl}`);
4668
5286
  }
4669
- fs2.writeFileSync(envPath, lines.join("\n") + "\n");
5287
+ fs4.writeFileSync(envPath, lines.join("\n") + "\n");
4670
5288
  return { name, ok: true, message: envPath };
4671
5289
  } catch (e) {
4672
5290
  return { name, ok: false, message: e.message };
4673
5291
  }
4674
5292
  }
4675
- function registerMcpServer(config) {
4676
- const name = "Register MCP server";
4677
- const tsxPath = path2.join(config.airchatDir, "node_modules", ".bin", "tsx");
4678
- const serverPath = path2.join(config.airchatDir, "packages", "mcp-server", "src", "index.ts");
4679
- if (!fs2.existsSync(serverPath)) {
4680
- return { name, ok: false, message: `MCP server not found at ${serverPath}` };
4681
- }
4682
- const cmd = `claude mcp add airchat -s user -- "${config.nodePath}" "${tsxPath}" "${serverPath}"`.replace(/\\/g, "/");
4683
- try {
4684
- execSync(cmd, { stdio: "pipe" });
4685
- return { name, ok: true, message: "Registered at user level (no env vars \u2014 reads from ~/.airchat/)" };
4686
- } catch (e) {
4687
- return {
4688
- name,
4689
- ok: false,
4690
- message: `Could not run "claude mcp add". Run manually:
4691
- ${cmd}`
4692
- };
5293
+ function buildMcpLaunch(config) {
5294
+ const tsxPath = path4.join(config.airchatDir, "node_modules", ".bin", "tsx");
5295
+ const serverPath = path4.join(config.airchatDir, "packages", "mcp-server", "src", "index.ts");
5296
+ if (!fs4.existsSync(serverPath)) return null;
5297
+ return { nodePath: config.nodePath, tsxPath, serverPath };
5298
+ }
5299
+ function registerMcpServers(config) {
5300
+ const launch = buildMcpLaunch(config);
5301
+ if (!launch) {
5302
+ return [{ name: "Register MCP server", ok: false, message: `MCP server not found under ${config.airchatDir}` }];
5303
+ }
5304
+ const results = [];
5305
+ for (const key of config.harnesses) {
5306
+ if (key === "other") continue;
5307
+ const harness = HARNESSES.find((h) => h.key === key);
5308
+ if (!harness) continue;
5309
+ const res = harness.registerMcp(launch, os3.homedir());
5310
+ const message = res.manualSnippet ? `${res.message}
5311
+ ${res.manualSnippet.split("\n").map((l) => ` ${l}`).join("\n")}` : res.message;
5312
+ results.push({ name: `MCP \xB7 ${harness.label}`, ok: res.ok, message });
4693
5313
  }
5314
+ return results;
4694
5315
  }
4695
- function installClaudeMd(config) {
4696
- const name = "Install agent instructions";
4697
- try {
4698
- const srcPath = path2.join(config.airchatDir, "setup", "global-CLAUDE.md");
4699
- if (!fs2.existsSync(srcPath)) {
4700
- return { name, ok: false, message: "setup/global-CLAUDE.md not found" };
4701
- }
4702
- const content = fs2.readFileSync(srcPath, "utf-8");
4703
- const claudeMdPath = path2.join(os2.homedir(), ".claude", "CLAUDE.md");
4704
- if (fs2.existsSync(claudeMdPath)) {
4705
- const existing = fs2.readFileSync(claudeMdPath, "utf-8");
4706
- if (existing.includes("# AirChat")) {
4707
- return { name, ok: true, message: "Already present in ~/.claude/CLAUDE.md" };
4708
- }
4709
- fs2.appendFileSync(claudeMdPath, "\n" + content);
4710
- } else {
4711
- fs2.mkdirSync(path2.dirname(claudeMdPath), { recursive: true });
4712
- fs2.writeFileSync(claudeMdPath, content);
4713
- }
4714
- return { name, ok: true, message: "~/.claude/CLAUDE.md" };
4715
- } catch (e) {
4716
- return { name, ok: false, message: e.message };
5316
+ function installAgentInstructions(config) {
5317
+ const srcPath = path4.join(config.airchatDir, "setup", "agent-instructions.md");
5318
+ if (!fs4.existsSync(srcPath)) {
5319
+ return [{ name: "Install agent instructions", ok: false, message: "setup/agent-instructions.md not found" }];
5320
+ }
5321
+ const content = fs4.readFileSync(srcPath, "utf-8");
5322
+ const results = [];
5323
+ for (const key of config.harnesses) {
5324
+ const harness = HARNESSES.find((h) => h.key === key);
5325
+ if (!harness?.instructionsPath) continue;
5326
+ const res = installInstructions(harness.instructionsPath(os3.homedir()), content);
5327
+ results.push({ name: `Instructions \xB7 ${harness.label}`, ok: res.ok, message: res.message });
4717
5328
  }
5329
+ return results;
4718
5330
  }
4719
5331
  function installMentionHook(config) {
4720
- const name = "Install mention hook";
5332
+ const name = "Install work hook";
4721
5333
  try {
4722
- const settingsPath = path2.join(os2.homedir(), ".claude", "settings.json");
5334
+ const settingsPath = path4.join(os3.homedir(), ".claude", "settings.json");
4723
5335
  let settings = {};
4724
- if (fs2.existsSync(settingsPath)) {
4725
- settings = JSON.parse(fs2.readFileSync(settingsPath, "utf-8"));
4726
- }
4727
- const hookCommand = `${config.nodePath} ${path2.join(config.airchatDir, "scripts", "check-mentions.mjs")}`.replace(/\\/g, "/");
4728
- if (settings.hooks?.UserPromptSubmit) {
4729
- const existing = settings.hooks.UserPromptSubmit;
4730
- const alreadyHasHook = Array.isArray(existing) && existing.some(
4731
- (entry) => entry.hooks?.some((h) => h.command?.includes("check-mentions"))
4732
- );
4733
- if (alreadyHasHook) {
4734
- return { name, ok: true, message: "Hook already configured" };
4735
- }
5336
+ try {
5337
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
5338
+ } catch (e) {
5339
+ if (e?.code !== "ENOENT") throw e;
4736
5340
  }
5341
+ const hookCommand = `${config.nodePath} ${path4.join(config.airchatDir, "scripts", "check-work.mjs")}`.replace(/\\/g, "/");
4737
5342
  if (!settings.hooks) settings.hooks = {};
4738
5343
  if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
5344
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
5345
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("check-mentions"))
5346
+ );
5347
+ const alreadyHasHook = settings.hooks.UserPromptSubmit.some(
5348
+ (entry) => entry.hooks?.some((h) => h.command?.includes("check-work"))
5349
+ );
5350
+ if (alreadyHasHook) {
5351
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5352
+ return { name, ok: true, message: "Hook already configured" };
5353
+ }
4739
5354
  settings.hooks.UserPromptSubmit.push({
4740
5355
  matcher: "",
4741
5356
  hooks: [{
@@ -4743,8 +5358,8 @@ function installMentionHook(config) {
4743
5358
  command: hookCommand
4744
5359
  }]
4745
5360
  });
4746
- fs2.mkdirSync(path2.dirname(settingsPath), { recursive: true });
4747
- fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5361
+ fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5362
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4748
5363
  return { name, ok: true, message: "~/.claude/settings.json" };
4749
5364
  } catch (e) {
4750
5365
  return { name, ok: false, message: e.message };
@@ -4753,12 +5368,12 @@ function installMentionHook(config) {
4753
5368
  function copySlashCommands(config) {
4754
5369
  const name = "Copy slash commands";
4755
5370
  try {
4756
- const setupDir = path2.join(config.airchatDir, "setup");
4757
- const commandsDir = path2.join(os2.homedir(), ".claude", "commands");
4758
- fs2.mkdirSync(commandsDir, { recursive: true });
4759
- const files = fs2.readdirSync(setupDir).filter((f) => f.startsWith("airchat-") && f.endsWith(".md"));
5371
+ const setupDir = path4.join(config.airchatDir, "setup");
5372
+ const commandsDir = path4.join(os3.homedir(), ".claude", "commands");
5373
+ fs4.mkdirSync(commandsDir, { recursive: true });
5374
+ const files = fs4.readdirSync(setupDir).filter((f) => f.startsWith("airchat-") && f.endsWith(".md"));
4760
5375
  for (const file of files) {
4761
- fs2.copyFileSync(path2.join(setupDir, file), path2.join(commandsDir, file));
5376
+ fs4.copyFileSync(path4.join(setupDir, file), path4.join(commandsDir, file));
4762
5377
  }
4763
5378
  return { name, ok: true, message: `${files.length} commands \u2192 ~/.claude/commands/` };
4764
5379
  } catch (e) {
@@ -4768,9 +5383,9 @@ function copySlashCommands(config) {
4768
5383
  async function main() {
4769
5384
  const reconfigure = process.argv.includes("--reconfigure");
4770
5385
  console.log("");
4771
- console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim("v0.3.0")}`);
5386
+ console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim(`v${AIRCHAT_VERSION}`)}`);
4772
5387
  console.log(` ${dim("Your AI agents can talk to each other")}`);
4773
- const rl = readline.createInterface({ input, output });
5388
+ const rl = readline2.createInterface({ input, output });
4774
5389
  let config;
4775
5390
  try {
4776
5391
  config = await collectConfig(rl, reconfigure);
@@ -4780,6 +5395,7 @@ async function main() {
4780
5395
  console.log(` ${bold("Summary:")}`);
4781
5396
  console.log(` Database: ${config.dbProvider}`);
4782
5397
  console.log(` Machine: ${config.machineName}`);
5398
+ console.log(` Harnesses: ${config.harnesses.join(", ")}`);
4783
5399
  console.log(` Repo: ${config.airchatDir}`);
4784
5400
  console.log(` Node: ${config.nodePath}`);
4785
5401
  if (config.deployDashboard) {
@@ -4827,22 +5443,26 @@ async function main() {
4827
5443
  if (envResult.ok && config.deployDashboard) ok(`Dashboard env \u2192 ${envResult.message}`);
4828
5444
  else if (!config.deployDashboard) {
4829
5445
  } else fail(envResult.message);
4830
- const mcpResult = registerMcpServer(config);
4831
- results.push(mcpResult);
4832
- if (mcpResult.ok) ok(`MCP server \u2192 ${mcpResult.message}`);
4833
- else warn(mcpResult.message);
4834
- const claudeResult = installClaudeMd(config);
4835
- results.push(claudeResult);
4836
- if (claudeResult.ok) ok(`Instructions \u2192 ${claudeResult.message}`);
4837
- else fail(claudeResult.message);
4838
- const hookResult = installMentionHook(config);
4839
- results.push(hookResult);
4840
- if (hookResult.ok) ok(`Mention hook \u2192 ${hookResult.message}`);
4841
- else fail(hookResult.message);
4842
- const cmdResult = copySlashCommands(config);
4843
- results.push(cmdResult);
4844
- if (cmdResult.ok) ok(`Slash commands \u2192 ${cmdResult.message}`);
4845
- else fail(cmdResult.message);
5446
+ for (const r of registerMcpServers(config)) {
5447
+ results.push(r);
5448
+ if (r.ok) ok(`${r.name} \u2192 ${r.message}`);
5449
+ else warn(`${r.name}: ${r.message}`);
5450
+ }
5451
+ for (const r of installAgentInstructions(config)) {
5452
+ results.push(r);
5453
+ if (r.ok) ok(`${r.name} \u2192 ${r.message}`);
5454
+ else fail(`${r.name}: ${r.message}`);
5455
+ }
5456
+ if (config.harnesses.includes("claude")) {
5457
+ const hookResult = installMentionHook(config);
5458
+ results.push(hookResult);
5459
+ if (hookResult.ok) ok(`Mention hook \u2192 ${hookResult.message}`);
5460
+ else fail(hookResult.message);
5461
+ const cmdResult = copySlashCommands(config);
5462
+ results.push(cmdResult);
5463
+ if (cmdResult.ok) ok(`Slash commands \u2192 ${cmdResult.message}`);
5464
+ else fail(cmdResult.message);
5465
+ }
4846
5466
  console.log("");
4847
5467
  const failures = results.filter((r) => !r.ok);
4848
5468
  if (failures.length === 0) {
@@ -4868,21 +5488,31 @@ async function main() {
4868
5488
  console.log(` ~/.airchat/machine.key ${dim("Ed25519 private key")}`);
4869
5489
  console.log(` ~/.airchat/machine.pub ${dim("Ed25519 public key")}`);
4870
5490
  console.log(` ~/.airchat/agents/ ${dim("Cached derived keys (auto-created)")}`);
5491
+ if (config.harnesses.includes("other")) {
5492
+ const launch = buildMcpLaunch(config);
5493
+ if (launch) {
5494
+ console.log("");
5495
+ console.log(genericSnippet(launch).split("\n").map((l) => ` ${l}`).join("\n"));
5496
+ }
5497
+ }
4871
5498
  console.log("");
4872
5499
  console.log(` ${dim("Next steps:")}`);
4873
5500
  let step = 1;
4874
- console.log(` ${step++}. Restart Claude Code`);
5501
+ const selectedLabels = config.harnesses.filter((k) => k !== "other").map((k) => HARNESSES.find((h) => h.key === k)?.label).filter(Boolean);
5502
+ console.log(` ${step++}. Restart your agent sessions${selectedLabels.length ? ` (${selectedLabels.join(", ")})` : ""}`);
4875
5503
  if (config.deployDashboard) {
4876
5504
  console.log(` ${step++}. Start the dashboard: cd ${config.airchatDir} && docker compose up -d --build`);
4877
5505
  }
4878
5506
  if (config.slackBotToken) {
4879
- console.log(` ${step++}. Start the Slack bridge: node ${path2.join(config.airchatDir, "packages", "slack-bridge", "dist", "index.js")}`);
5507
+ console.log(` ${step++}. Start the Slack bridge: node ${path4.join(config.airchatDir, "packages", "slack-bridge", "dist", "index.js")}`);
4880
5508
  console.log(` ${dim("Or: cd " + config.airchatDir + " && npx tsx packages/slack-bridge/src/index.ts")}`);
4881
5509
  }
4882
5510
  console.log(` ${dim("Run with --reconfigure to regenerate keypair and update settings.")}`);
4883
5511
  console.log("");
4884
5512
  }
4885
5513
  var CLI_COMMANDS = /* @__PURE__ */ new Set([
5514
+ // 'dm' ships from fix/mention-hook-delivery; listed here so the published
5515
+ // bin routes it to the CLI once that branch merges.
4886
5516
  "doctor",
4887
5517
  "check",
4888
5518
  "read",
@@ -4890,13 +5520,18 @@ var CLI_COMMANDS = /* @__PURE__ */ new Set([
4890
5520
  "search",
4891
5521
  "status",
4892
5522
  "channels",
5523
+ "agents",
5524
+ "tasks",
5525
+ "dm",
4893
5526
  "notes",
4894
5527
  "note",
4895
5528
  "write-note",
4896
5529
  "backlinks",
4897
5530
  "summarize",
4898
5531
  "gossip",
4899
- "peer"
5532
+ "peer",
5533
+ "dm",
5534
+ "whoami"
4900
5535
  ]);
4901
5536
  var sub = process.argv[2];
4902
5537
  if (sub && CLI_COMMANDS.has(sub)) {