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.
- package/dist/index.js +834 -171
- 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, 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3679
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
4012
|
-
if (
|
|
4439
|
+
const configPath = join2(configDir, "config");
|
|
4440
|
+
if (existsSync2(configPath)) {
|
|
4013
4441
|
try {
|
|
4014
|
-
const lines =
|
|
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 =
|
|
4048
|
-
if (
|
|
4475
|
+
const keyPath = join2(configDir, "machine.key");
|
|
4476
|
+
if (existsSync2(keyPath)) {
|
|
4049
4477
|
try {
|
|
4050
|
-
privateKey =
|
|
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 (!
|
|
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
|
|
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
|
|
4561
|
+
return readFileSync3(opts.bodyFile, "utf8");
|
|
4134
4562
|
if (!process.stdin.isTTY) {
|
|
4135
4563
|
try {
|
|
4136
|
-
return
|
|
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 =
|
|
4915
|
+
const configPath = path4.join(os3.homedir(), ".airchat", "config");
|
|
4291
4916
|
const config = {};
|
|
4292
4917
|
try {
|
|
4293
|
-
const lines =
|
|
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 (
|
|
4936
|
+
if (fs4.existsSync(path4.join(dir, "packages", "mcp-server", "src", "index.ts"))) {
|
|
4312
4937
|
return dir;
|
|
4313
4938
|
}
|
|
4314
|
-
const parent =
|
|
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 ||
|
|
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",
|
|
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 (
|
|
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
|
-
|
|
5117
|
+
execFileSync2("git", ["clone", "https://github.com/prone/airchat.git", config.airchatDir], {
|
|
5118
|
+
stdio: "pipe"
|
|
5119
|
+
});
|
|
4477
5120
|
console.log(` Installing dependencies...`);
|
|
4478
|
-
|
|
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:
|
|
4494
|
-
if (
|
|
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
|
-
|
|
4499
|
-
|
|
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 =
|
|
4507
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4532
|
-
const files =
|
|
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
|
-
|
|
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 =
|
|
4568
|
-
const privateKeyPath =
|
|
4569
|
-
const publicKeyPath =
|
|
4570
|
-
if (!reconfigure &&
|
|
4571
|
-
const existingPub =
|
|
4572
|
-
const existingPriv =
|
|
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
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
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 =
|
|
4634
|
-
|
|
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
|
-
|
|
4643
|
-
const agentsDir =
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4676
|
-
const
|
|
4677
|
-
const
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
const
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
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
|
|
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 };
|
|
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
|
|
5360
|
+
const name = "Install work hook";
|
|
4721
5361
|
try {
|
|
4722
|
-
const settingsPath =
|
|
5362
|
+
const settingsPath = path4.join(os3.homedir(), ".claude", "settings.json");
|
|
4723
5363
|
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
|
-
}
|
|
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
|
-
|
|
4747
|
-
|
|
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 =
|
|
4757
|
-
const commandsDir =
|
|
4758
|
-
|
|
4759
|
-
const files =
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
if (
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
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
|
-
|
|
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 ${
|
|
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)) {
|