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.
- package/dist/index.js +800 -165
- 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
|
-
|
|
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
|
|
1154
|
-
var
|
|
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 (
|
|
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 =
|
|
2154
|
-
if (
|
|
2155
|
-
if (sourceExt.includes(
|
|
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) =>
|
|
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 =
|
|
2173
|
+
resolvedScriptPath = fs5.realpathSync(this._scriptPath);
|
|
2170
2174
|
} catch {
|
|
2171
2175
|
resolvedScriptPath = this._scriptPath;
|
|
2172
2176
|
}
|
|
2173
|
-
executableDir =
|
|
2174
|
-
|
|
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 =
|
|
2185
|
+
const legacyName = path5.basename(
|
|
2182
2186
|
this._scriptPath,
|
|
2183
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
3056
|
-
if (
|
|
3057
|
-
this._executableDir =
|
|
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
|
|
3334
|
+
import * as readline2 from "node:readline/promises";
|
|
3331
3335
|
import { stdin as input, stdout as output } from "node:process";
|
|
3332
|
-
import * as
|
|
3333
|
-
import * as
|
|
3334
|
-
import * as
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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 =
|
|
4012
|
-
if (
|
|
4417
|
+
const configPath = join2(configDir, "config");
|
|
4418
|
+
if (existsSync2(configPath)) {
|
|
4013
4419
|
try {
|
|
4014
|
-
const lines =
|
|
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 =
|
|
4048
|
-
if (
|
|
4453
|
+
const keyPath = join2(configDir, "machine.key");
|
|
4454
|
+
if (existsSync2(keyPath)) {
|
|
4049
4455
|
try {
|
|
4050
|
-
privateKey =
|
|
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 (!
|
|
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
|
|
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
|
|
4539
|
+
return readFileSync3(opts.bodyFile, "utf8");
|
|
4134
4540
|
if (!process.stdin.isTTY) {
|
|
4135
4541
|
try {
|
|
4136
|
-
return
|
|
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 =
|
|
4892
|
+
const configPath = path4.join(os3.homedir(), ".airchat", "config");
|
|
4291
4893
|
const config = {};
|
|
4292
4894
|
try {
|
|
4293
|
-
const lines =
|
|
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 (
|
|
4913
|
+
if (fs4.existsSync(path4.join(dir, "packages", "mcp-server", "src", "index.ts"))) {
|
|
4312
4914
|
return dir;
|
|
4313
4915
|
}
|
|
4314
|
-
const parent =
|
|
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 ||
|
|
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",
|
|
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 (
|
|
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
|
-
|
|
5094
|
+
execSync2(`git clone https://github.com/prone/airchat.git "${config.airchatDir}"`, { stdio: "pipe" });
|
|
4477
5095
|
console.log(` Installing dependencies...`);
|
|
4478
|
-
|
|
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:
|
|
4494
|
-
if (
|
|
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
|
-
|
|
4499
|
-
|
|
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 =
|
|
4507
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4532
|
-
const files =
|
|
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
|
-
|
|
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 =
|
|
4568
|
-
const privateKeyPath =
|
|
4569
|
-
const publicKeyPath =
|
|
4570
|
-
if (!reconfigure &&
|
|
4571
|
-
const existingPub =
|
|
4572
|
-
const existingPriv =
|
|
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
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
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 =
|
|
4634
|
-
|
|
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
|
-
|
|
4643
|
-
const agentsDir =
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4676
|
-
const
|
|
4677
|
-
const
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
const
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
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
|
|
4696
|
-
const
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
const
|
|
4704
|
-
if (
|
|
4705
|
-
|
|
4706
|
-
|
|
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
|
|
5332
|
+
const name = "Install work hook";
|
|
4721
5333
|
try {
|
|
4722
|
-
const settingsPath =
|
|
5334
|
+
const settingsPath = path4.join(os3.homedir(), ".claude", "settings.json");
|
|
4723
5335
|
let settings = {};
|
|
4724
|
-
|
|
4725
|
-
settings = JSON.parse(
|
|
4726
|
-
}
|
|
4727
|
-
|
|
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
|
-
|
|
4747
|
-
|
|
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 =
|
|
4757
|
-
const commandsDir =
|
|
4758
|
-
|
|
4759
|
-
const files =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
if (
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
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
|
-
|
|
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 ${
|
|
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)) {
|