airchat 0.3.0 → 1.0.1

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 +834 -171
  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, execFileSync as execFileSync2 } 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) {
@@ -3403,23 +3486,42 @@ function unwrapEnvelope(body) {
3403
3486
  }
3404
3487
  return body;
3405
3488
  }
3489
+ var AirChatHttpError = class extends Error {
3490
+ status;
3491
+ /** From the `Retry-After` header, in ms. Undefined when absent or unparseable. */
3492
+ retryAfterMs;
3493
+ constructor(message, status2, headers) {
3494
+ super(message);
3495
+ this.name = "AirChatHttpError";
3496
+ this.status = status2;
3497
+ const seconds = Number(headers?.get("retry-after"));
3498
+ if (Number.isFinite(seconds) && seconds > 0) {
3499
+ this.retryAfterMs = seconds * 1e3;
3500
+ }
3501
+ }
3502
+ };
3406
3503
  var AirChatRestClient = class _AirChatRestClient {
3407
3504
  webUrl;
3408
3505
  machineName;
3409
3506
  privateKeyHex;
3410
3507
  agentName;
3411
3508
  cacheDir;
3509
+ card;
3412
3510
  derivedKey = null;
3413
3511
  registering = null;
3414
3512
  constructor(config) {
3415
3513
  if (!/^https?:\/\//i.test(config.webUrl)) {
3416
3514
  throw new Error(`Invalid AIRCHAT_WEB_URL: "${config.webUrl}" \u2014 must start with http:// or https://`);
3417
3515
  }
3418
- this.webUrl = config.webUrl.replace(/\/+$/, "");
3516
+ let webUrl = config.webUrl;
3517
+ while (webUrl.endsWith("/"))
3518
+ webUrl = webUrl.slice(0, -1);
3519
+ this.webUrl = webUrl;
3419
3520
  this.machineName = config.machineName;
3420
3521
  this.privateKeyHex = config.privateKeyHex;
3421
3522
  this.agentName = config.agentName;
3422
3523
  this.cacheDir = config.cacheDir ?? path.join(os.homedir(), ".airchat", "agents");
3524
+ this.card = config.card ?? null;
3423
3525
  this.checkPermissions();
3424
3526
  }
3425
3527
  // ── Public: identity ────────────────────────────────────────────────────
@@ -3431,8 +3533,60 @@ var AirChatRestClient = class _AirChatRestClient {
3431
3533
  return this.request("GET", "/api/v2/board");
3432
3534
  }
3433
3535
  // ── Public: agents ─────────────────────────────────────────────────────
3434
- async listAgents() {
3435
- return this.request("GET", "/api/v2/agents");
3536
+ async listAgents(capability, activeWithin) {
3537
+ const params = new URLSearchParams();
3538
+ if (capability)
3539
+ params.set("capability", capability);
3540
+ if (activeWithin)
3541
+ params.set("active_within", activeWithin);
3542
+ return this.request("GET", "/api/v2/agents", params);
3543
+ }
3544
+ /**
3545
+ * Update this agent's capability card on the server. Registration already
3546
+ * sends the card, but registration is skipped while a cached derived key
3547
+ * exists — this is the explicit path for card changes mid-life.
3548
+ */
3549
+ async setCard(card) {
3550
+ return this.request("POST", "/api/v2/agents/card", void 0, { card });
3551
+ }
3552
+ /**
3553
+ * Everything waiting for this agent in one call: unread mentions, open
3554
+ * tasks matching its card, its claimed tasks, and completions of tasks it
3555
+ * posted (since the given ISO timestamp; default last 24h).
3556
+ */
3557
+ async checkWork(since) {
3558
+ const params = new URLSearchParams();
3559
+ if (since)
3560
+ params.set("since", since);
3561
+ return this.request("GET", "/api/v2/work", params);
3562
+ }
3563
+ // ── Public: tasks ──────────────────────────────────────────────────────
3564
+ async postTask(channel, title, body, capabilityTags) {
3565
+ return this.request("POST", "/api/v2/tasks", void 0, {
3566
+ channel,
3567
+ title,
3568
+ body,
3569
+ capability_tags: capabilityTags
3570
+ });
3571
+ }
3572
+ async listTasks(opts) {
3573
+ const params = new URLSearchParams();
3574
+ if (opts?.status)
3575
+ params.set("status", opts.status);
3576
+ if (opts?.capability)
3577
+ params.set("capability", opts.capability);
3578
+ if (opts?.mine)
3579
+ params.set("mine", opts.mine);
3580
+ if (opts?.channel)
3581
+ params.set("channel", opts.channel);
3582
+ if (opts?.forMe)
3583
+ params.set("for_me", "1");
3584
+ if (opts?.limit !== void 0)
3585
+ params.set("limit", String(opts.limit));
3586
+ return this.request("GET", "/api/v2/tasks", params);
3587
+ }
3588
+ async updateTask(taskId, action, result) {
3589
+ return this.request("POST", `/api/v2/tasks/${encodeURIComponent(taskId)}`, void 0, { action, result });
3436
3590
  }
3437
3591
  // ── Public: channels ────────────────────────────────────────────────────
3438
3592
  async listChannels(type) {
@@ -3622,7 +3776,8 @@ var AirChatRestClient = class _AirChatRestClient {
3622
3776
  machineName,
3623
3777
  privateKeyHex,
3624
3778
  agentName,
3625
- cacheDir: overrides?.cacheDir
3779
+ cacheDir: overrides?.cacheDir,
3780
+ card: overrides?.card !== void 0 ? overrides.card : cardFromEnv()
3626
3781
  });
3627
3782
  }
3628
3783
  // ── Internal: HTTP request with auth ────────────────────────────────────
@@ -3631,17 +3786,17 @@ var AirChatRestClient = class _AirChatRestClient {
3631
3786
  const result = await this.doFetch(method, pathname, params, body);
3632
3787
  if (result.status === 401) {
3633
3788
  this.derivedKey = null;
3634
- await this.register();
3789
+ await this.register(true);
3635
3790
  const retry = await this.doFetch(method, pathname, params, body);
3636
3791
  if (!retry.ok) {
3637
3792
  const text = await retry.text().catch(() => "");
3638
- throw new Error(`AirChat API ${method} ${pathname} failed after re-registration: HTTP ${retry.status} \u2014 ${text}`);
3793
+ throw new AirChatHttpError(`AirChat API ${method} ${pathname} failed after re-registration: HTTP ${retry.status} \u2014 ${text}`, retry.status, retry.headers);
3639
3794
  }
3640
3795
  return unwrapEnvelope(await retry.json());
3641
3796
  }
3642
3797
  if (!result.ok) {
3643
3798
  const text = await result.text().catch(() => "");
3644
- throw new Error(`AirChat API ${method} ${pathname} failed: HTTP ${result.status} \u2014 ${text}`);
3799
+ throw new AirChatHttpError(`AirChat API ${method} ${pathname} failed: HTTP ${result.status} \u2014 ${text}`, result.status, result.headers);
3645
3800
  }
3646
3801
  return unwrapEnvelope(await result.json());
3647
3802
  }
@@ -3675,8 +3830,16 @@ var AirChatRestClient = class _AirChatRestClient {
3675
3830
  this.registering = null;
3676
3831
  }
3677
3832
  }
3678
- async register() {
3679
- const cachedKey = this.loadCachedKey();
3833
+ /**
3834
+ * @param ignoreCache Skip the on-disk key and register with the server for
3835
+ * real. Required when recovering from a 401: the cached key *is* the
3836
+ * credential the server just rejected, so reloading it sends the same bad
3837
+ * key again. Without this the retry path could never recover — it reported
3838
+ * "failed after re-registration" having made no registration request at
3839
+ * all, which is how deactivated agents came to look permanently broken.
3840
+ */
3841
+ async register(ignoreCache = false) {
3842
+ const cachedKey = ignoreCache ? null : this.loadCachedKey();
3680
3843
  if (cachedKey) {
3681
3844
  this.derivedKey = cachedKey;
3682
3845
  return;
@@ -3697,7 +3860,7 @@ var AirChatRestClient = class _AirChatRestClient {
3697
3860
  const res = await fetch(url, {
3698
3861
  method: "POST",
3699
3862
  headers: { "Content-Type": "application/json" },
3700
- body: JSON.stringify({ ...payload, signature }),
3863
+ body: JSON.stringify({ ...payload, signature, ...this.card ? { card: this.card } : {} }),
3701
3864
  signal: AbortSignal.timeout(15e3)
3702
3865
  });
3703
3866
  if (res.status === 409) {
@@ -3706,7 +3869,7 @@ var AirChatRestClient = class _AirChatRestClient {
3706
3869
  }
3707
3870
  if (!res.ok) {
3708
3871
  const body = await res.text().catch(() => "");
3709
- throw new Error(`Registration failed: HTTP ${res.status} \u2014 ${body}`);
3872
+ throw new AirChatHttpError(`Registration failed: HTTP ${res.status} \u2014 ${body}`, res.status, res.headers);
3710
3873
  }
3711
3874
  this.saveCachedKey(derivedKey);
3712
3875
  this.derivedKey = derivedKey;
@@ -3816,6 +3979,82 @@ async function post(client, channelName, content, parentMessageId) {
3816
3979
  }
3817
3980
  }
3818
3981
 
3982
+ // ../cli/dist/commands/dm.js
3983
+ async function dm(client, targetAgent, content) {
3984
+ const target = targetAgent.replace(/^@/, "");
3985
+ if (!target) {
3986
+ console.error("Usage: airchat dm <agent> <message>");
3987
+ process.exitCode = 1;
3988
+ return;
3989
+ }
3990
+ if (!content.trim()) {
3991
+ console.error("Refusing to send an empty message.");
3992
+ process.exitCode = 1;
3993
+ return;
3994
+ }
3995
+ try {
3996
+ await client.sendDirectMessage(target, content);
3997
+ console.log(`
3998
+ \u2713 DM sent to @${target}
3999
+ `);
4000
+ console.log(" They will see it on their next prompt, via the mention hook.");
4001
+ console.log(" See who else is around with: airchat agents\n");
4002
+ } catch (e) {
4003
+ const message = e instanceof Error ? e.message : String(e);
4004
+ console.error(`
4005
+ \u2717 Could not DM @${target}: ${message}`);
4006
+ console.error(" Check the name with: airchat agents\n");
4007
+ process.exitCode = 1;
4008
+ }
4009
+ }
4010
+
4011
+ // ../cli/dist/commands/whoami.js
4012
+ import * as os2 from "node:os";
4013
+ import * as path2 from "node:path";
4014
+ import * as fs2 from "node:fs";
4015
+ function whoami() {
4016
+ const airchatDir = path2.join(os2.homedir(), ".airchat");
4017
+ let machineName = "";
4018
+ try {
4019
+ for (const line of fs2.readFileSync(path2.join(airchatDir, "config"), "utf-8").split("\n")) {
4020
+ const trimmed = line.trim();
4021
+ if (!trimmed || trimmed.startsWith("#"))
4022
+ continue;
4023
+ const eq = trimmed.indexOf("=");
4024
+ if (eq === -1)
4025
+ continue;
4026
+ if (trimmed.slice(0, eq).trim() === "MACHINE_NAME") {
4027
+ machineName = trimmed.slice(eq + 1).trim();
4028
+ }
4029
+ }
4030
+ } catch {
4031
+ console.error("\nNo ~/.airchat/config found \u2014 this machine is not set up. Run: npx airchat\n");
4032
+ process.exitCode = 1;
4033
+ return;
4034
+ }
4035
+ if (!machineName) {
4036
+ console.error("\nMACHINE_NAME is not set in ~/.airchat/config.\n");
4037
+ process.exitCode = 1;
4038
+ return;
4039
+ }
4040
+ const project = path2.basename(process.cwd());
4041
+ const agentName = `${machineName}-${project}`;
4042
+ const keyPath = path2.join(airchatDir, "agents", `${agentName}.key`);
4043
+ const registered = fs2.existsSync(keyPath);
4044
+ console.log("");
4045
+ console.log(` Agent name: ${agentName}`);
4046
+ console.log(` Machine: ${machineName}`);
4047
+ console.log(` Project dir: ${process.cwd()}`);
4048
+ console.log(` Registered: ${registered ? "yes" : "not yet \u2014 registers on first check"}`);
4049
+ console.log("");
4050
+ console.log(` Others reach you at: @${agentName}`);
4051
+ console.log(` They would send: airchat dm ${agentName} "..."`);
4052
+ console.log("");
4053
+ console.log(" The name comes from the directory you are running in, so the same");
4054
+ console.log(" machine runs a different agent per project.");
4055
+ console.log("");
4056
+ }
4057
+
3819
4058
  // ../cli/dist/commands/search.js
3820
4059
  async function search(client, queryText, channelName) {
3821
4060
  const data = await client.searchMessages(queryText, channelName);
@@ -3872,6 +4111,195 @@ async function channels(client, type) {
3872
4111
  console.log("");
3873
4112
  }
3874
4113
 
4114
+ // ../cli/dist/commands/agents.js
4115
+ import * as readline from "node:readline";
4116
+ import * as readlinePromises from "node:readline/promises";
4117
+ import { stdin, stdout } from "node:process";
4118
+ var PAGE = 5;
4119
+ async function agents(client, capability, activeWithin, opts = {}) {
4120
+ const data = await client.listAgents(capability, activeWithin);
4121
+ const list = data.agents ?? [];
4122
+ const filter = [
4123
+ capability ? ` with capability "${capability}"` : "",
4124
+ activeWithin ? ` seen within ${activeWithin}` : ""
4125
+ ].join("");
4126
+ if (list.length === 0) {
4127
+ console.log(`
4128
+ \u{1F916} No agents${filter}.
4129
+ `);
4130
+ if (activeWithin)
4131
+ console.log(" Try a wider window: airchat agents -w 7d\n");
4132
+ return;
4133
+ }
4134
+ const canPick = opts.interactive !== false && stdout.isTTY && stdin.isTTY;
4135
+ if (!canPick) {
4136
+ console.log(`
4137
+ \u{1F916} Agents${filter} (${list.length})
4138
+ `);
4139
+ for (const a of list)
4140
+ console.log(describe(a));
4141
+ console.log("");
4142
+ return;
4143
+ }
4144
+ await pick(client, list, filter);
4145
+ }
4146
+ function ageOf(a) {
4147
+ if (!a.last_seen_at)
4148
+ return "never seen";
4149
+ const ms = Date.now() - new Date(a.last_seen_at).getTime();
4150
+ const mins = Math.floor(ms / 6e4);
4151
+ if (mins < 1)
4152
+ return "just now";
4153
+ if (mins < 60)
4154
+ return `${mins}m ago`;
4155
+ const hours = Math.floor(mins / 60);
4156
+ if (hours < 24)
4157
+ return `${hours}h ago`;
4158
+ return `${Math.floor(hours / 24)}d ago`;
4159
+ }
4160
+ function cardLine(a) {
4161
+ const parts = [];
4162
+ if (a.machine)
4163
+ parts.push(a.machine);
4164
+ if (a.card?.harness)
4165
+ parts.push(a.card.harness);
4166
+ if (a.card?.model)
4167
+ parts.push(a.card.model);
4168
+ if (a.card?.capabilities?.length)
4169
+ parts.push(a.card.capabilities.join(", "));
4170
+ return parts.join(" \xB7 ");
4171
+ }
4172
+ function describe(a) {
4173
+ const card = cardLine(a);
4174
+ return ` ${a.name.padEnd(30)} ${ageOf(a).padEnd(10)}${card}`;
4175
+ }
4176
+ async function pick(client, list, filter) {
4177
+ let cursor = 0;
4178
+ let offset = 0;
4179
+ let lastLines = 0;
4180
+ const render = () => {
4181
+ if (lastLines > 0)
4182
+ stdout.write(`\x1B[${lastLines}A\x1B[0J`);
4183
+ const page = list.slice(offset, offset + PAGE);
4184
+ const out = [];
4185
+ out.push(`\u{1F916} Agents${filter} \u2014 ${offset + 1}\u2013${offset + page.length} of ${list.length}`);
4186
+ out.push("");
4187
+ page.forEach((a, i) => {
4188
+ const selected = offset + i === cursor;
4189
+ const card = cardLine(a);
4190
+ 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`);
4191
+ });
4192
+ out.push("");
4193
+ out.push("\x1B[2m \u2191/\u2193 move \xB7 enter message \xB7 n/p page \xB7 q quit\x1B[0m");
4194
+ stdout.write(out.join("\n") + "\n");
4195
+ lastLines = out.length;
4196
+ };
4197
+ readline.emitKeypressEvents(stdin);
4198
+ if (stdin.isTTY)
4199
+ stdin.setRawMode(true);
4200
+ render();
4201
+ const chosen = await new Promise((resolve) => {
4202
+ const onKey = (_s, key) => {
4203
+ if (!key)
4204
+ return;
4205
+ const done = (value) => {
4206
+ stdin.off("keypress", onKey);
4207
+ if (stdin.isTTY)
4208
+ stdin.setRawMode(false);
4209
+ resolve(value);
4210
+ };
4211
+ if (key.name === "q" || key.ctrl && key.name === "c")
4212
+ return done(null);
4213
+ if (key.name === "return")
4214
+ return done(list[cursor]);
4215
+ if (key.name === "down")
4216
+ cursor = Math.min(cursor + 1, list.length - 1);
4217
+ else if (key.name === "up")
4218
+ cursor = Math.max(cursor - 1, 0);
4219
+ else if (key.name === "n")
4220
+ cursor = Math.min(offset + PAGE, list.length - 1);
4221
+ else if (key.name === "p")
4222
+ cursor = Math.max(offset - PAGE, 0);
4223
+ else
4224
+ return;
4225
+ if (cursor < offset)
4226
+ offset = Math.max(0, cursor - cursor % PAGE);
4227
+ if (cursor >= offset + PAGE)
4228
+ offset = cursor - cursor % PAGE;
4229
+ render();
4230
+ };
4231
+ stdin.on("keypress", onKey);
4232
+ });
4233
+ if (!chosen) {
4234
+ console.log("");
4235
+ return;
4236
+ }
4237
+ const rl = readlinePromises.createInterface({ input: stdin, output: stdout });
4238
+ const message = (await rl.question(`
4239
+ Message to @${chosen.name}: `)).trim();
4240
+ rl.close();
4241
+ if (!message) {
4242
+ console.log("\nNothing sent.\n");
4243
+ return;
4244
+ }
4245
+ try {
4246
+ await client.sendDirectMessage(chosen.name, message);
4247
+ console.log(`
4248
+ \u2713 Sent to @${chosen.name}`);
4249
+ console.log(" They see it on their next prompt, via the mention hook.\n");
4250
+ } catch (e) {
4251
+ console.error(`
4252
+ \u2717 Not sent: ${e instanceof Error ? e.message : String(e)}
4253
+ `);
4254
+ process.exitCode = 1;
4255
+ }
4256
+ }
4257
+
4258
+ // ../cli/dist/commands/tasks.js
4259
+ function printTask(task) {
4260
+ const tags = task.capability_tags.length ? ` [${task.capability_tags.join(", ")}]` : "";
4261
+ console.log(` ${task.id.slice(0, 8)} ${task.status.padEnd(9)} ${task.title}${tags}`);
4262
+ if (task.result && task.status === "done") {
4263
+ const firstLine = task.result.split("\n")[0];
4264
+ console.log(` \u2192 ${firstLine.slice(0, 100)}`);
4265
+ }
4266
+ }
4267
+ async function tasksList(client, opts) {
4268
+ const hasFilter = opts.status || opts.capability || opts.mine || opts.channel;
4269
+ if (!hasFilter) {
4270
+ const data2 = await client.listTasks({ forMe: true });
4271
+ console.log(`
4272
+ \u{1F4CB} Open tasks matching your card (${data2.open_matching.length})
4273
+ `);
4274
+ data2.open_matching.forEach(printTask);
4275
+ console.log(`
4276
+ \u{1F4CC} Your claimed tasks (${data2.mine_claimed.length})
4277
+ `);
4278
+ data2.mine_claimed.forEach(printTask);
4279
+ console.log("");
4280
+ return;
4281
+ }
4282
+ const data = await client.listTasks(opts);
4283
+ console.log(`
4284
+ \u{1F4CB} Tasks (${data.tasks.length})
4285
+ `);
4286
+ data.tasks.forEach(printTask);
4287
+ console.log("");
4288
+ }
4289
+ async function tasksPost(client, channel, title, opts) {
4290
+ const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
4291
+ const data = await client.postTask(channel, title, opts.body, tags);
4292
+ console.log(`
4293
+ \u2713 Task posted to #${channel}: ${data.task.id}
4294
+ `);
4295
+ }
4296
+ async function tasksUpdate(client, id, action, result) {
4297
+ const data = await client.updateTask(id, action, result);
4298
+ console.log(`
4299
+ \u2713 Task ${data.task.id.slice(0, 8)} \u2192 ${data.task.status}
4300
+ `);
4301
+ }
4302
+
3875
4303
  // ../cli/dist/commands/gossip.js
3876
4304
  async function gossipEnable(client) {
3877
4305
  try {
@@ -3990,17 +4418,17 @@ async function peerList(client) {
3990
4418
  }
3991
4419
 
3992
4420
  // ../cli/dist/commands/doctor.js
3993
- import { readFileSync, existsSync } from "fs";
3994
- import { join } from "path";
3995
- import { homedir } from "os";
4421
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
4422
+ import { join as join2 } from "path";
4423
+ import { homedir as homedir2 } from "os";
3996
4424
  async function doctor() {
3997
- const configDir = join(homedir(), ".airchat");
4425
+ const configDir = join2(homedir2(), ".airchat");
3998
4426
  const checks = [];
3999
4427
  let machineName;
4000
4428
  let webUrl;
4001
4429
  let privateKey;
4002
4430
  console.log("\n\u{1FA7A} AirChat Doctor\n");
4003
- if (existsSync(configDir)) {
4431
+ if (existsSync2(configDir)) {
4004
4432
  checks.push({ name: "Config directory", status: "pass", message: configDir });
4005
4433
  } else {
4006
4434
  checks.push({ name: "Config directory", status: "fail", message: `Missing ${configDir}` });
@@ -4008,10 +4436,10 @@ async function doctor() {
4008
4436
  printFix(['Run "npx airchat" to set up AirChat.']);
4009
4437
  return;
4010
4438
  }
4011
- const configPath = join(configDir, "config");
4012
- if (existsSync(configPath)) {
4439
+ const configPath = join2(configDir, "config");
4440
+ if (existsSync2(configPath)) {
4013
4441
  try {
4014
- const lines = readFileSync(configPath, "utf-8").split("\n");
4442
+ const lines = readFileSync2(configPath, "utf-8").split("\n");
4015
4443
  for (const line of lines) {
4016
4444
  const trimmed = line.trim();
4017
4445
  if (!trimmed || trimmed.startsWith("#"))
@@ -4044,10 +4472,10 @@ async function doctor() {
4044
4472
  webUrl = DEFAULT_AIRCHAT_URL;
4045
4473
  checks.push({ name: "AIRCHAT_WEB_URL", status: "warn", message: `Not set \u2014 using default: ${DEFAULT_AIRCHAT_URL}` });
4046
4474
  }
4047
- const keyPath = join(configDir, "machine.key");
4048
- if (existsSync(keyPath)) {
4475
+ const keyPath = join2(configDir, "machine.key");
4476
+ if (existsSync2(keyPath)) {
4049
4477
  try {
4050
- privateKey = readFileSync(keyPath, "utf-8").trim();
4478
+ privateKey = readFileSync2(keyPath, "utf-8").trim();
4051
4479
  if (privateKey.length > 0) {
4052
4480
  checks.push({ name: "Machine key", status: "pass", message: `${keyPath} (${privateKey.length} chars)` });
4053
4481
  } else {
@@ -4086,7 +4514,7 @@ async function doctor() {
4086
4514
  printChecks(checks);
4087
4515
  const fixes = [];
4088
4516
  const hasFail = checks.some((c) => c.status === "fail");
4089
- if (!existsSync(configPath) || !machineName || !existsSync(keyPath)) {
4517
+ if (!existsSync2(configPath) || !machineName || !existsSync2(keyPath)) {
4090
4518
  fixes.push('Run "npx airchat" to set up machine credentials.');
4091
4519
  }
4092
4520
  if (checks.find((c) => c.name === "Server connectivity" && c.status === "fail")) {
@@ -4116,7 +4544,7 @@ function printFix(fixes) {
4116
4544
  }
4117
4545
 
4118
4546
  // ../cli/dist/commands/notes.js
4119
- import { readFileSync as readFileSync2 } from "node:fs";
4547
+ import { readFileSync as readFileSync3 } from "node:fs";
4120
4548
  function parseScope(scope) {
4121
4549
  if (scope === "global")
4122
4550
  return null;
@@ -4130,10 +4558,10 @@ function resolveBody(opts) {
4130
4558
  if (opts.body !== void 0)
4131
4559
  return opts.body;
4132
4560
  if (opts.bodyFile)
4133
- return readFileSync2(opts.bodyFile, "utf8");
4561
+ return readFileSync3(opts.bodyFile, "utf8");
4134
4562
  if (!process.stdin.isTTY) {
4135
4563
  try {
4136
- return readFileSync2(0, "utf8");
4564
+ return readFileSync3(0, "utf8");
4137
4565
  } catch {
4138
4566
  }
4139
4567
  }
@@ -4244,9 +4672,18 @@ async function run(argv = process.argv) {
4244
4672
  program2.command("check").description("Show unread counts and latest message per channel").action(() => check(client));
4245
4673
  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
4674
  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));
4675
+ program2.command("whoami").description("Show this agent's AirChat name \u2014 what others use to message it").action(() => whoami());
4676
+ program2.command("dm <agent> <message...>").description("Send a direct message to another agent (notifies them)").action((agent, messageParts) => dm(client, agent, messageParts.join(" ")));
4247
4677
  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
4678
  program2.command("status").description("Show channel memberships, roles, and unread counts").action(() => status(client));
4249
4679
  program2.command("channels").description("List all available channels").option("-t, --type <type>", "Filter by channel type").action((opts) => channels(client, opts.type));
4680
+ 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 }));
4681
+ const tasks = program2.command("tasks").description("Capability-tagged task queue");
4682
+ 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));
4683
+ 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));
4684
+ tasks.command("claim <id>").description("Claim an open task (atomic \u2014 one winner)").action((id) => tasksUpdate(client, id, "claim"));
4685
+ 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));
4686
+ tasks.command("cancel <id>").description("Cancel a task you created").action((id) => tasksUpdate(client, id, "cancel"));
4250
4687
  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
4688
  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
4689
  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 +4700,195 @@ async function run(argv = process.argv) {
4263
4700
  await program2.parseAsync(argv);
4264
4701
  }
4265
4702
 
4703
+ // src/harnesses.ts
4704
+ import * as fs3 from "node:fs";
4705
+ import * as path3 from "node:path";
4706
+ import { execSync, execFileSync } from "node:child_process";
4707
+ function binaryExists(bin) {
4708
+ try {
4709
+ execSync(process.platform === "win32" ? `where ${bin}` : `command -v ${bin}`, { stdio: "pipe" });
4710
+ return true;
4711
+ } catch {
4712
+ return false;
4713
+ }
4714
+ }
4715
+ function fwd(p) {
4716
+ return p.replace(/\\/g, "/");
4717
+ }
4718
+ function launchParts(launch) {
4719
+ return {
4720
+ command: fwd(launch.nodePath),
4721
+ args: [fwd(launch.tsxPath), fwd(launch.serverPath)]
4722
+ };
4723
+ }
4724
+ function readIfExists(filePath) {
4725
+ try {
4726
+ return fs3.readFileSync(filePath, "utf-8");
4727
+ } catch (e) {
4728
+ if (e?.code === "ENOENT") return null;
4729
+ throw e;
4730
+ }
4731
+ }
4732
+ function mergeJsonFile(filePath, mutate, manualSnippet) {
4733
+ let config = {};
4734
+ const raw = readIfExists(filePath);
4735
+ if (raw !== null && raw.trim()) {
4736
+ try {
4737
+ config = JSON.parse(raw);
4738
+ } catch {
4739
+ return {
4740
+ ok: false,
4741
+ message: `${filePath} exists but could not be parsed (comments or trailing commas?). Left untouched \u2014 add the entry manually:`,
4742
+ manualSnippet
4743
+ };
4744
+ }
4745
+ }
4746
+ mutate(config);
4747
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
4748
+ fs3.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
4749
+ return { ok: true, message: filePath };
4750
+ }
4751
+ function tryCliRegister(bin, argv, manualSnippet) {
4752
+ try {
4753
+ execFileSync(bin, argv, { stdio: "pipe" });
4754
+ return { ok: true, message: "Registered via CLI" };
4755
+ } catch {
4756
+ return {
4757
+ ok: false,
4758
+ message: `Could not run "${[bin, ...argv.slice(0, 2)].join(" ")} \u2026". Add manually:`,
4759
+ manualSnippet
4760
+ };
4761
+ }
4762
+ }
4763
+ var HARNESSES = [
4764
+ {
4765
+ key: "claude",
4766
+ label: "Claude Code",
4767
+ instructionsPath: (home) => path3.join(home, ".claude", "CLAUDE.md"),
4768
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".claude")) || hasBinary("claude"),
4769
+ registerMcp(launch) {
4770
+ const { command, args } = launchParts(launch);
4771
+ const argv = ["mcp", "add", "airchat", "-s", "user", "--", command, ...args];
4772
+ const display = `claude mcp add airchat -s user -- "${command}" ${args.map((a) => `"${a}"`).join(" ")}`;
4773
+ return tryCliRegister("claude", argv, display);
4774
+ }
4775
+ },
4776
+ {
4777
+ key: "codex",
4778
+ label: "Codex CLI",
4779
+ instructionsPath: (home) => path3.join(home, ".codex", "AGENTS.md"),
4780
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".codex")) || hasBinary("codex"),
4781
+ registerMcp(launch) {
4782
+ const { command, args } = launchParts(launch);
4783
+ const argv = ["mcp", "add", "airchat", "--", command, ...args];
4784
+ const toml = [
4785
+ "# ~/.codex/config.toml",
4786
+ "[mcp_servers.airchat]",
4787
+ `command = "${command}"`,
4788
+ `args = [${args.map((a) => `"${a}"`).join(", ")}]`
4789
+ ].join("\n");
4790
+ return tryCliRegister("codex", argv, toml);
4791
+ }
4792
+ },
4793
+ {
4794
+ key: "antigravity",
4795
+ label: "Antigravity CLI (formerly Gemini CLI)",
4796
+ // Both GEMINI.md and AGENTS.md are honored; GEMINI.md is the long-standing
4797
+ // global location so existing setups keep working.
4798
+ instructionsPath: (home) => path3.join(home, ".gemini", "GEMINI.md"),
4799
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".gemini", "config")) || hasBinary("agy") || hasBinary("gemini"),
4800
+ registerMcp(launch, home) {
4801
+ const { command, args } = launchParts(launch);
4802
+ const filePath = path3.join(home, ".gemini", "config", "mcp_config.json");
4803
+ const snippet = JSON.stringify({ mcpServers: { airchat: { command, args } } }, null, 2);
4804
+ return mergeJsonFile(
4805
+ filePath,
4806
+ (config) => {
4807
+ config.mcpServers = config.mcpServers ?? {};
4808
+ config.mcpServers.airchat = { command, args };
4809
+ },
4810
+ `// ${filePath}
4811
+ ${snippet}`
4812
+ );
4813
+ }
4814
+ },
4815
+ {
4816
+ key: "cursor",
4817
+ label: "Cursor",
4818
+ // Cursor's global rules live in app settings, not a file; project-level
4819
+ // AGENTS.md is out of scope for a global installer.
4820
+ instructionsPath: null,
4821
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".cursor")) || hasBinary("cursor-agent"),
4822
+ registerMcp(launch, home) {
4823
+ const { command, args } = launchParts(launch);
4824
+ const filePath = path3.join(home, ".cursor", "mcp.json");
4825
+ const entry = { type: "stdio", command, args };
4826
+ const snippet = JSON.stringify({ mcpServers: { airchat: entry } }, null, 2);
4827
+ return mergeJsonFile(
4828
+ filePath,
4829
+ (config) => {
4830
+ config.mcpServers = config.mcpServers ?? {};
4831
+ config.mcpServers.airchat = entry;
4832
+ },
4833
+ `// ${filePath}
4834
+ ${snippet}`
4835
+ );
4836
+ }
4837
+ },
4838
+ {
4839
+ key: "opencode",
4840
+ label: "OpenCode",
4841
+ instructionsPath: (home) => path3.join(home, ".config", "opencode", "AGENTS.md"),
4842
+ detect: (home, hasBinary) => fs3.existsSync(path3.join(home, ".config", "opencode")) || hasBinary("opencode"),
4843
+ registerMcp(launch, home) {
4844
+ const { command, args } = launchParts(launch);
4845
+ const filePath = path3.join(home, ".config", "opencode", "opencode.json");
4846
+ const entry = { type: "local", command: [command, ...args], enabled: true };
4847
+ const snippet = JSON.stringify({ mcp: { airchat: entry } }, null, 2);
4848
+ return mergeJsonFile(
4849
+ filePath,
4850
+ (config) => {
4851
+ config.mcp = config.mcp ?? {};
4852
+ config.mcp.airchat = entry;
4853
+ },
4854
+ `// ${filePath}
4855
+ ${snippet}`
4856
+ );
4857
+ }
4858
+ }
4859
+ ];
4860
+ function detectHarnesses(home, hasBinary = binaryExists) {
4861
+ return HARNESSES.filter((h) => h.detect(home, hasBinary));
4862
+ }
4863
+ function genericSnippet(launch) {
4864
+ const { command, args } = launchParts(launch);
4865
+ return [
4866
+ "AirChat is a standard stdio MCP server. For any other MCP client, register:",
4867
+ ` command: ${command}`,
4868
+ ` args: ${args.join(" ")}`,
4869
+ " env: none (reads ~/.airchat/config and ~/.airchat/machine.key)"
4870
+ ].join("\n");
4871
+ }
4872
+ function installInstructions(filePath, content) {
4873
+ try {
4874
+ const existing = readIfExists(filePath);
4875
+ if (existing !== null) {
4876
+ if (existing.includes("# AirChat")) {
4877
+ return { ok: true, message: `Already present in ${filePath}` };
4878
+ }
4879
+ fs3.appendFileSync(filePath, "\n" + content);
4880
+ } else {
4881
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
4882
+ fs3.writeFileSync(filePath, content);
4883
+ }
4884
+ return { ok: true, message: filePath };
4885
+ } catch (e) {
4886
+ return { ok: false, message: e.message };
4887
+ }
4888
+ }
4889
+
4266
4890
  // src/index.ts
4891
+ var AIRCHAT_VERSION = true ? "1.0.1" : "dev";
4267
4892
  var DEFAULT_AIRCHAT_URL2 = "https://supernode-web-production.up.railway.app";
4268
4893
  var GREEN = "\x1B[32m";
4269
4894
  var YELLOW = "\x1B[33m";
@@ -4287,10 +4912,10 @@ function bold(msg) {
4287
4912
  return `${BOLD}${msg}${RESET}`;
4288
4913
  }
4289
4914
  function readExistingConfig() {
4290
- const configPath = path2.join(os2.homedir(), ".airchat", "config");
4915
+ const configPath = path4.join(os3.homedir(), ".airchat", "config");
4291
4916
  const config = {};
4292
4917
  try {
4293
- const lines = fs2.readFileSync(configPath, "utf-8").split("\n");
4918
+ const lines = fs4.readFileSync(configPath, "utf-8").split("\n");
4294
4919
  for (const line of lines) {
4295
4920
  const trimmed = line.trim();
4296
4921
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -4308,10 +4933,10 @@ function detectNodePath() {
4308
4933
  function detectRepoDir() {
4309
4934
  let dir = process.cwd();
4310
4935
  for (let i = 0; i < 10; i++) {
4311
- if (fs2.existsSync(path2.join(dir, "packages", "mcp-server", "src", "index.ts"))) {
4936
+ if (fs4.existsSync(path4.join(dir, "packages", "mcp-server", "src", "index.ts"))) {
4312
4937
  return dir;
4313
4938
  }
4314
- const parent = path2.dirname(dir);
4939
+ const parent = path4.dirname(dir);
4315
4940
  if (parent === dir) break;
4316
4941
  dir = parent;
4317
4942
  }
@@ -4382,7 +5007,7 @@ async function collectConfig(rl, reconfigure) {
4382
5007
  console.log(` ${dim("A PostgreSQL container will be started alongside the dashboard.")}`);
4383
5008
  console.log("");
4384
5009
  }
4385
- const defaultMachine = existing.MACHINE_NAME || os2.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "laptop";
5010
+ const defaultMachine = existing.MACHINE_NAME || os3.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "laptop";
4386
5011
  let machineName = "";
4387
5012
  while (!machineName) {
4388
5013
  machineName = await ask(rl, "Machine name (e.g. laptop, server, nas)", defaultMachine);
@@ -4392,6 +5017,21 @@ async function collectConfig(rl, reconfigure) {
4392
5017
  }
4393
5018
  }
4394
5019
  console.log("");
5020
+ const detected = detectHarnesses(os3.homedir());
5021
+ const detectedKeys = new Set(detected.map((h) => h.key));
5022
+ console.log(" Which AI harnesses should connect to AirChat?");
5023
+ HARNESSES.forEach((h, i) => {
5024
+ const mark = detectedKeys.has(h.key) ? ` ${GREEN}(detected)${RESET}` : "";
5025
+ console.log(` ${BOLD}${i + 1}${RESET}) ${h.label}${mark}`);
5026
+ });
5027
+ console.log(` ${BOLD}${HARNESSES.length + 1}${RESET}) Other MCP client ${dim("(prints a generic snippet)")}`);
5028
+ const fallbackSel = detected.length ? detected.map((h) => h.key) : ["claude"];
5029
+ const selRaw = await rl.question(
5030
+ ` Select, comma-separated ${dim(`(Enter = ${fallbackSel.join(", ")})`)}: `
5031
+ );
5032
+ 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);
5033
+ if (harnesses.length === 0) harnesses = fallbackSel;
5034
+ console.log("");
4395
5035
  const deployDashboard = await askYesNo(rl, "Deploy web dashboard?", true);
4396
5036
  let dashboardPort = 3003;
4397
5037
  if (deployDashboard) {
@@ -4428,7 +5068,7 @@ async function collectConfig(rl, reconfigure) {
4428
5068
  if (!airchatDir) {
4429
5069
  const shouldClone = await askYesNo(rl, "Clone the airchat repo?", true);
4430
5070
  if (shouldClone) {
4431
- const cloneDir = await ask(rl, "Clone to", path2.join(os2.homedir(), "projects", "airchat"));
5071
+ const cloneDir = await ask(rl, "Clone to", path4.join(os3.homedir(), "projects", "airchat"));
4432
5072
  airchatDir = cloneDir;
4433
5073
  } else {
4434
5074
  airchatDir = await ask(rl, "Path to existing airchat repo");
@@ -4455,6 +5095,7 @@ async function collectConfig(rl, reconfigure) {
4455
5095
  supabaseServiceRoleKey,
4456
5096
  databaseUrl,
4457
5097
  machineName,
5098
+ harnesses,
4458
5099
  dashboardPort,
4459
5100
  deployDashboard,
4460
5101
  airchatDir,
@@ -4468,14 +5109,16 @@ async function collectConfig(rl, reconfigure) {
4468
5109
  }
4469
5110
  async function cloneAndInstall(config) {
4470
5111
  const name = "Clone & install";
4471
- if (fs2.existsSync(path2.join(config.airchatDir, "packages", "mcp-server"))) {
5112
+ if (fs4.existsSync(path4.join(config.airchatDir, "packages", "mcp-server"))) {
4472
5113
  return { name, ok: true, message: "Repo already exists" };
4473
5114
  }
4474
5115
  try {
4475
5116
  console.log(` Cloning airchat...`);
4476
- execSync(`git clone https://github.com/prone/airchat.git "${config.airchatDir}"`, { stdio: "pipe" });
5117
+ execFileSync2("git", ["clone", "https://github.com/prone/airchat.git", config.airchatDir], {
5118
+ stdio: "pipe"
5119
+ });
4477
5120
  console.log(` Installing dependencies...`);
4478
- execSync("npm install", { cwd: config.airchatDir, stdio: "pipe" });
5121
+ execSync2("npm install", { cwd: config.airchatDir, stdio: "pipe" });
4479
5122
  return { name, ok: true, message: "Cloned and installed" };
4480
5123
  } catch (e) {
4481
5124
  return { name, ok: false, message: `Failed: ${e.message}` };
@@ -4490,27 +5133,27 @@ async function runMigrations(config) {
4490
5133
  const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
4491
5134
  auth: { persistSession: false }
4492
5135
  });
4493
- const { data: agents } = await supabase.from("agents").select("id").limit(1);
4494
- if (agents !== null) {
5136
+ const { data: agents2 } = await supabase.from("agents").select("id").limit(1);
5137
+ if (agents2 !== null) {
4495
5138
  return { name, ok: true, message: "Tables already exist \u2014 skipping migrations" };
4496
5139
  }
4497
5140
  try {
4498
- execSync("which supabase", { stdio: "pipe" });
4499
- execSync(`supabase db push --db-url "${config.supabaseUrl}"`, {
5141
+ execSync2("which supabase", { stdio: "pipe" });
5142
+ execFileSync2("supabase", ["db", "push", "--db-url", config.supabaseUrl], {
4500
5143
  cwd: config.airchatDir,
4501
5144
  stdio: "pipe"
4502
5145
  });
4503
5146
  return { name, ok: true, message: "Migrations applied via Supabase CLI" };
4504
5147
  } catch {
4505
5148
  }
4506
- const migrationsDir = path2.join(config.airchatDir, "supabase", "migrations");
4507
- if (!fs2.existsSync(migrationsDir)) {
5149
+ const migrationsDir = path4.join(config.airchatDir, "supabase", "migrations");
5150
+ if (!fs4.existsSync(migrationsDir)) {
4508
5151
  return { name, ok: false, message: `Migrations directory not found at ${migrationsDir}` };
4509
5152
  }
4510
- const files = fs2.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
5153
+ const files = fs4.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
4511
5154
  let applied = 0;
4512
5155
  for (const file of files) {
4513
- const sql = fs2.readFileSync(path2.join(migrationsDir, file), "utf-8");
5156
+ const sql = fs4.readFileSync(path4.join(migrationsDir, file), "utf-8");
4514
5157
  const { error } = await supabase.rpc("", {}).then(
4515
5158
  () => ({ error: null }),
4516
5159
  () => ({ error: null })
@@ -4528,11 +5171,13 @@ async function runMigrations(config) {
4528
5171
  return { name, ok: true, message: "Migrations applied" };
4529
5172
  }
4530
5173
  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();
5174
+ const migrationsDir = path4.join(config.airchatDir, "supabase", "migrations");
5175
+ const files = fs4.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
4533
5176
  try {
4534
5177
  for (const file of files) {
4535
- execSync(`psql "${config.databaseUrl}" -f "${path2.join(migrationsDir, file)}"`, { stdio: "pipe" });
5178
+ execFileSync2("psql", [config.databaseUrl, "-f", path4.join(migrationsDir, file)], {
5179
+ stdio: "pipe"
5180
+ });
4536
5181
  }
4537
5182
  return { name, ok: true, message: `Applied ${files.length} migrations` };
4538
5183
  } catch (e) {
@@ -4564,12 +5209,12 @@ async function seedChannels(config) {
4564
5209
  }
4565
5210
  async function generateAndRegisterKeypair(config, reconfigure) {
4566
5211
  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();
5212
+ const configDir = path4.join(os3.homedir(), ".airchat");
5213
+ const privateKeyPath = path4.join(configDir, "machine.key");
5214
+ const publicKeyPath = path4.join(configDir, "machine.pub");
5215
+ if (!reconfigure && fs4.existsSync(privateKeyPath) && fs4.existsSync(publicKeyPath)) {
5216
+ const existingPub = fs4.readFileSync(publicKeyPath, "utf-8").trim();
5217
+ const existingPriv = fs4.readFileSync(privateKeyPath, "utf-8").trim();
4573
5218
  if (existingPub.length === 64 && existingPriv.length === 64) {
4574
5219
  config.keypair = { publicKey: existingPub, privateKey: existingPriv };
4575
5220
  return { name, ok: true, message: "Existing keypair found \u2014 reusing" };
@@ -4577,11 +5222,11 @@ async function generateAndRegisterKeypair(config, reconfigure) {
4577
5222
  }
4578
5223
  const keypair = generateKeypair();
4579
5224
  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);
5225
+ fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
5226
+ fs4.writeFileSync(privateKeyPath, keypair.privateKey + "\n", { mode: 384 });
5227
+ fs4.writeFileSync(publicKeyPath, keypair.publicKey + "\n", { mode: 420 });
5228
+ fs4.chmodSync(configDir, 448);
5229
+ fs4.chmodSync(privateKeyPath, 384);
4585
5230
  if (config.dbProvider === "supabase" && config.supabaseUrl && config.supabaseServiceRoleKey) {
4586
5231
  const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
4587
5232
  auth: { persistSession: false }
@@ -4630,8 +5275,8 @@ async function generateAndRegisterKeypair(config, reconfigure) {
4630
5275
  function writeAirchatConfig(config) {
4631
5276
  const name = "Write ~/.airchat/config";
4632
5277
  try {
4633
- const configDir = path2.join(os2.homedir(), ".airchat");
4634
- fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
5278
+ const configDir = path4.join(os3.homedir(), ".airchat");
5279
+ fs4.mkdirSync(configDir, { recursive: true, mode: 448 });
4635
5280
  const lines = [
4636
5281
  `MACHINE_NAME=${config.machineName}`
4637
5282
  ];
@@ -4639,11 +5284,11 @@ function writeAirchatConfig(config) {
4639
5284
  if (config.slackBotToken) lines.push(`SLACK_BOT_TOKEN=${config.slackBotToken}`);
4640
5285
  if (config.slackAppToken) lines.push(`SLACK_APP_TOKEN=${config.slackAppToken}`);
4641
5286
  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);
5287
+ fs4.writeFileSync(path4.join(configDir, "config"), lines.join("\n") + "\n", { mode: 384 });
5288
+ const agentsDir = path4.join(configDir, "agents");
5289
+ fs4.mkdirSync(agentsDir, { recursive: true, mode: 448 });
5290
+ fs4.chmodSync(agentsDir, 448);
5291
+ fs4.chmodSync(configDir, 448);
4647
5292
  return { name, ok: true, message: configDir };
4648
5293
  } catch (e) {
4649
5294
  return { name, ok: false, message: e.message };
@@ -4655,7 +5300,7 @@ function writeWebEnv(config) {
4655
5300
  return { name, ok: true, message: "Skipped" };
4656
5301
  }
4657
5302
  try {
4658
- const envPath = path2.join(config.airchatDir, "apps", "web", ".env.local");
5303
+ const envPath = path4.join(config.airchatDir, "apps", "web", ".env.local");
4659
5304
  const lines = [];
4660
5305
  if (config.supabaseUrl) {
4661
5306
  lines.push(`NEXT_PUBLIC_SUPABASE_URL=${config.supabaseUrl}`);
@@ -4666,76 +5311,74 @@ function writeWebEnv(config) {
4666
5311
  if (config.databaseUrl) {
4667
5312
  lines.push(`DATABASE_URL=${config.databaseUrl}`);
4668
5313
  }
4669
- fs2.writeFileSync(envPath, lines.join("\n") + "\n");
5314
+ fs4.writeFileSync(envPath, lines.join("\n") + "\n", { mode: 384 });
5315
+ fs4.chmodSync(envPath, 384);
4670
5316
  return { name, ok: true, message: envPath };
4671
5317
  } catch (e) {
4672
5318
  return { name, ok: false, message: e.message };
4673
5319
  }
4674
5320
  }
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
- };
5321
+ function buildMcpLaunch(config) {
5322
+ const tsxPath = path4.join(config.airchatDir, "node_modules", ".bin", "tsx");
5323
+ const serverPath = path4.join(config.airchatDir, "packages", "mcp-server", "src", "index.ts");
5324
+ if (!fs4.existsSync(serverPath)) return null;
5325
+ return { nodePath: config.nodePath, tsxPath, serverPath };
5326
+ }
5327
+ function registerMcpServers(config) {
5328
+ const launch = buildMcpLaunch(config);
5329
+ if (!launch) {
5330
+ return [{ name: "Register MCP server", ok: false, message: `MCP server not found under ${config.airchatDir}` }];
5331
+ }
5332
+ const results = [];
5333
+ for (const key of config.harnesses) {
5334
+ if (key === "other") continue;
5335
+ const harness = HARNESSES.find((h) => h.key === key);
5336
+ if (!harness) continue;
5337
+ const res = harness.registerMcp(launch, os3.homedir());
5338
+ const message = res.manualSnippet ? `${res.message}
5339
+ ${res.manualSnippet.split("\n").map((l) => ` ${l}`).join("\n")}` : res.message;
5340
+ results.push({ name: `MCP \xB7 ${harness.label}`, ok: res.ok, message });
4693
5341
  }
5342
+ return results;
4694
5343
  }
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 };
5344
+ function installAgentInstructions(config) {
5345
+ const srcPath = path4.join(config.airchatDir, "setup", "agent-instructions.md");
5346
+ if (!fs4.existsSync(srcPath)) {
5347
+ return [{ name: "Install agent instructions", ok: false, message: "setup/agent-instructions.md not found" }];
5348
+ }
5349
+ const content = fs4.readFileSync(srcPath, "utf-8");
5350
+ const results = [];
5351
+ for (const key of config.harnesses) {
5352
+ const harness = HARNESSES.find((h) => h.key === key);
5353
+ if (!harness?.instructionsPath) continue;
5354
+ const res = installInstructions(harness.instructionsPath(os3.homedir()), content);
5355
+ results.push({ name: `Instructions \xB7 ${harness.label}`, ok: res.ok, message: res.message });
4717
5356
  }
5357
+ return results;
4718
5358
  }
4719
5359
  function installMentionHook(config) {
4720
- const name = "Install mention hook";
5360
+ const name = "Install work hook";
4721
5361
  try {
4722
- const settingsPath = path2.join(os2.homedir(), ".claude", "settings.json");
5362
+ const settingsPath = path4.join(os3.homedir(), ".claude", "settings.json");
4723
5363
  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
- }
5364
+ try {
5365
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
5366
+ } catch (e) {
5367
+ if (e?.code !== "ENOENT") throw e;
4736
5368
  }
5369
+ const hookCommand = `${config.nodePath} ${path4.join(config.airchatDir, "scripts", "check-work.mjs")}`.replace(/\\/g, "/");
4737
5370
  if (!settings.hooks) settings.hooks = {};
4738
5371
  if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
5372
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
5373
+ (entry) => !entry.hooks?.some((h) => h.command?.includes("check-mentions"))
5374
+ );
5375
+ const alreadyHasHook = settings.hooks.UserPromptSubmit.some(
5376
+ (entry) => entry.hooks?.some((h) => h.command?.includes("check-work"))
5377
+ );
5378
+ if (alreadyHasHook) {
5379
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5380
+ return { name, ok: true, message: "Hook already configured" };
5381
+ }
4739
5382
  settings.hooks.UserPromptSubmit.push({
4740
5383
  matcher: "",
4741
5384
  hooks: [{
@@ -4743,8 +5386,8 @@ function installMentionHook(config) {
4743
5386
  command: hookCommand
4744
5387
  }]
4745
5388
  });
4746
- fs2.mkdirSync(path2.dirname(settingsPath), { recursive: true });
4747
- fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5389
+ fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5390
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4748
5391
  return { name, ok: true, message: "~/.claude/settings.json" };
4749
5392
  } catch (e) {
4750
5393
  return { name, ok: false, message: e.message };
@@ -4753,12 +5396,12 @@ function installMentionHook(config) {
4753
5396
  function copySlashCommands(config) {
4754
5397
  const name = "Copy slash commands";
4755
5398
  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"));
5399
+ const setupDir = path4.join(config.airchatDir, "setup");
5400
+ const commandsDir = path4.join(os3.homedir(), ".claude", "commands");
5401
+ fs4.mkdirSync(commandsDir, { recursive: true });
5402
+ const files = fs4.readdirSync(setupDir).filter((f) => f.startsWith("airchat-") && f.endsWith(".md"));
4760
5403
  for (const file of files) {
4761
- fs2.copyFileSync(path2.join(setupDir, file), path2.join(commandsDir, file));
5404
+ fs4.copyFileSync(path4.join(setupDir, file), path4.join(commandsDir, file));
4762
5405
  }
4763
5406
  return { name, ok: true, message: `${files.length} commands \u2192 ~/.claude/commands/` };
4764
5407
  } catch (e) {
@@ -4768,9 +5411,9 @@ function copySlashCommands(config) {
4768
5411
  async function main() {
4769
5412
  const reconfigure = process.argv.includes("--reconfigure");
4770
5413
  console.log("");
4771
- console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim("v0.3.0")}`);
5414
+ console.log(` ${GREEN}>${RESET} ${BOLD}AirChat${RESET} ${dim(`v${AIRCHAT_VERSION}`)}`);
4772
5415
  console.log(` ${dim("Your AI agents can talk to each other")}`);
4773
- const rl = readline.createInterface({ input, output });
5416
+ const rl = readline2.createInterface({ input, output });
4774
5417
  let config;
4775
5418
  try {
4776
5419
  config = await collectConfig(rl, reconfigure);
@@ -4780,6 +5423,7 @@ async function main() {
4780
5423
  console.log(` ${bold("Summary:")}`);
4781
5424
  console.log(` Database: ${config.dbProvider}`);
4782
5425
  console.log(` Machine: ${config.machineName}`);
5426
+ console.log(` Harnesses: ${config.harnesses.join(", ")}`);
4783
5427
  console.log(` Repo: ${config.airchatDir}`);
4784
5428
  console.log(` Node: ${config.nodePath}`);
4785
5429
  if (config.deployDashboard) {
@@ -4827,22 +5471,26 @@ async function main() {
4827
5471
  if (envResult.ok && config.deployDashboard) ok(`Dashboard env \u2192 ${envResult.message}`);
4828
5472
  else if (!config.deployDashboard) {
4829
5473
  } 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);
5474
+ for (const r of registerMcpServers(config)) {
5475
+ results.push(r);
5476
+ if (r.ok) ok(`${r.name} \u2192 ${r.message}`);
5477
+ else warn(`${r.name}: ${r.message}`);
5478
+ }
5479
+ for (const r of installAgentInstructions(config)) {
5480
+ results.push(r);
5481
+ if (r.ok) ok(`${r.name} \u2192 ${r.message}`);
5482
+ else fail(`${r.name}: ${r.message}`);
5483
+ }
5484
+ if (config.harnesses.includes("claude")) {
5485
+ const hookResult = installMentionHook(config);
5486
+ results.push(hookResult);
5487
+ if (hookResult.ok) ok(`Mention hook \u2192 ${hookResult.message}`);
5488
+ else fail(hookResult.message);
5489
+ const cmdResult = copySlashCommands(config);
5490
+ results.push(cmdResult);
5491
+ if (cmdResult.ok) ok(`Slash commands \u2192 ${cmdResult.message}`);
5492
+ else fail(cmdResult.message);
5493
+ }
4846
5494
  console.log("");
4847
5495
  const failures = results.filter((r) => !r.ok);
4848
5496
  if (failures.length === 0) {
@@ -4868,21 +5516,31 @@ async function main() {
4868
5516
  console.log(` ~/.airchat/machine.key ${dim("Ed25519 private key")}`);
4869
5517
  console.log(` ~/.airchat/machine.pub ${dim("Ed25519 public key")}`);
4870
5518
  console.log(` ~/.airchat/agents/ ${dim("Cached derived keys (auto-created)")}`);
5519
+ if (config.harnesses.includes("other")) {
5520
+ const launch = buildMcpLaunch(config);
5521
+ if (launch) {
5522
+ console.log("");
5523
+ console.log(genericSnippet(launch).split("\n").map((l) => ` ${l}`).join("\n"));
5524
+ }
5525
+ }
4871
5526
  console.log("");
4872
5527
  console.log(` ${dim("Next steps:")}`);
4873
5528
  let step = 1;
4874
- console.log(` ${step++}. Restart Claude Code`);
5529
+ const selectedLabels = config.harnesses.filter((k) => k !== "other").map((k) => HARNESSES.find((h) => h.key === k)?.label).filter(Boolean);
5530
+ console.log(` ${step++}. Restart your agent sessions${selectedLabels.length ? ` (${selectedLabels.join(", ")})` : ""}`);
4875
5531
  if (config.deployDashboard) {
4876
5532
  console.log(` ${step++}. Start the dashboard: cd ${config.airchatDir} && docker compose up -d --build`);
4877
5533
  }
4878
5534
  if (config.slackBotToken) {
4879
- console.log(` ${step++}. Start the Slack bridge: node ${path2.join(config.airchatDir, "packages", "slack-bridge", "dist", "index.js")}`);
5535
+ console.log(` ${step++}. Start the Slack bridge: node ${path4.join(config.airchatDir, "packages", "slack-bridge", "dist", "index.js")}`);
4880
5536
  console.log(` ${dim("Or: cd " + config.airchatDir + " && npx tsx packages/slack-bridge/src/index.ts")}`);
4881
5537
  }
4882
5538
  console.log(` ${dim("Run with --reconfigure to regenerate keypair and update settings.")}`);
4883
5539
  console.log("");
4884
5540
  }
4885
5541
  var CLI_COMMANDS = /* @__PURE__ */ new Set([
5542
+ // 'dm' ships from fix/mention-hook-delivery; listed here so the published
5543
+ // bin routes it to the CLI once that branch merges.
4886
5544
  "doctor",
4887
5545
  "check",
4888
5546
  "read",
@@ -4890,13 +5548,18 @@ var CLI_COMMANDS = /* @__PURE__ */ new Set([
4890
5548
  "search",
4891
5549
  "status",
4892
5550
  "channels",
5551
+ "agents",
5552
+ "tasks",
5553
+ "dm",
4893
5554
  "notes",
4894
5555
  "note",
4895
5556
  "write-note",
4896
5557
  "backlinks",
4897
5558
  "summarize",
4898
5559
  "gossip",
4899
- "peer"
5560
+ "peer",
5561
+ "dm",
5562
+ "whoami"
4900
5563
  ]);
4901
5564
  var sub = process.argv[2];
4902
5565
  if (sub && CLI_COMMANDS.has(sub)) {