@ghostystudio/cli 0.7.2 → 0.9.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/README.md +26 -2
- package/dist/index.js +870 -59
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -273,6 +273,40 @@ async function currentToken() {
|
|
|
273
273
|
if (cfg.expiresAt !== void 0 && cfg.expiresAt - Date.now() < 6e4) cfg = await refresh(cfg) ?? cfg;
|
|
274
274
|
return cfg.accessToken;
|
|
275
275
|
}
|
|
276
|
+
async function postLong(path, body, timeoutMs = 9e5) {
|
|
277
|
+
const base = await baseUrl();
|
|
278
|
+
const token = await currentToken();
|
|
279
|
+
const httpsRequest = base.startsWith("https:") ? (await import("node:https")).request : (await import("node:http")).request;
|
|
280
|
+
const data = JSON.stringify(body);
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const req = httpsRequest(`${base}${path}`, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
timeout: timeoutMs,
|
|
285
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.9.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
286
|
+
}, (res) => {
|
|
287
|
+
let buf = "";
|
|
288
|
+
res.on("data", (c) => buf += c);
|
|
289
|
+
res.on("end", () => {
|
|
290
|
+
let parsed;
|
|
291
|
+
try {
|
|
292
|
+
parsed = JSON.parse(buf);
|
|
293
|
+
} catch {
|
|
294
|
+
parsed = { error: buf.slice(0, 300) };
|
|
295
|
+
}
|
|
296
|
+
resolve({ status: res.statusCode ?? 0, retryAfter: retryAfterSeconds(res.headers["retry-after"]), body: parsed });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
req.on("timeout", () => req.destroy(new Error(`no answer in ${Math.round(timeoutMs / 6e4)} min`)));
|
|
300
|
+
req.on("error", (e) => resolve({ status: 0, retryAfter: null, body: { error: String(e) } }));
|
|
301
|
+
req.end(data);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function retryAfterSeconds(h, now = Date.now()) {
|
|
305
|
+
if (!h) return null;
|
|
306
|
+
if (/^\d+$/.test(h.trim())) return Number(h.trim());
|
|
307
|
+
const t = Date.parse(h);
|
|
308
|
+
return Number.isNaN(t) ? null : Math.max(0, Math.round((t - now) / 1e3));
|
|
309
|
+
}
|
|
276
310
|
async function api(path, opts = {}) {
|
|
277
311
|
const res = await request(path, opts);
|
|
278
312
|
const text = await res.text();
|
|
@@ -290,7 +324,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
290
324
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
291
325
|
headers: {
|
|
292
326
|
authorization: `Bearer ${token}`,
|
|
293
|
-
"user-agent": `ghosty-cli/${"0.
|
|
327
|
+
"user-agent": `ghosty-cli/${"0.9.0"}`,
|
|
294
328
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
295
329
|
...opts.headers
|
|
296
330
|
},
|
|
@@ -313,7 +347,7 @@ async function toError(res) {
|
|
|
313
347
|
}
|
|
314
348
|
const detail = body.message || body.error_description || (body.detalle && body.error ? `${body.error}: ${body.detalle}` : body.error) || text.slice(0, 300) || res.statusText;
|
|
315
349
|
if (res.status === 401) return new CliError("Your session is not valid anymore.", EXIT.AUTH, "Run: ghosty login");
|
|
316
|
-
if (res.status === 403) return new CliError(`Not allowed: ${detail}`, EXIT.API, "
|
|
350
|
+
if (res.status === 403) return new CliError(`Not allowed: ${detail}`, EXIT.API, "If the agent was shared with you, its workspace (board, channels, numbers) stays with the owner: ask them to add you to that workspace. Otherwise run: ghosty login");
|
|
317
351
|
if (res.status === 404) return new CliError("Not found.", EXIT.API, "Check the agent id (ghosty agents ls) or that your token belongs to that agent.");
|
|
318
352
|
if (body.error === "restart_no_aplica" || body.error === "agente_sin_maquina") {
|
|
319
353
|
return new CliError("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", EXIT.API);
|
|
@@ -322,9 +356,146 @@ async function toError(res) {
|
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
// src/commands/agents.ts
|
|
325
|
-
import { readFile as
|
|
359
|
+
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
360
|
+
import { homedir as homedir2 } from "node:os";
|
|
361
|
+
import { dirname as dirname2, join as join3, relative } from "node:path";
|
|
362
|
+
|
|
363
|
+
// src/prompt.ts
|
|
364
|
+
import { createInterface } from "node:readline/promises";
|
|
365
|
+
var canPrompt = () => Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
366
|
+
function requireYesIfHeadless(yes, retry) {
|
|
367
|
+
if (!yes && !canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${retry} --yes`);
|
|
368
|
+
}
|
|
369
|
+
async function confirm(question, opts) {
|
|
370
|
+
if (opts.yes) return;
|
|
371
|
+
if (!canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${opts.retry} --yes`);
|
|
372
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
373
|
+
try {
|
|
374
|
+
const ask = opts.expect ? `${question}
|
|
375
|
+
Type ${bold(opts.expect)} to confirm: ` : `${question} [y/N] `;
|
|
376
|
+
const a = (await rl.question(ask)).trim();
|
|
377
|
+
const ok = opts.expect ? a === opts.expect : /^y(es)?$/i.test(a);
|
|
378
|
+
if (!ok) throw usage("Cancelled.");
|
|
379
|
+
} finally {
|
|
380
|
+
rl.close();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function distance(a, b) {
|
|
384
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
385
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
386
|
+
for (let i = 1; i <= a.length; i++)
|
|
387
|
+
for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
388
|
+
return d[a.length][b.length];
|
|
389
|
+
}
|
|
390
|
+
function suggest(input, known) {
|
|
391
|
+
const best = known.map((k) => [k, distance(input, k)]).sort((x, y) => x[1] - y[1])[0];
|
|
392
|
+
if (best && best[1] <= 2) return best[0];
|
|
393
|
+
return known.find((k) => k.startsWith(input) && input.length >= 2);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/commands/replay.ts
|
|
397
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
398
|
+
import { basename, join as join2 } from "node:path";
|
|
326
399
|
var enc = encodeURIComponent;
|
|
327
|
-
var
|
|
400
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
401
|
+
var MIME = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", pdf: "application/pdf", ogg: "audio/ogg", opus: "audio/ogg", mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", mp4: "video/mp4" };
|
|
402
|
+
async function tryPost(id, body) {
|
|
403
|
+
for (let attempt = 0; ; attempt++) {
|
|
404
|
+
const r = await postLong(`/api/v2/agents/${enc(id)}/try`, body);
|
|
405
|
+
if ((r.status === 503 || r.status === 409 || r.status === 0) && attempt < 40) {
|
|
406
|
+
const wait = r.status === 503 ? r.retryAfter ?? 15 : Math.min(30, 2 ** Math.min(attempt, 4) + 1);
|
|
407
|
+
if (!globals.json) info(dim(` ${r.status === 503 ? "Ghosty is restarting" : r.status === 409 ? "previous turn still running" : "network error"} \u2014 retrying in ${wait}s`));
|
|
408
|
+
await sleep(wait * 1e3);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
return { status: r.status, body: r.body };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function uploadMedia(file) {
|
|
415
|
+
const bytes = await readFile2(file);
|
|
416
|
+
const ext = file.split(".").pop()?.toLowerCase() ?? "";
|
|
417
|
+
const r = await api(`/api/v2/me/files?nombre=${enc(basename(file))}`, {
|
|
418
|
+
method: "POST",
|
|
419
|
+
raw: new Uint8Array(bytes),
|
|
420
|
+
headers: { "content-type": "application/octet-stream" }
|
|
421
|
+
});
|
|
422
|
+
const mime = MIME[ext] ?? "application/octet-stream";
|
|
423
|
+
return { url: r.url, mime, filename: r.name, type: mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document" };
|
|
424
|
+
}
|
|
425
|
+
function sentText(sent) {
|
|
426
|
+
return sent.map((s) => s.kind === "text" ? `${lavender("\u2192")} ${s.text}` : s.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s.text}` : `${lavender("\u{1F4CE}")} ${s.name} ${dim(s.url)}`).join("\n\n");
|
|
427
|
+
}
|
|
428
|
+
async function tryWaba(id, text, v) {
|
|
429
|
+
const phone = v.session ?? "";
|
|
430
|
+
if (!/^\d{10,15}$/.test(phone)) throw usage("--waba needs --session <phone> (10-15 digits, a made-up number).", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
431
|
+
if ((v.media?.length ?? 0) > 1) throw usage("One --media per turn (WhatsApp sends one file per message).", "Send several turns, one file each.");
|
|
432
|
+
if (!text && !v.media?.length && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
433
|
+
let history;
|
|
434
|
+
if (v.history) {
|
|
435
|
+
const raw = JSON.parse(await readFile2(v.history, "utf8"));
|
|
436
|
+
history = Array.isArray(raw) ? raw : raw.messages;
|
|
437
|
+
if (!Array.isArray(history)) throw usage(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`);
|
|
438
|
+
if (!v.reset) info(dim("Note: history only counts on the first turn of a session; add --reset to start over."));
|
|
439
|
+
}
|
|
440
|
+
const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
|
|
441
|
+
if (process.stderr.isTTY && !globals.json && (text || media)) info(dim("Thinking\u2026 (the real WhatsApp path; nothing is sent)"));
|
|
442
|
+
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {} });
|
|
443
|
+
if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ?? "turn failed"}`), { code: 1 });
|
|
444
|
+
out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim("(the agent sent nothing)") : dim("Session reset.")) + (r.body.error ? `
|
|
445
|
+
${dim(`(warning: ${r.body.error})`)}` : ""));
|
|
446
|
+
}
|
|
447
|
+
async function tryReplay(id, v) {
|
|
448
|
+
if (!v.out) throw usage("--replay needs --out DIR (the report is written there after every step).", "Example: ghosty try <agent-id> --replay muestra.json --out ./replay");
|
|
449
|
+
const max = Number(v.max ?? 15);
|
|
450
|
+
const conc = Math.max(1, Math.min(30, Number(v.concurrency ?? 8)));
|
|
451
|
+
const input = JSON.parse(await readFile2(v.replay, "utf8"));
|
|
452
|
+
const convs = input.sample ?? [];
|
|
453
|
+
if (!convs.length) throw usage(`${v.replay}: no conversations in "sample".`, 'Format: {"sample": [{"id", "fakePhone", "messages": [{"role": "user"|"agent", "text"}]}]}');
|
|
454
|
+
await mkdir2(v.out, { recursive: true });
|
|
455
|
+
const file = join2(v.out, "replay.json");
|
|
456
|
+
const prev = v.resume ? JSON.parse(await readFile2(file, "utf8").catch(() => "null")) : null;
|
|
457
|
+
const report = prev ?? { agentId: id, startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, done: false, results: {} };
|
|
458
|
+
report.done = false;
|
|
459
|
+
let saving = Promise.resolve();
|
|
460
|
+
const save = () => saving = saving.then(() => writeFile2(file, JSON.stringify(report, null, 1)));
|
|
461
|
+
info(`${bold("Replay")} ${convs.length} conversation(s), up to ${max} customer turns each, ${conc} at a time \u2192 ${file}`);
|
|
462
|
+
const queue = [...convs];
|
|
463
|
+
const worker = async () => {
|
|
464
|
+
for (let c = queue.shift(); c; c = queue.shift()) {
|
|
465
|
+
const pairs = [];
|
|
466
|
+
for (const m of c.messages) {
|
|
467
|
+
if (m.role === "user") pairs.push({ user: m.text, original: "" });
|
|
468
|
+
else if (pairs.length) pairs[pairs.length - 1].original += (pairs[pairs.length - 1].original ? "\n\n" : "") + m.text;
|
|
469
|
+
}
|
|
470
|
+
const entry = report.results[c.id] ??= { tags: c.tags ?? [], steps: [] };
|
|
471
|
+
const todo = pairs.slice(0, max).slice(entry.steps.length);
|
|
472
|
+
if (!todo.length) continue;
|
|
473
|
+
if (!entry.steps.length) await tryPost(id, { as: "waba", session: c.fakePhone, reset: true });
|
|
474
|
+
for (const p of todo) {
|
|
475
|
+
const t0 = Date.now();
|
|
476
|
+
const r = await tryPost(id, { as: "waba", session: c.fakePhone, text: p.user });
|
|
477
|
+
entry.steps.push({ user: p.user, original: p.original, reply: r.body.text ?? "", sent: r.body.sent ?? [], error: r.body.error ?? null, ms: Date.now() - t0, status: r.status });
|
|
478
|
+
await save();
|
|
479
|
+
}
|
|
480
|
+
const bad = entry.steps.filter((s) => s.status !== 200 || !s.reply).length;
|
|
481
|
+
info(` ${bad ? red("!") : lavender("\u2713")} ${c.id}: ${entry.steps.length} turn(s)${bad ? `, ${bad} without an answer` : ""}`);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
await Promise.all(Array.from({ length: Math.min(conc, convs.length) }, worker));
|
|
485
|
+
report.done = true;
|
|
486
|
+
report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
487
|
+
await save();
|
|
488
|
+
const steps = Object.values(report.results).flatMap((r) => r.steps);
|
|
489
|
+
const failed = steps.filter((s) => s.status !== 200 || !s.reply).length;
|
|
490
|
+
const summary = { file, conversations: Object.keys(report.results).length, turns: steps.length, failed, medianMs: steps.map((s) => s.ms).sort((a, b) => a - b)[Math.floor(steps.length / 2)] ?? 0 };
|
|
491
|
+
out(summary, () => `${failed ? red("!") : lavender("\u2713")} ${summary.turns} turn(s) in ${summary.conversations} conversation(s), ${failed} without an answer, median ${Math.round(summary.medianMs / 1e3)}s
|
|
492
|
+
${dim(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since 2h`)}`);
|
|
493
|
+
if (failed) process.exitCode = 1;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/commands/agents.ts
|
|
497
|
+
var enc2 = encodeURIComponent;
|
|
498
|
+
var agentPath = (id, rest = "") => `/api/v2/agents/${enc2(id)}${rest}`;
|
|
328
499
|
var need = (v, what, example) => {
|
|
329
500
|
if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
|
|
330
501
|
return v;
|
|
@@ -357,7 +528,7 @@ async function agentsCreate(v) {
|
|
|
357
528
|
if (v.engine) body.engine = v.engine;
|
|
358
529
|
if (v.model) body.model = v.model;
|
|
359
530
|
if (v.prompt) body.prompt = v.prompt;
|
|
360
|
-
if (v["prompt-file"]) body.prompt = await
|
|
531
|
+
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
361
532
|
const env = parseEnv(v.env);
|
|
362
533
|
if (env) body.env = env;
|
|
363
534
|
const r = await api("/api/v2/me/agents", { method: "POST", body });
|
|
@@ -366,7 +537,7 @@ async function agentsCreate(v) {
|
|
|
366
537
|
}
|
|
367
538
|
async function agentsGet(id, fields) {
|
|
368
539
|
id = need(id, "agent id", "ghosty agents get <agent-id>");
|
|
369
|
-
const q = fields ? `?fields=${
|
|
540
|
+
const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
|
|
370
541
|
out(await api(agentPath(id, q)));
|
|
371
542
|
}
|
|
372
543
|
async function agentsSet(id, v) {
|
|
@@ -376,19 +547,40 @@ async function agentsSet(id, v) {
|
|
|
376
547
|
if (v.engine) body.engine = v.engine;
|
|
377
548
|
if (v.model) body.model = v.model;
|
|
378
549
|
if (v.prompt) body.prompt = v.prompt;
|
|
379
|
-
if (v["prompt-file"]) body.prompt = await
|
|
550
|
+
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
380
551
|
const env = parseEnv(v.env);
|
|
381
552
|
if (env) body.env = env;
|
|
382
553
|
const connectors = {};
|
|
383
554
|
for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
|
|
384
555
|
for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
|
|
385
556
|
if (Object.keys(connectors).length) body.connectors = connectors;
|
|
386
|
-
|
|
557
|
+
const voice = {};
|
|
558
|
+
if (v.voice) {
|
|
559
|
+
const [engine, ...rest] = v.voice.split(":");
|
|
560
|
+
if (engine !== "elevenlabs" && engine !== "kokoro" || !rest.join(":")) throw usage(`Bad --voice: ${v.voice}.`, "Use elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora");
|
|
561
|
+
voice.engine = engine;
|
|
562
|
+
voice.voiceId = rest.join(":");
|
|
563
|
+
}
|
|
564
|
+
if (v["voice-replies"]) {
|
|
565
|
+
if (v["voice-replies"] !== "auto" && v["voice-replies"] !== "off") throw usage("--voice-replies takes auto or off.");
|
|
566
|
+
voice.replies = v["voice-replies"];
|
|
567
|
+
}
|
|
568
|
+
if (Object.keys(voice).length) body.voice = voice;
|
|
569
|
+
if (v["auto-stage"]) {
|
|
570
|
+
if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
|
|
571
|
+
body.autoStage = v["auto-stage"] === "on";
|
|
572
|
+
}
|
|
573
|
+
if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
|
|
387
574
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
388
575
|
out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
|
|
389
576
|
}
|
|
390
|
-
async function agentsRm(id) {
|
|
577
|
+
async function agentsRm(id, v = {}) {
|
|
391
578
|
id = need(id, "agent id", "ghosty agents rm <agent-id>");
|
|
579
|
+
requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
|
|
580
|
+
if (!v.yes) {
|
|
581
|
+
const a = await api(agentPath(id, "?fields=name"));
|
|
582
|
+
await confirm(`Delete agent ${a.name ?? id}? Its machine, memory and files go too. This can't be undone.`, { expect: a.name || id, retry: `ghosty agents rm ${id}` });
|
|
583
|
+
}
|
|
392
584
|
out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
|
|
393
585
|
}
|
|
394
586
|
async function agentsRestart(id) {
|
|
@@ -397,11 +589,13 @@ async function agentsRestart(id) {
|
|
|
397
589
|
}
|
|
398
590
|
async function tryTurn(id, text, v) {
|
|
399
591
|
id = need(id, "agent id", 'ghosty try <agent-id> "hello"');
|
|
592
|
+
if (v.replay) return tryReplay(id, v);
|
|
593
|
+
if (v.whatsapp && !v.waba) info(dim("Note: --whatsapp is now --waba (the real WhatsApp path)."));
|
|
594
|
+
if (v.waba || v.whatsapp || v.media?.length || v.history) return tryWaba(id, text, v);
|
|
400
595
|
if (!text && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hello"');
|
|
401
|
-
if (
|
|
402
|
-
} else info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
|
|
596
|
+
if (process.stderr.isTTY && text && !globals.json) info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
|
|
403
597
|
const r = await api(agentPath(id, "/try"), {
|
|
404
|
-
body: { text, session: v.session, reset: v.reset
|
|
598
|
+
body: { text, session: v.session, reset: v.reset }
|
|
405
599
|
});
|
|
406
600
|
out(r, () => (r.text ?? "") + (r.error ? `
|
|
407
601
|
${dim(`(warning: ${r.error})`)}` : ""));
|
|
@@ -423,20 +617,21 @@ ${table(r.files ?? [], cols)}`);
|
|
|
423
617
|
const res = await request(agentPath(id, `/files/${p}`));
|
|
424
618
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
425
619
|
if (v.out) {
|
|
426
|
-
await
|
|
620
|
+
await writeFile3(v.out, bytes);
|
|
427
621
|
out({ path: p, saved: v.out, bytes: bytes.length }, () => `${lavender("\u2713")} Saved ${bytes.length} bytes to ${v.out}`);
|
|
428
622
|
} else process.stdout.write(bytes);
|
|
429
623
|
return;
|
|
430
624
|
}
|
|
431
625
|
case "put": {
|
|
432
626
|
need(path, "path", ex);
|
|
433
|
-
const data = v.file ? await
|
|
627
|
+
const data = v.file ? await readFile3(v.file) : await readStdin();
|
|
434
628
|
const r = await api(agentPath(id, `/files/${p}`), { method: "PUT", raw: new Uint8Array(data), headers: { "content-type": "application/octet-stream" } });
|
|
435
629
|
out(r ?? { path: p, bytes: data.length }, () => `${lavender("\u2713")} Wrote ${p} (${data.length} bytes).`);
|
|
436
630
|
return;
|
|
437
631
|
}
|
|
438
632
|
case "rm": {
|
|
439
633
|
need(path, "path", ex);
|
|
634
|
+
await confirm(`Delete ${p} from the agent's machine?`, { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
|
|
440
635
|
out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} Deleted ${p}`);
|
|
441
636
|
return;
|
|
442
637
|
}
|
|
@@ -450,11 +645,38 @@ async function readStdin() {
|
|
|
450
645
|
for await (const c of process.stdin) parts.push(c);
|
|
451
646
|
return Buffer.concat(parts);
|
|
452
647
|
}
|
|
648
|
+
async function writeSkill(dir, sk) {
|
|
649
|
+
let bytes = 0;
|
|
650
|
+
for (const f of sk.files) {
|
|
651
|
+
const full = join3(dir, f.path);
|
|
652
|
+
if (relative(dir, full).startsWith("..")) continue;
|
|
653
|
+
await mkdir3(dirname2(full), { recursive: true });
|
|
654
|
+
const b = Buffer.from(f.contentBase64, "base64");
|
|
655
|
+
await writeFile3(full, b);
|
|
656
|
+
bytes += b.length;
|
|
657
|
+
}
|
|
658
|
+
return bytes;
|
|
659
|
+
}
|
|
660
|
+
async function readSkillDir(dir) {
|
|
661
|
+
const files = (await readdir(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile());
|
|
662
|
+
let markdown;
|
|
663
|
+
const assets = [];
|
|
664
|
+
for (const e of files) {
|
|
665
|
+
const full = join3(e.parentPath, e.name);
|
|
666
|
+
const rel = relative(dir, full).split("\\").join("/");
|
|
667
|
+
if (rel === "SKILL.md") markdown = await readFile3(full, "utf8");
|
|
668
|
+
else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await readFile3(full)).toString("base64") });
|
|
669
|
+
}
|
|
670
|
+
if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
|
|
671
|
+
return { markdown, assets };
|
|
672
|
+
}
|
|
673
|
+
var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
453
674
|
async function skillsCmd(sub, id, slug, v) {
|
|
454
|
-
const ex = "ghosty skills ls <agent-id> | add <agent-id> <slug> [--file SKILL.md] | rm <agent-id> <slug>";
|
|
675
|
+
const ex = "ghosty skills ls <agent-id> | get <agent-id> <slug> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
|
|
455
676
|
id = need(id, "agent id", ex);
|
|
456
|
-
switch (sub) {
|
|
457
|
-
case "ls":
|
|
677
|
+
switch (sub ?? "ls") {
|
|
678
|
+
case "ls":
|
|
679
|
+
case "list": {
|
|
458
680
|
const r = await api(agentPath(id, "/skills"));
|
|
459
681
|
const short2 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
|
|
460
682
|
const rows = [
|
|
@@ -463,22 +685,42 @@ async function skillsCmd(sub, id, slug, v) {
|
|
|
463
685
|
];
|
|
464
686
|
const more = r.community.filter((c) => !c.installed).length;
|
|
465
687
|
out(r, () => `${table(rows, ["slug", "source", "on", "description"])}
|
|
466
|
-
${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them)`)}`);
|
|
688
|
+
${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
case "get": {
|
|
692
|
+
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
693
|
+
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
|
|
694
|
+
const dir = v.out ?? slug;
|
|
695
|
+
const bytes = await writeSkill(dir, sk);
|
|
696
|
+
out({ slug, saved: dir, files: sk.files.map((f) => f.path), bytes }, () => `${lavender("\u2713")} ${slug} \u2192 ${dir}/ ${dim(`(${sk.files.length} file(s), ${bytes} bytes)`)}`);
|
|
467
697
|
return;
|
|
468
698
|
}
|
|
469
699
|
case "add": {
|
|
470
700
|
slug = need(slug, "skill slug", ex);
|
|
471
|
-
|
|
472
|
-
const
|
|
701
|
+
if (v.file && v.dir) throw usage("Use --file or --dir, not both.");
|
|
702
|
+
const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await readFile3(v.file, "utf8") } : { fromCatalog: true };
|
|
703
|
+
const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PUT", body });
|
|
473
704
|
out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
|
|
474
705
|
return;
|
|
475
706
|
}
|
|
476
707
|
case "rm":
|
|
708
|
+
case "remove": {
|
|
477
709
|
slug = need(slug, "skill slug", ex);
|
|
478
|
-
|
|
710
|
+
requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
|
|
711
|
+
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`)).catch(() => null);
|
|
712
|
+
await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
|
|
713
|
+
const backup = sk ? trashDir(id, slug) : null;
|
|
714
|
+
if (sk && backup) await writeSkill(backup, sk);
|
|
715
|
+
const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
|
|
716
|
+
const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
|
|
717
|
+
out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
|
|
718
|
+
${dim(`Backup: ${backup}
|
|
719
|
+
Undo: ${restore}`)}` : ""}`);
|
|
479
720
|
return;
|
|
721
|
+
}
|
|
480
722
|
default:
|
|
481
|
-
throw usage(`Unknown skills subcommand: ${sub
|
|
723
|
+
throw usage(`Unknown skills subcommand: ${sub}.`, ex);
|
|
482
724
|
}
|
|
483
725
|
}
|
|
484
726
|
async function mcpCmd(sub, id, v) {
|
|
@@ -487,7 +729,7 @@ async function mcpCmd(sub, id, v) {
|
|
|
487
729
|
if (sub === "get") return out(await api(agentPath(id, "/mcp")));
|
|
488
730
|
if (sub === "set") {
|
|
489
731
|
const file = need(v.file, "--file", ex);
|
|
490
|
-
const parsed = JSON.parse(await
|
|
732
|
+
const parsed = JSON.parse(await readFile3(file, "utf8"));
|
|
491
733
|
const body = { servers: Array.isArray(parsed) ? parsed : parsed.servers ?? [] };
|
|
492
734
|
const r = await api(agentPath(id, "/mcp"), { method: "PUT", body });
|
|
493
735
|
return out(r, () => `${lavender("\u2713")} MCP servers replaced \u2014 ${r?.reiniciado ? "the agent restarted with them." : "they load on the next turn."}`);
|
|
@@ -507,6 +749,7 @@ async function agentsDemo(id, v) {
|
|
|
507
749
|
if (v.welcome !== void 0) body.welcome = v.welcome;
|
|
508
750
|
if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
|
|
509
751
|
if (v.starter !== void 0) body.starter = v.starter || null;
|
|
752
|
+
if (v.rotate) body.rotate = true;
|
|
510
753
|
if (v.sponsor !== void 0) {
|
|
511
754
|
if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
|
|
512
755
|
body.sponsor = v.sponsor === "on";
|
|
@@ -528,9 +771,9 @@ async function agentsDemo(id, v) {
|
|
|
528
771
|
|
|
529
772
|
// src/commands/chat.ts
|
|
530
773
|
import { randomUUID } from "node:crypto";
|
|
531
|
-
import { createInterface } from "node:readline/promises";
|
|
532
|
-
import { readFile as
|
|
533
|
-
import { basename } from "node:path";
|
|
774
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
775
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
776
|
+
import { basename as basename2 } from "node:path";
|
|
534
777
|
|
|
535
778
|
// src/sse.ts
|
|
536
779
|
async function* readSse(body) {
|
|
@@ -565,8 +808,8 @@ function parseData(e) {
|
|
|
565
808
|
}
|
|
566
809
|
|
|
567
810
|
// src/commands/chat.ts
|
|
568
|
-
var
|
|
569
|
-
var convPath = (agent, rest = "") => `/api/v2/me/agents/${
|
|
811
|
+
var enc3 = encodeURIComponent;
|
|
812
|
+
var convPath = (agent, rest = "") => `/api/v2/me/agents/${enc3(agent)}/conversations${rest}`;
|
|
570
813
|
async function conversationsLs(agent) {
|
|
571
814
|
if (!agent) throw usage("Missing agent id.", "Example: ghosty conversations ls <agent-id>");
|
|
572
815
|
const r = await api(convPath(agent));
|
|
@@ -576,17 +819,17 @@ async function conversationsLs(agent) {
|
|
|
576
819
|
async function turn(agent, sid, text, first) {
|
|
577
820
|
const turnId = randomUUID();
|
|
578
821
|
const ctrl = new AbortController();
|
|
579
|
-
const res = await request(convPath(agent, `/${
|
|
822
|
+
const res = await request(convPath(agent, `/${enc3(sid)}/events`), { signal: ctrl.signal, headers: { accept: "text/event-stream" } });
|
|
580
823
|
let cancelled = false;
|
|
581
824
|
const onSigint = () => {
|
|
582
825
|
if (cancelled) process.exit(130);
|
|
583
826
|
cancelled = true;
|
|
584
827
|
info(dim("\n(cancelling\u2026 press Ctrl-C again to quit)"));
|
|
585
|
-
void api(convPath(agent, `/${
|
|
828
|
+
void api(convPath(agent, `/${enc3(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
|
|
586
829
|
});
|
|
587
830
|
};
|
|
588
831
|
process.on("SIGINT", onSigint);
|
|
589
|
-
const sent = api(convPath(agent, `/${
|
|
832
|
+
const sent = api(convPath(agent, `/${enc3(sid)}/messages`), { body: { content: text, turnId, primerTurno: first } });
|
|
590
833
|
let answer = "";
|
|
591
834
|
let usageData;
|
|
592
835
|
let ours = false;
|
|
@@ -638,7 +881,7 @@ async function chat(agent, message, v) {
|
|
|
638
881
|
await turn(agent, sid, piped, first);
|
|
639
882
|
return;
|
|
640
883
|
}
|
|
641
|
-
const rl =
|
|
884
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
642
885
|
info(dim("Type your message. Ctrl-C cancels a turn, Ctrl-D exits."));
|
|
643
886
|
for (; ; ) {
|
|
644
887
|
let line;
|
|
@@ -665,7 +908,7 @@ async function meFiles(sub, arg, v) {
|
|
|
665
908
|
switch (sub) {
|
|
666
909
|
case "ls": {
|
|
667
910
|
const r = await api(
|
|
668
|
-
`/api/v2/me/files${v.kind ? `?kind=${
|
|
911
|
+
`/api/v2/me/files${v.kind ? `?kind=${enc3(v.kind)}` : ""}`
|
|
669
912
|
);
|
|
670
913
|
out(r, () => {
|
|
671
914
|
const rows = r.files.map((f) => ({ id: f.id, name: f.name, kind: f.kind, size: f.size, created: f.createdAt.slice(0, 10) }));
|
|
@@ -676,8 +919,8 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
|
|
|
676
919
|
}
|
|
677
920
|
case "upload": {
|
|
678
921
|
if (!arg) throw usage("Missing file.", ex);
|
|
679
|
-
const bytes = await
|
|
680
|
-
const r = await api(`/api/v2/me/files?nombre=${
|
|
922
|
+
const bytes = await readFile4(arg);
|
|
923
|
+
const r = await api(`/api/v2/me/files?nombre=${enc3(basename2(arg))}`, {
|
|
681
924
|
method: "POST",
|
|
682
925
|
raw: new Uint8Array(bytes),
|
|
683
926
|
headers: { "content-type": "application/octet-stream" }
|
|
@@ -687,7 +930,7 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
|
|
|
687
930
|
}
|
|
688
931
|
case "rm": {
|
|
689
932
|
if (!arg) throw usage("Missing file id.", ex);
|
|
690
|
-
out(await api(`/api/v2/me/files/${
|
|
933
|
+
out(await api(`/api/v2/me/files/${enc3(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} Deleted ${arg}`);
|
|
691
934
|
return;
|
|
692
935
|
}
|
|
693
936
|
default:
|
|
@@ -710,7 +953,7 @@ async function meUsage() {
|
|
|
710
953
|
}
|
|
711
954
|
|
|
712
955
|
// src/commands/schedule.ts
|
|
713
|
-
var
|
|
956
|
+
var enc4 = encodeURIComponent;
|
|
714
957
|
function minutesOf(s, flag) {
|
|
715
958
|
const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s.trim());
|
|
716
959
|
if (!m) throw usage(`Invalid ${flag}: ${s}.`, "Use minutes, hours or days: 30m, 12h, 1d");
|
|
@@ -726,7 +969,7 @@ function isoOf(s, flag) {
|
|
|
726
969
|
var every = (min) => min == null ? "once" : min % 1440 === 0 ? `${min / 1440}d` : min % 60 === 0 ? `${min / 60}h` : `${min}m`;
|
|
727
970
|
var short = (s, n = 60) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
728
971
|
async function agentSchedule(agent) {
|
|
729
|
-
return api(`/api/v2/me/agents/${
|
|
972
|
+
return api(`/api/v2/me/agents/${enc4(agent)}/schedule`);
|
|
730
973
|
}
|
|
731
974
|
async function scheduleCmd(sub, agent, arg, v) {
|
|
732
975
|
if (sub === "ls" || sub === "list") {
|
|
@@ -769,13 +1012,13 @@ async function scheduleCmd(sub, agent, arg, v) {
|
|
|
769
1012
|
}
|
|
770
1013
|
let r;
|
|
771
1014
|
try {
|
|
772
|
-
r = await api(convPath(agent, `/${
|
|
1015
|
+
r = await api(convPath(agent, `/${enc4(sid)}/schedule`), { body });
|
|
773
1016
|
} catch (e) {
|
|
774
|
-
if (created) await api(convPath(agent, `/${
|
|
1017
|
+
if (created) await api(convPath(agent, `/${enc4(sid)}`), { method: "DELETE" }).catch(() => {
|
|
775
1018
|
});
|
|
776
1019
|
throw e;
|
|
777
1020
|
}
|
|
778
|
-
if (v.title && created) await api(convPath(agent, `/${
|
|
1021
|
+
if (v.title && created) await api(convPath(agent, `/${enc4(sid)}`), { method: "PATCH", body: { title: v.title } });
|
|
779
1022
|
const p = r.programado;
|
|
780
1023
|
return out(
|
|
781
1024
|
{ scheduled: p },
|
|
@@ -787,7 +1030,7 @@ ${dim(`conversation ${p.sessionId}`)}`
|
|
|
787
1030
|
if (!agent || !arg) throw usage("Missing agent id or schedule id.", "Example: ghosty schedule rm <agent-id> <schedule-id> (ids: ghosty schedule ls <agent-id>)");
|
|
788
1031
|
const row = (await agentSchedule(agent)).pendientes.find((p) => p.id === arg);
|
|
789
1032
|
if (!row) throw usage(`No pending scheduled turn ${arg}.`, "See them with: ghosty schedule ls <agent-id>");
|
|
790
|
-
await api(convPath(agent, `/${
|
|
1033
|
+
await api(convPath(agent, `/${enc4(row.sessionId)}/schedule/${enc4(arg)}`), { method: "DELETE" });
|
|
791
1034
|
return out({ canceled: arg }, () => `${lavender("\u2713")} Canceled ${arg}`);
|
|
792
1035
|
}
|
|
793
1036
|
throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
|
|
@@ -795,7 +1038,7 @@ ${dim(`conversation ${p.sessionId}`)}`
|
|
|
795
1038
|
|
|
796
1039
|
// src/commands/partner.ts
|
|
797
1040
|
import { createHmac } from "node:crypto";
|
|
798
|
-
import { readFile as
|
|
1041
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
799
1042
|
var PARTNER_HELP = `${bold("Usage:")} ghosty partner <command> [options]
|
|
800
1043
|
|
|
801
1044
|
${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
|
|
@@ -827,7 +1070,7 @@ ${bold("Options")}
|
|
|
827
1070
|
|
|
828
1071
|
${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
|
|
829
1072
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
830
|
-
var
|
|
1073
|
+
var enc5 = encodeURIComponent;
|
|
831
1074
|
function partnerCreds() {
|
|
832
1075
|
const key = process.env.GHOSTY_PARTNER_KEY;
|
|
833
1076
|
const secret = process.env.GHOSTY_PARTNER_SECRET;
|
|
@@ -847,7 +1090,7 @@ async function call(path, v, init = {}) {
|
|
|
847
1090
|
"x-ghosty-ts": ts,
|
|
848
1091
|
"x-ghosty-sig": sig,
|
|
849
1092
|
"content-type": "application/json",
|
|
850
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1093
|
+
"user-agent": `ghosty-cli/${"0.9.0"}`
|
|
851
1094
|
},
|
|
852
1095
|
body: method === "GET" ? void 0 : raw
|
|
853
1096
|
});
|
|
@@ -895,10 +1138,10 @@ async function credentialsCmd(sub, id, v) {
|
|
|
895
1138
|
if (sub === "rotate") {
|
|
896
1139
|
const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
|
|
897
1140
|
if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage("--grace must be minutes \u2265 0.");
|
|
898
|
-
return showSecret(await json(`/api/v2/partner/credentials/${
|
|
1141
|
+
return showSecret(await json(`/api/v2/partner/credentials/${enc5(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
|
|
899
1142
|
}
|
|
900
1143
|
if (sub === "revoke" || sub === "rm") {
|
|
901
|
-
const r = await json(`/api/v2/partner/credentials/${
|
|
1144
|
+
const r = await json(`/api/v2/partner/credentials/${enc5(id)}`, v, { ...owner, method: "DELETE" });
|
|
902
1145
|
return out(r, () => `${lavender("\u2713")} Revoked ${r.keyId}.`);
|
|
903
1146
|
}
|
|
904
1147
|
throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty partner credentials ls | create | rotate | revoke");
|
|
@@ -917,7 +1160,7 @@ async function agentCmd(sub, v) {
|
|
|
917
1160
|
}
|
|
918
1161
|
if (sub === "apply" || sub === "set") {
|
|
919
1162
|
if (!v.file) throw usage("Missing --file.", "Example: ghosty partner agent apply --file agent.json");
|
|
920
|
-
const body = JSON.parse(await
|
|
1163
|
+
const body = JSON.parse(await readFile5(v.file, "utf8"));
|
|
921
1164
|
const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
|
|
922
1165
|
return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} \xB7 v${r.agent.version}`);
|
|
923
1166
|
}
|
|
@@ -929,7 +1172,7 @@ async function tenantsCmd(sub, id, v) {
|
|
|
929
1172
|
return out(r, () => table(r.tenants.map((t) => ({ externalId: t.externalId, config: JSON.stringify(t.config ?? {}), updated: fmt(t.updatedAt) })), ["externalId", "config", "updated"]));
|
|
930
1173
|
}
|
|
931
1174
|
if (!id) throw usage("Missing externalId.", `Example: ghosty partner tenants ${sub} org_123`);
|
|
932
|
-
const path = `/api/v2/partner/tenants/${
|
|
1175
|
+
const path = `/api/v2/partner/tenants/${enc5(id)}`;
|
|
933
1176
|
if (sub === "get") return out(await json(path, v));
|
|
934
1177
|
if (sub === "set" || sub === "apply") {
|
|
935
1178
|
if (!v.config) throw usage("Missing --config.", `Example: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
|
|
@@ -1012,6 +1255,509 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
1012
1255
|
}
|
|
1013
1256
|
}
|
|
1014
1257
|
|
|
1258
|
+
// src/commands/data.ts
|
|
1259
|
+
var enc6 = encodeURIComponent;
|
|
1260
|
+
var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc6(id)}${rest}`;
|
|
1261
|
+
var need2 = (v, what, example) => {
|
|
1262
|
+
if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
|
|
1263
|
+
return v;
|
|
1264
|
+
};
|
|
1265
|
+
var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
1266
|
+
function planText(plan) {
|
|
1267
|
+
return plan.map((p) => p.action === "create" ? ` + ${p.table} create, ${p.sourceRows} row(s)` : ` ~ ${p.table} ${p.action} ${p.targetRows} \u2192 ${p.resultRows} row(s)`).join("\n");
|
|
1268
|
+
}
|
|
1269
|
+
async function dbsImport(id, db, v) {
|
|
1270
|
+
const ex = "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--dry-run]";
|
|
1271
|
+
const m = /^easybits:(.+)$/.exec(v.from ?? "");
|
|
1272
|
+
if (!m) throw usage("Missing or bad --from.", `Example: ${ex}`);
|
|
1273
|
+
const easybitsKey = process.env.EASYBITS_API_KEY;
|
|
1274
|
+
if (!easybitsKey) throw usage("EASYBITS_API_KEY is not set.", "Export your EasyBits API key (https://www.easybits.cloud/dash/developer) and run again.");
|
|
1275
|
+
const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {} };
|
|
1276
|
+
const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
|
|
1277
|
+
const dry = await api(path, { body: { ...body, dryRun: true } });
|
|
1278
|
+
if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
|
|
1279
|
+
${planText(dry.plan)}
|
|
1280
|
+
${dim("Dry run: nothing changed.")}`);
|
|
1281
|
+
const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
|
|
1282
|
+
if (destructive) await confirm(`${planText(dry.plan)}
|
|
1283
|
+
Replace the rows above in ${dry.db}?`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
|
|
1284
|
+
const r = await api(path, { body });
|
|
1285
|
+
return out(r, () => `${planText(r.plan)}
|
|
1286
|
+
${lavender("\u2713")} Imported into ${r.db}. ${dim("Run it again any time: with replace it ends in the same state.")}`);
|
|
1287
|
+
}
|
|
1288
|
+
async function dbsCmd(sub, id, db, rest, v) {
|
|
1289
|
+
const ex = "ghosty dbs ls <agent-id>";
|
|
1290
|
+
id = need2(id, "agent id", ex);
|
|
1291
|
+
switch (sub ?? "ls") {
|
|
1292
|
+
case "ls":
|
|
1293
|
+
case "list": {
|
|
1294
|
+
const r = await api(agentPath2(id, "/dbs"));
|
|
1295
|
+
const rows = r.dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", id: d.id }));
|
|
1296
|
+
return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "id"]) : dim("This account has no databases yet: create one at /app/dbs."));
|
|
1297
|
+
}
|
|
1298
|
+
case "tables": {
|
|
1299
|
+
db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
|
|
1300
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
|
|
1301
|
+
const ext = new Set(r.externalWriteTables);
|
|
1302
|
+
const rows = r.tables.map((t) => ({ table: t.name, rows: t.rows, "customer writes": ext.has(t.name) ? "yes" : "", columns: t.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
|
|
1303
|
+
return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
|
|
1304
|
+
${table(rows, ["table", "rows", "customer writes", "columns"])}`);
|
|
1305
|
+
}
|
|
1306
|
+
case "query":
|
|
1307
|
+
case "sql": {
|
|
1308
|
+
db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
|
|
1309
|
+
const sql = need2(rest.join(" ").trim() || void 0, "SQL", 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
|
|
1310
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
|
|
1311
|
+
if (e instanceof CliError && !v.write && /lectura|SELECT|escribir/i.test(e.message)) throw new CliError(e.message, EXIT.API, "To write on purpose, add --write.");
|
|
1312
|
+
throw e;
|
|
1313
|
+
});
|
|
1314
|
+
return out(r, () => {
|
|
1315
|
+
if (!r.cols.length) return `${lavender("\u2713")} ${r.affected} row(s) affected ${dim(`(${Math.round(r.ms)} ms)`)}`;
|
|
1316
|
+
const rows = r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]])));
|
|
1317
|
+
return `${table(rows, r.cols)}
|
|
1318
|
+
${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
case "grant": {
|
|
1322
|
+
db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
|
|
1323
|
+
const body = v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {};
|
|
1324
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "PUT", body });
|
|
1325
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
|
|
1326
|
+
}
|
|
1327
|
+
case "import": {
|
|
1328
|
+
db = need2(db, "database (id or name)", "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
|
|
1329
|
+
return dbsImport(id, db, v);
|
|
1330
|
+
}
|
|
1331
|
+
case "revoke": {
|
|
1332
|
+
db = need2(db, "database (id or name)", "ghosty dbs revoke <agent-id> <db>");
|
|
1333
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "DELETE" });
|
|
1334
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
|
|
1335
|
+
}
|
|
1336
|
+
default:
|
|
1337
|
+
throw usage(`Unknown dbs subcommand: ${sub}.`, "Try: ghosty dbs ls | tables | query | import | grant | revoke (ghosty dbs --help)");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
async function agentDocsCmd(sub, id, doc, v) {
|
|
1341
|
+
id = need2(id, "agent id", "ghosty agents docs ls <agent-id>");
|
|
1342
|
+
switch (sub ?? "ls") {
|
|
1343
|
+
case "ls":
|
|
1344
|
+
case "list": {
|
|
1345
|
+
const r = await api(agentPath2(id, "/documents"));
|
|
1346
|
+
const rows = r.documents.map((d) => ({ name: d.name, access: d.granted ? d.mode === "write" ? "read+replace" : "read" : "\u2014", delivers: d.granted ? d.deliverable ? "yes" : "no" : "", id: d.id }));
|
|
1347
|
+
return out(r.documents, () => rows.length ? table(rows, ["name", "access", "delivers", "id"]) : dim("This account has no documents yet: upload them at /app/datos."));
|
|
1348
|
+
}
|
|
1349
|
+
case "grant": {
|
|
1350
|
+
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf"');
|
|
1351
|
+
const body = {};
|
|
1352
|
+
if (v.write) body.mode = "write";
|
|
1353
|
+
if (v["no-deliver"]) body.deliverable = false;
|
|
1354
|
+
else if (v.deliver) body.deliverable = true;
|
|
1355
|
+
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
|
|
1356
|
+
return out(r, () => `${lavender("\u2713")} ${r.name}: ${r.mode === "write" ? "reads and replaces it" : "reads it"}${r.deliverable ? ", and can send it" : ", never sends it"}.`);
|
|
1357
|
+
}
|
|
1358
|
+
case "revoke": {
|
|
1359
|
+
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
|
|
1360
|
+
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
|
|
1361
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
|
|
1362
|
+
}
|
|
1363
|
+
default:
|
|
1364
|
+
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async function turnsCmd(sub, id, v) {
|
|
1368
|
+
if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h");
|
|
1369
|
+
id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
|
|
1370
|
+
const q = new URLSearchParams();
|
|
1371
|
+
const status = v.errors ? "error" : v.status;
|
|
1372
|
+
if (status) q.set("state", status);
|
|
1373
|
+
if (v.since) q.set("since", v.since);
|
|
1374
|
+
q.set("limit", v.limit ?? "20");
|
|
1375
|
+
const r = await api(agentPath2(id, `/turns?${q}`));
|
|
1376
|
+
const when = (s) => s.slice(0, 16).replace("T", " ");
|
|
1377
|
+
const rows = r.turns.map((t) => ({
|
|
1378
|
+
started: when(t.startedAt),
|
|
1379
|
+
state: t.state === "error" ? red(t.state) : t.state,
|
|
1380
|
+
secs: t.ms == null ? "" : Math.round(t.ms / 1e3),
|
|
1381
|
+
from: t.surface ?? "",
|
|
1382
|
+
session: t.sessionId.slice(0, 24),
|
|
1383
|
+
error: (t.error ?? "").replace(/\s+/g, " ").slice(0, 90)
|
|
1384
|
+
}));
|
|
1385
|
+
const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
|
|
1386
|
+
out(r, () => rows.length ? `${table(rows, ["started", "state", "secs", "from", "session", "error"])}
|
|
1387
|
+
${dim(`${v.since ? `since ${v.since}: ` : ""}${tally}`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
|
|
1388
|
+
}
|
|
1389
|
+
async function agentsDoctor(id, v) {
|
|
1390
|
+
id = need2(id, "agent id", "ghosty agents doctor <agent-id>");
|
|
1391
|
+
const r = await api(agentPath2(id, "/doctor"));
|
|
1392
|
+
const problems = r.summary.warn + r.summary.error;
|
|
1393
|
+
if (globals.json) out(r);
|
|
1394
|
+
else {
|
|
1395
|
+
const mark = { ok: lavender("\u2713"), warn: "\x1B[33m!\x1B[0m", error: red("\u2717") };
|
|
1396
|
+
const lines = [];
|
|
1397
|
+
for (const c of r.checks) {
|
|
1398
|
+
if (c.level === "ok" && !v.verbose) continue;
|
|
1399
|
+
lines.push(`${process.stdout.isTTY ? mark[c.level] : { ok: "\u2713", warn: "!", error: "\u2717" }[c.level]} ${c.title}`);
|
|
1400
|
+
if (c.detail && c.level !== "ok") lines.push(...c.detail.split("\n").map((l) => ` ${dim(l)}`));
|
|
1401
|
+
if (c.fix && c.level !== "ok") lines.push(` ${dim("fix:")} ${c.fix}`);
|
|
1402
|
+
}
|
|
1403
|
+
lines.push(problems ? `Doctor found ${r.summary.error} error(s) and ${r.summary.warn} warning(s) in ${r.checks.length} checks.` : `${lavender("\u2713")} No issues found (${r.checks.length} checks).`);
|
|
1404
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1405
|
+
}
|
|
1406
|
+
if (problems) {
|
|
1407
|
+
if (!globals.json) info(dim("Exit 1: problems found."));
|
|
1408
|
+
process.exit(EXIT.API);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/commands/credentials.ts
|
|
1413
|
+
async function hiddenPrompt(label) {
|
|
1414
|
+
process.stderr.write(label);
|
|
1415
|
+
const stdin = process.stdin;
|
|
1416
|
+
stdin.setRawMode(true);
|
|
1417
|
+
stdin.resume();
|
|
1418
|
+
stdin.setEncoding("utf8");
|
|
1419
|
+
return new Promise((resolve, reject) => {
|
|
1420
|
+
let value = "";
|
|
1421
|
+
const onData = (chunk) => {
|
|
1422
|
+
for (const ch of chunk) {
|
|
1423
|
+
if (ch === "\r" || ch === "\n") {
|
|
1424
|
+
stdin.setRawMode(false);
|
|
1425
|
+
stdin.pause();
|
|
1426
|
+
stdin.off("data", onData);
|
|
1427
|
+
process.stderr.write("\n");
|
|
1428
|
+
return resolve(value);
|
|
1429
|
+
}
|
|
1430
|
+
if (ch === "") {
|
|
1431
|
+
stdin.setRawMode(false);
|
|
1432
|
+
process.stderr.write("\n");
|
|
1433
|
+
return reject(usage("Cancelled."));
|
|
1434
|
+
}
|
|
1435
|
+
if (ch === "\x7F") value = value.slice(0, -1);
|
|
1436
|
+
else value += ch;
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
stdin.on("data", onData);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
async function readSecret(provider, fromEnv) {
|
|
1443
|
+
if (fromEnv) {
|
|
1444
|
+
const v = process.env[fromEnv];
|
|
1445
|
+
if (!v) throw usage(`$${fromEnv} is empty or not set.`, `Example: export ${fromEnv}=\u2026 && ghosty credentials set ${provider} --from-env ${fromEnv}`);
|
|
1446
|
+
return v;
|
|
1447
|
+
}
|
|
1448
|
+
if (!process.stdin.isTTY) {
|
|
1449
|
+
const parts = [];
|
|
1450
|
+
for await (const c of process.stdin) parts.push(c);
|
|
1451
|
+
return Buffer.concat(parts).toString("utf8").replace(/\r?\n$/, "");
|
|
1452
|
+
}
|
|
1453
|
+
if (!canPrompt()) throw usage("No value to save.", `Pipe it (\u2026 | ghosty credentials set ${provider}) or use --from-env VAR.`);
|
|
1454
|
+
return hiddenPrompt(`${provider} key (hidden): `);
|
|
1455
|
+
}
|
|
1456
|
+
async function credentialsCmd2(sub, provider, v) {
|
|
1457
|
+
switch (sub ?? "ls") {
|
|
1458
|
+
case "ls":
|
|
1459
|
+
case "list": {
|
|
1460
|
+
const r = await api("/api/v2/me/credentials");
|
|
1461
|
+
const rows = r.credentials.map((c) => ({ provider: c.provider, set: c.configured ? `\u2026${c.lastFour ?? ""}` : "\u2014", updated: c.updatedAt?.slice(0, 10) ?? "" }));
|
|
1462
|
+
return out(r.credentials, () => table(rows, ["provider", "set", "updated"]));
|
|
1463
|
+
}
|
|
1464
|
+
case "set": {
|
|
1465
|
+
if (!provider) throw usage("Missing provider.", "Example: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls lists them)");
|
|
1466
|
+
const key = (await readSecret(provider, v["from-env"])).trim();
|
|
1467
|
+
if (!key) throw usage("Empty key: nothing saved.");
|
|
1468
|
+
const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "PUT", body: { key } });
|
|
1469
|
+
return out(r, () => `${lavender("\u2713")} ${provider} saved ${dim(`(\u2026${r.lastFour ?? ""}). Agents with a machine pick it up on their next turn.`)}`);
|
|
1470
|
+
}
|
|
1471
|
+
case "rm":
|
|
1472
|
+
case "remove": {
|
|
1473
|
+
if (!provider) throw usage("Missing provider.", "Example: ghosty credentials rm elevenlabs");
|
|
1474
|
+
await confirm(`Remove your ${provider} key? Agents using it stop working until you set another.`, { yes: v.yes, retry: `ghosty credentials rm ${provider}` });
|
|
1475
|
+
const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "DELETE" });
|
|
1476
|
+
return out(r, () => `${lavender("\u2713")} ${provider} removed.`);
|
|
1477
|
+
}
|
|
1478
|
+
default:
|
|
1479
|
+
throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty credentials ls | set | rm (ghosty credentials --help)");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/commands/channels.ts
|
|
1484
|
+
var enc7 = encodeURIComponent;
|
|
1485
|
+
var digits = (s) => s.replace(/\D/g, "");
|
|
1486
|
+
function find(nums, ref) {
|
|
1487
|
+
const d = digits(ref);
|
|
1488
|
+
const hit = nums.find((n) => n.id === ref) ?? nums.find((n) => n.label.toLowerCase() === ref.toLowerCase()) ?? (d.length >= 6 ? nums.find((n) => digits(n.label).endsWith(d)) : void 0);
|
|
1489
|
+
if (!hit) throw usage(`No WhatsApp number "${ref}" for this agent.`, nums.length ? `Available: ${nums.map((n) => n.label).join(", ")}` : "Pair a number first in the web (the agent's Channels page).");
|
|
1490
|
+
return hit;
|
|
1491
|
+
}
|
|
1492
|
+
async function channelsCmd(kind, sub, id, ref, v) {
|
|
1493
|
+
if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
|
|
1494
|
+
if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
|
|
1495
|
+
if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
|
|
1496
|
+
const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
|
|
1497
|
+
const cur = await api(path);
|
|
1498
|
+
if (!sub || sub === "ls" || sub === "list") {
|
|
1499
|
+
const rows = cur.numbers.map((n2) => ({ number: n2.label, answers: n2.mine ? lavender("this agent") : n2.takenByOther ? "another agent" : dim("nobody (silent)"), id: n2.id }));
|
|
1500
|
+
return out(cur, () => rows.length ? table(rows, ["number", "answers", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
|
|
1501
|
+
}
|
|
1502
|
+
if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable <agent-id> <number>");
|
|
1503
|
+
if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
|
|
1504
|
+
const n = find(cur.numbers, ref);
|
|
1505
|
+
const on = sub === "enable";
|
|
1506
|
+
if (on === n.mine) return out({ changed: 0, number: n.label, answers: on }, () => dim(`Nothing to do: ${n.label} is already ${on ? "answered by this agent" : "not answered by it"}.`));
|
|
1507
|
+
const plan = on ? `+ ${n.label}: this agent starts answering its customers${n.takenByOther ? " (takes it from the agent answering today)" : ""}` : `- ${n.label}: this agent stops answering (messages are still logged)`;
|
|
1508
|
+
if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
|
|
1509
|
+
${dim("Dry run: nothing changed.")}`);
|
|
1510
|
+
if (on) await confirm(`${bold(plan)}
|
|
1511
|
+
Real customers will get answers from this agent.`, { yes: v.yes, expect: digits(n.label).slice(-4) || n.id, retry: `ghosty channels whatsapp enable ${id} ${ref}` });
|
|
1512
|
+
const wanted = cur.numbers.filter((x) => x.id === n.id ? on : x.mine).map((x) => x.id);
|
|
1513
|
+
const r = await api(path, { method: "PUT", body: { numbers: wanted } });
|
|
1514
|
+
return out(r, () => `${lavender("\u2713")} ${plan.slice(2)}`);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/commands/board.ts
|
|
1518
|
+
async function boardCmd(sub, id, v) {
|
|
1519
|
+
if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1520
|
+
if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1521
|
+
const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
|
|
1522
|
+
if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
|
|
1523
|
+
const retry = `ghosty board archive ${id} ${Object.entries(filter).map(([k, x]) => `--${k} ${JSON.stringify(x)}`).join(" ")}`;
|
|
1524
|
+
if (!v["dry-run"]) requireYesIfHeadless(v.yes, retry);
|
|
1525
|
+
const path = `/api/v2/agents/${encodeURIComponent(id)}/board/archive`;
|
|
1526
|
+
const plan = await api(path, { body: { ...filter, dryRun: true } });
|
|
1527
|
+
const sample = () => table(plan.sample.map((c) => ({ name: c.name ?? "", phone: c.phone, column: c.estado_label ?? "" })), ["name", "phone", "column"]);
|
|
1528
|
+
if (v["dry-run"] || plan.matched === 0) {
|
|
1529
|
+
return out(plan, () => plan.matched ? `${sample()}
|
|
1530
|
+
${dim(`Would archive ${plan.matched} card(s) on \xAB${plan.board}\xBB. Dry run: nothing changed.`)}` : dim(`No cards match on \xAB${plan.board}\xBB.`));
|
|
1531
|
+
}
|
|
1532
|
+
await confirm(`${sample()}
|
|
1533
|
+
Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new message brings a card back)`, { yes: v.yes, retry });
|
|
1534
|
+
const r = await api(path, { body: filter });
|
|
1535
|
+
out(r, () => `${lavender("\u2713")} Archived ${r.archived} card(s) on \xAB${r.board}\xBB.`);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/help.ts
|
|
1539
|
+
var DOCS = "https://www.ghosty.studio/docs/cli";
|
|
1540
|
+
var HELPS = {
|
|
1541
|
+
agents: {
|
|
1542
|
+
summary: "Create, inspect, change, check and delete agents.",
|
|
1543
|
+
usage: [
|
|
1544
|
+
"ghosty agents ls",
|
|
1545
|
+
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
|
|
1546
|
+
"ghosty agents get <agent-id> [--fields name,model,prompt]",
|
|
1547
|
+
"ghosty agents set <agent-id> [--name] [--engine] [--model] [--prompt | --prompt-file] [--env K=V,\u2026] [--connect ID,\u2026] [--disconnect ID,\u2026] [--voice E:ID] [--voice-replies auto|off] [--auto-stage on|off]",
|
|
1548
|
+
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1549
|
+
"ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
|
|
1550
|
+
"ghosty agents restart <agent-id>",
|
|
1551
|
+
"ghosty agents demo <agent-id> [--slug NAME] [--on | --off] \u2026",
|
|
1552
|
+
"ghosty agents rm <agent-id> [--yes]"
|
|
1553
|
+
],
|
|
1554
|
+
flags: [
|
|
1555
|
+
["--fields a,b", "get: return only these fields"],
|
|
1556
|
+
["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
|
|
1557
|
+
["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
|
|
1558
|
+
["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
|
|
1559
|
+
["--verbose", "doctor: also list the checks that passed"],
|
|
1560
|
+
["--write", "docs grant: the agent may replace the document (default: read only)"],
|
|
1561
|
+
["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
|
|
1562
|
+
["--yes", "rm: skip the confirmation (required without a terminal)"]
|
|
1563
|
+
],
|
|
1564
|
+
examples: [
|
|
1565
|
+
"ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
|
|
1566
|
+
"ghosty agents set <id> --prompt-file PROMPT.md",
|
|
1567
|
+
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1568
|
+
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1569
|
+
"ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
|
|
1570
|
+
],
|
|
1571
|
+
notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
|
|
1572
|
+
learn: `${DOCS}#agents`
|
|
1573
|
+
},
|
|
1574
|
+
skills: {
|
|
1575
|
+
summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
|
|
1576
|
+
usage: [
|
|
1577
|
+
"ghosty skills ls <agent-id>",
|
|
1578
|
+
"ghosty skills get <agent-id> <slug> [--out DIR]",
|
|
1579
|
+
"ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
|
|
1580
|
+
"ghosty skills rm <agent-id> <slug> [--yes]"
|
|
1581
|
+
],
|
|
1582
|
+
flags: [
|
|
1583
|
+
["--out DIR", "get: folder to write into (default ./<slug>)"],
|
|
1584
|
+
["--file FILE", "add: upload one SKILL.md"],
|
|
1585
|
+
["--dir FOLDER", "add: upload a whole skill folder (SKILL.md + assets); restores a backup"],
|
|
1586
|
+
["--yes", "rm: skip the confirmation (required without a terminal)"]
|
|
1587
|
+
],
|
|
1588
|
+
examples: [
|
|
1589
|
+
"ghosty skills get <id> cotizar --out ./backup/cotizar",
|
|
1590
|
+
"ghosty skills add <id> cotizar --dir ./backup/cotizar",
|
|
1591
|
+
"ghosty skills add <id> pdf-doc # from the community catalog",
|
|
1592
|
+
"ghosty skills rm <id> cotizacion # keeps a local copy first"
|
|
1593
|
+
],
|
|
1594
|
+
notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
|
|
1595
|
+
learn: `${DOCS}#skills`
|
|
1596
|
+
},
|
|
1597
|
+
dbs: {
|
|
1598
|
+
summary: "Databases an agent can use, and what it may write from customer channels.",
|
|
1599
|
+
usage: [
|
|
1600
|
+
"ghosty dbs ls <agent-id>",
|
|
1601
|
+
"ghosty dbs tables <agent-id> <db>",
|
|
1602
|
+
'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
|
|
1603
|
+
"ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--dry-run] [--yes]",
|
|
1604
|
+
"ghosty dbs grant <agent-id> <db> [--external-write t1,t2]",
|
|
1605
|
+
"ghosty dbs revoke <agent-id> <db>"
|
|
1606
|
+
],
|
|
1607
|
+
flags: [
|
|
1608
|
+
["--arg V", "query: value for a ? placeholder (repeatable)"],
|
|
1609
|
+
["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
|
|
1610
|
+
["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
|
|
1611
|
+
["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
|
|
1612
|
+
["--tables a,b", "import: only these tables (default: all)"],
|
|
1613
|
+
["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
|
|
1614
|
+
["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing"]
|
|
1615
|
+
],
|
|
1616
|
+
examples: [
|
|
1617
|
+
"ghosty dbs ls <id>",
|
|
1618
|
+
"ghosty dbs tables <id> totequim",
|
|
1619
|
+
'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
|
|
1620
|
+
"ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
|
|
1621
|
+
"ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
|
|
1622
|
+
],
|
|
1623
|
+
notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
|
|
1624
|
+
learn: `${DOCS}#dbs`
|
|
1625
|
+
},
|
|
1626
|
+
turns: {
|
|
1627
|
+
summary: "An agent's recent turns: what answered, what failed and why.",
|
|
1628
|
+
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]"],
|
|
1629
|
+
flags: [
|
|
1630
|
+
["--errors", "only failed turns (same as --status error)"],
|
|
1631
|
+
["--status S", "running | done | stopped | error"],
|
|
1632
|
+
["--since T", "30m, 1h, 2d or an ISO date"],
|
|
1633
|
+
["--limit N", "max rows (default 20, max 500)"]
|
|
1634
|
+
],
|
|
1635
|
+
examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
|
|
1636
|
+
learn: `${DOCS}#turns`
|
|
1637
|
+
},
|
|
1638
|
+
files: {
|
|
1639
|
+
summary: "Files in the agent's machine.",
|
|
1640
|
+
usage: ["ghosty files ls <agent-id> [dir]", "ghosty files get <agent-id> <path> [--out LOCAL]", "ghosty files put <agent-id> <path> [--file LOCAL | < stdin]", "ghosty files rm <agent-id> <path> [--yes]"],
|
|
1641
|
+
examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
|
|
1642
|
+
learn: `${DOCS}#files`
|
|
1643
|
+
},
|
|
1644
|
+
mcp: {
|
|
1645
|
+
summary: "MCP servers of an agent. set REPLACES the whole list.",
|
|
1646
|
+
usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
|
|
1647
|
+
examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
|
|
1648
|
+
learn: `${DOCS}#mcp`
|
|
1649
|
+
},
|
|
1650
|
+
try: {
|
|
1651
|
+
summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
|
|
1652
|
+
usage: [
|
|
1653
|
+
'ghosty try <agent-id> "message" [--session ID] [--reset]',
|
|
1654
|
+
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
|
|
1655
|
+
"ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
|
|
1656
|
+
],
|
|
1657
|
+
flags: [
|
|
1658
|
+
["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
|
|
1659
|
+
["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
|
|
1660
|
+
["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
|
|
1661
|
+
["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
|
|
1662
|
+
["--reset", "forget that test session (with --waba also its test thread)"],
|
|
1663
|
+
["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
|
|
1664
|
+
["--out DIR", "replay: where replay.json is written, after every step"],
|
|
1665
|
+
["--max N", "replay: customer turns per conversation (default 15)"],
|
|
1666
|
+
["--concurrency N", "replay: conversations at the same time (default 8, max 30)"],
|
|
1667
|
+
["--resume", "replay: continue from DIR/replay.json, skipping finished steps"]
|
|
1668
|
+
],
|
|
1669
|
+
examples: [
|
|
1670
|
+
'ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"',
|
|
1671
|
+
'ghosty try <id> "cot\xEDzame esto" --waba --session 5210000000009 --media lista.jpg',
|
|
1672
|
+
'ghosty try <id> "\xBFy el env\xEDo?" --waba --session 5210000000009 --history hilo.json --reset',
|
|
1673
|
+
"ghosty try <id> --replay muestra.json --out ./replay --max 5"
|
|
1674
|
+
],
|
|
1675
|
+
notes: [
|
|
1676
|
+
"Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
|
|
1677
|
+
"--whatsapp is now an alias of --waba."
|
|
1678
|
+
],
|
|
1679
|
+
learn: `${DOCS}#try`
|
|
1680
|
+
},
|
|
1681
|
+
chat: {
|
|
1682
|
+
summary: "Stream a conversation; with no message it is interactive.",
|
|
1683
|
+
usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
|
|
1684
|
+
examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
|
|
1685
|
+
learn: `${DOCS}#chat`
|
|
1686
|
+
},
|
|
1687
|
+
schedule: {
|
|
1688
|
+
summary: "Turns that run later, on the agent's own clock.",
|
|
1689
|
+
usage: ['ghosty schedule add <agent-id> "what to do" --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]', "ghosty schedule ls <agent-id>", "ghosty schedule rm <agent-id> <schedule-id>"],
|
|
1690
|
+
examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
|
|
1691
|
+
learn: `${DOCS}#schedule`
|
|
1692
|
+
},
|
|
1693
|
+
me: {
|
|
1694
|
+
summary: "Your account: files and usage.",
|
|
1695
|
+
usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
|
|
1696
|
+
examples: ["ghosty me files ls --kind document"],
|
|
1697
|
+
learn: `${DOCS}#me`
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
HELPS.credentials = {
|
|
1701
|
+
summary: "Provider keys in your account's vault (encrypted; only the last 4 characters come back).",
|
|
1702
|
+
usage: ["ghosty credentials ls", "ghosty credentials set <provider> [--from-env VAR | < stdin]", "ghosty credentials rm <provider> [--yes]"],
|
|
1703
|
+
flags: [
|
|
1704
|
+
["--from-env VAR", "set: read the key from this environment variable (pass its NAME)"],
|
|
1705
|
+
["--yes", "rm: skip the confirmation"]
|
|
1706
|
+
],
|
|
1707
|
+
examples: [
|
|
1708
|
+
"ghosty credentials set elevenlabs # hidden prompt",
|
|
1709
|
+
"ghosty credentials set mercadopago --from-env MP_ACCESS_TOKEN",
|
|
1710
|
+
"op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
|
|
1711
|
+
],
|
|
1712
|
+
notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
|
|
1713
|
+
learn: `${DOCS}#credentials`
|
|
1714
|
+
};
|
|
1715
|
+
HELPS.creds = HELPS.credentials;
|
|
1716
|
+
HELPS.channels = {
|
|
1717
|
+
summary: "Which WhatsApp Business numbers an agent answers. Pair new numbers in the web.",
|
|
1718
|
+
usage: ["ghosty channels whatsapp ls <agent-id>", "ghosty channels whatsapp enable <agent-id> <number> [--dry-run] [--yes]", "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]"],
|
|
1719
|
+
flags: [
|
|
1720
|
+
["-n, --dry-run", "print the plan (+ starts answering, - stops) and change nothing"],
|
|
1721
|
+
["--yes", "enable: skip typing the last 4 digits (required without a terminal)"]
|
|
1722
|
+
],
|
|
1723
|
+
examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
|
|
1724
|
+
notes: ["<number> is its id, its label or its last digits. One number, one agent: enabling it takes it from the other agent.", "Disabling keeps logging messages; nobody answers them."],
|
|
1725
|
+
learn: `${DOCS}#channels`
|
|
1726
|
+
};
|
|
1727
|
+
HELPS.board = {
|
|
1728
|
+
summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
|
|
1729
|
+
usage: ["ghosty board archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"],
|
|
1730
|
+
flags: [
|
|
1731
|
+
["--integration ID", "cards from this channel/integration (e.g. prueba)"],
|
|
1732
|
+
["--column NAME", "cards in this column"],
|
|
1733
|
+
["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
|
|
1734
|
+
["-n, --dry-run", "count and show a sample, change nothing"],
|
|
1735
|
+
["--yes", "skip the confirmation (required without a terminal)"]
|
|
1736
|
+
],
|
|
1737
|
+
examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
1738
|
+
notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
1739
|
+
learn: `${DOCS}#board`
|
|
1740
|
+
};
|
|
1741
|
+
HELPS.conversations = HELPS.chat;
|
|
1742
|
+
HELPS.usage = HELPS.me;
|
|
1743
|
+
function helpText(cmd) {
|
|
1744
|
+
const h = HELPS[cmd];
|
|
1745
|
+
if (!h) return void 0;
|
|
1746
|
+
const section = (t, lines) => [bold(t), ...lines.map((l) => ` ${l}`), ""];
|
|
1747
|
+
const w = Math.max(0, ...(h.flags ?? []).map(([f]) => f.length)) + 2;
|
|
1748
|
+
return [
|
|
1749
|
+
h.summary,
|
|
1750
|
+
"",
|
|
1751
|
+
...section("USAGE", h.usage),
|
|
1752
|
+
...h.flags?.length ? section("FLAGS", h.flags.map(([f, d]) => `${f.padEnd(w)}${d}`)) : [],
|
|
1753
|
+
...section("EXAMPLES", h.examples.map((e) => `$ ${e}`)),
|
|
1754
|
+
...h.notes?.length ? section("NOTES", h.notes) : [],
|
|
1755
|
+
bold("LEARN MORE"),
|
|
1756
|
+
` Add --json for machine output. Exit codes: 0 ok \xB7 1 API error \xB7 2 usage \xB7 3 not signed in.`,
|
|
1757
|
+
` ${dim(h.learn ?? DOCS)}`
|
|
1758
|
+
].join("\n");
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1015
1761
|
// src/index.ts
|
|
1016
1762
|
var HELP = `${bold("Usage:")} ghosty <command> [options]
|
|
1017
1763
|
|
|
@@ -1028,20 +1774,29 @@ ${bold("Agents")}
|
|
|
1028
1774
|
agents get <id> Show an agent's config [--fields name,model,prompt]
|
|
1029
1775
|
agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
1030
1776
|
[--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
|
|
1031
|
-
|
|
1777
|
+
[--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
|
|
1778
|
+
agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
|
|
1779
|
+
agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
|
|
1780
|
+
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
1032
1781
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
1033
1782
|
agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
|
|
1034
|
-
[--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off]
|
|
1783
|
+
[--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
|
|
1035
1784
|
|
|
1036
1785
|
${bold("Configure an agent")}
|
|
1037
1786
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
1038
|
-
skills ls|add|rm <id> [slug]
|
|
1787
|
+
skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; rm keeps a copy)
|
|
1788
|
+
dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
|
|
1039
1789
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
1790
|
+
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
1791
|
+
board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
1040
1792
|
|
|
1041
1793
|
${bold("Talk to an agent")}
|
|
1042
|
-
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
1794
|
+
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
1795
|
+
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
|
|
1796
|
+
--replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
|
|
1043
1797
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
1044
1798
|
conversations ls <id> List conversations
|
|
1799
|
+
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
1045
1800
|
|
|
1046
1801
|
${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
|
|
1047
1802
|
schedule ls <id> Pending turns and recent runs
|
|
@@ -1053,18 +1808,26 @@ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see:
|
|
|
1053
1808
|
|
|
1054
1809
|
${bold("Your account")}
|
|
1055
1810
|
me files ls|upload|rm Files in your account [--kind image|video|audio|document|other]
|
|
1811
|
+
credentials ls|set|rm [provider] Provider keys in your vault (hidden prompt, stdin or --from-env VAR; never as an argument)
|
|
1056
1812
|
docs [topic] Open the docs
|
|
1057
1813
|
|
|
1058
1814
|
${bold("Global options")}
|
|
1059
1815
|
--json Machine-readable output (stdout is JSON only)
|
|
1060
1816
|
--token <token> Use this token (or env GHOSTY_TOKEN). A gat_\u2026 agent token reaches only that agent.
|
|
1061
1817
|
--profile <name> Use a separate saved session (or env GHOSTY_PROFILE), e.g. one per account
|
|
1062
|
-
|
|
1818
|
+
--yes Skip confirmations (required for rm without a terminal)
|
|
1819
|
+
-h, --help Show help (also: ghosty <command> --help, ghosty help <command>)
|
|
1063
1820
|
-v, --version Show version
|
|
1064
1821
|
|
|
1065
1822
|
${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signed in
|
|
1066
1823
|
${dim("Note: `ghosty` may collide with another binary on your PATH (e.g. the Ghosty TUI). Check with `which -a ghosty`; run `npx @ghostystudio/cli` to be sure.")}
|
|
1067
1824
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
1825
|
+
var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
|
|
1826
|
+
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo"];
|
|
1827
|
+
function unknown(what, input, known, helpCmd) {
|
|
1828
|
+
const s = suggest(input, known);
|
|
1829
|
+
return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
|
|
1830
|
+
}
|
|
1068
1831
|
function showHelp() {
|
|
1069
1832
|
if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
|
|
1070
1833
|
process.stdout.write(HELP + "\n");
|
|
@@ -1093,6 +1856,13 @@ async function main() {
|
|
|
1093
1856
|
session: { type: "string" },
|
|
1094
1857
|
reset: { type: "boolean" },
|
|
1095
1858
|
whatsapp: { type: "boolean" },
|
|
1859
|
+
waba: { type: "boolean" },
|
|
1860
|
+
media: { type: "string", multiple: true },
|
|
1861
|
+
history: { type: "string" },
|
|
1862
|
+
replay: { type: "string" },
|
|
1863
|
+
max: { type: "string" },
|
|
1864
|
+
concurrency: { type: "string" },
|
|
1865
|
+
resume: { type: "boolean" },
|
|
1096
1866
|
conversation: { type: "string" },
|
|
1097
1867
|
kind: { type: "string" },
|
|
1098
1868
|
at: { type: "string" },
|
|
@@ -1116,16 +1886,43 @@ async function main() {
|
|
|
1116
1886
|
welcome: { type: "string" },
|
|
1117
1887
|
chips: { type: "string" },
|
|
1118
1888
|
starter: { type: "string" },
|
|
1119
|
-
|
|
1889
|
+
rotate: { type: "boolean" },
|
|
1890
|
+
sponsor: { type: "string" },
|
|
1891
|
+
yes: { type: "boolean", short: "y" },
|
|
1892
|
+
dir: { type: "string" },
|
|
1893
|
+
arg: { type: "string", multiple: true },
|
|
1894
|
+
write: { type: "boolean" },
|
|
1895
|
+
"external-write": { type: "string" },
|
|
1896
|
+
"no-deliver": { type: "boolean" },
|
|
1897
|
+
deliver: { type: "boolean" },
|
|
1898
|
+
errors: { type: "boolean" },
|
|
1899
|
+
status: { type: "string" },
|
|
1900
|
+
since: { type: "string" },
|
|
1901
|
+
limit: { type: "string" },
|
|
1902
|
+
verbose: { type: "boolean" },
|
|
1903
|
+
voice: { type: "string" },
|
|
1904
|
+
"voice-replies": { type: "string" },
|
|
1905
|
+
"auto-stage": { type: "string" },
|
|
1906
|
+
"from-env": { type: "string" },
|
|
1907
|
+
"dry-run": { type: "boolean", short: "n" },
|
|
1908
|
+
integration: { type: "string" },
|
|
1909
|
+
from: { type: "string" },
|
|
1910
|
+
tables: { type: "string" },
|
|
1911
|
+
append: { type: "boolean" },
|
|
1912
|
+
column: { type: "string" },
|
|
1913
|
+
before: { type: "string" }
|
|
1120
1914
|
}
|
|
1121
1915
|
});
|
|
1122
1916
|
globals.json = Boolean(v.json);
|
|
1123
1917
|
const tokens = splitTokens(pos, v);
|
|
1124
1918
|
globals.token = tokens.session;
|
|
1125
1919
|
globals.profile = v.profile;
|
|
1126
|
-
if (v.version) return out({ version: "0.
|
|
1920
|
+
if (v.version) return out({ version: "0.9.0" }, () => "0.9.0");
|
|
1127
1921
|
const [cmd, sub, a1, a2] = pos;
|
|
1128
1922
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1923
|
+
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
1924
|
+
if (topic === "partner") return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1925
|
+
if (topic && helpText(topic)) return out({ help: helpText(topic) }, () => helpText(topic));
|
|
1129
1926
|
if (!cmd || v.help || cmd === "help") return showHelp();
|
|
1130
1927
|
switch (cmd) {
|
|
1131
1928
|
case "login": {
|
|
@@ -1151,16 +1948,30 @@ async function main() {
|
|
|
1151
1948
|
if (sub === "create" || sub === "new") return agentsCreate(v);
|
|
1152
1949
|
if (sub === "get") return agentsGet(a1, v.fields);
|
|
1153
1950
|
if (sub === "set") return agentsSet(a1, v);
|
|
1154
|
-
if (sub === "rm" || sub === "delete") return agentsRm(a1);
|
|
1951
|
+
if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
|
|
1952
|
+
if (sub === "doctor") return agentsDoctor(a1, v);
|
|
1953
|
+
if (sub === "docs" || sub === "documents") return agentDocsCmd(a1, a2, pos[4], v);
|
|
1155
1954
|
if (sub === "restart") return agentsRestart(a1);
|
|
1156
1955
|
if (sub === "demo") return agentsDemo(a1, v);
|
|
1157
|
-
throw
|
|
1956
|
+
throw unknown(`agents ${sub}`, sub, AGENTS_SUBS, "ghosty agents --help");
|
|
1158
1957
|
case "files":
|
|
1159
1958
|
return filesCmd(sub, a1, a2, v);
|
|
1160
1959
|
case "skills":
|
|
1161
1960
|
return skillsCmd(sub, a1, a2, v);
|
|
1162
1961
|
case "mcp":
|
|
1163
1962
|
return mcpCmd(sub, a1, v);
|
|
1963
|
+
case "dbs":
|
|
1964
|
+
case "db":
|
|
1965
|
+
return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
|
|
1966
|
+
case "turns":
|
|
1967
|
+
return turnsCmd(sub, a1, v);
|
|
1968
|
+
case "board":
|
|
1969
|
+
return boardCmd(sub, a1, v);
|
|
1970
|
+
case "channels":
|
|
1971
|
+
return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
|
|
1972
|
+
case "credentials":
|
|
1973
|
+
case "creds":
|
|
1974
|
+
return credentialsCmd2(sub, a1, v);
|
|
1164
1975
|
case "try":
|
|
1165
1976
|
return tryTurn(sub, a1, v);
|
|
1166
1977
|
case "chat":
|
|
@@ -1185,7 +1996,7 @@ async function main() {
|
|
|
1185
1996
|
return openUrl(url);
|
|
1186
1997
|
}
|
|
1187
1998
|
default:
|
|
1188
|
-
throw
|
|
1999
|
+
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
1189
2000
|
}
|
|
1190
2001
|
}
|
|
1191
2002
|
main().then(
|