@ghostystudio/cli 0.7.1 → 0.8.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 +17 -1
- package/dist/index.js +669 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,9 +18,16 @@ ghosty agents ls
|
|
|
18
18
|
ghosty agents get <id> [--fields name,model,prompt]
|
|
19
19
|
ghosty agents set <id> [--name] [--model] [--prompt | --prompt-file FILE]
|
|
20
20
|
ghosty agents restart <id>
|
|
21
|
+
ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
|
|
22
|
+
ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
|
|
23
|
+
ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
|
|
21
24
|
|
|
22
25
|
ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
|
|
23
|
-
ghosty skills ls|add|rm <id> [slug]
|
|
26
|
+
ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR]
|
|
27
|
+
ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
|
|
28
|
+
ghosty turns ls <id> [--errors] [--since 1h] [--limit N]
|
|
29
|
+
ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
|
|
30
|
+
ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
|
|
24
31
|
ghosty mcp get|set <id> [--file servers.json]
|
|
25
32
|
|
|
26
33
|
ghosty try <id> "message" [--session] [--reset] [--whatsapp]
|
|
@@ -32,11 +39,20 @@ ghosty schedule ls <id>
|
|
|
32
39
|
ghosty schedule rm <id> <schedule-id>
|
|
33
40
|
|
|
34
41
|
ghosty me files ls|upload|rm [--kind image|video|audio|document|other]
|
|
42
|
+
ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argument
|
|
35
43
|
ghosty docs
|
|
36
44
|
```
|
|
37
45
|
|
|
38
46
|
Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--help`, `--version`.
|
|
39
47
|
|
|
48
|
+
Every command has its own help with examples: `ghosty <command> --help` or `ghosty help <command>`.
|
|
49
|
+
|
|
40
50
|
Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
41
51
|
|
|
52
|
+
### Changes in 0.8.0
|
|
53
|
+
|
|
54
|
+
- Destructive commands (`agents rm`, `files rm`, `skills rm`, `credentials rm`, `board archive`, `dbs import` over existing rows, `channels whatsapp enable`) ask first. **Without a terminal they need `--yes`** or exit `2`.
|
|
55
|
+
- `skills rm` keeps a local copy under `~/.config/ghosty/trash/` and prints the undo command.
|
|
56
|
+
- New: `agents doctor`, `agents docs`, `dbs`, `turns`, `channels whatsapp`, `board archive`, `credentials`, `skills get`, `skills add --dir`, voice and auto-stage flags on `agents set`, per-command help and "did you mean".
|
|
57
|
+
|
|
42
58
|
Requires Node 22+. Docs: https://www.ghosty.studio/docs/cli · Skill for coding agents: `npx skills add https://www.ghosty.studio`
|
package/dist/index.js
CHANGED
|
@@ -290,7 +290,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
290
290
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
291
291
|
headers: {
|
|
292
292
|
authorization: `Bearer ${token}`,
|
|
293
|
-
"user-agent": `ghosty-cli/${"0.
|
|
293
|
+
"user-agent": `ghosty-cli/${"0.8.0"}`,
|
|
294
294
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
295
295
|
...opts.headers
|
|
296
296
|
},
|
|
@@ -313,7 +313,7 @@ async function toError(res) {
|
|
|
313
313
|
}
|
|
314
314
|
const detail = body.message || body.error_description || (body.detalle && body.error ? `${body.error}: ${body.detalle}` : body.error) || text.slice(0, 300) || res.statusText;
|
|
315
315
|
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, "
|
|
316
|
+
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
317
|
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
318
|
if (body.error === "restart_no_aplica" || body.error === "agente_sin_maquina") {
|
|
319
319
|
return new CliError("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", EXIT.API);
|
|
@@ -322,7 +322,44 @@ async function toError(res) {
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
// src/commands/agents.ts
|
|
325
|
-
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
325
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
326
|
+
import { homedir as homedir2 } from "node:os";
|
|
327
|
+
import { dirname as dirname2, join as join2, relative } from "node:path";
|
|
328
|
+
|
|
329
|
+
// src/prompt.ts
|
|
330
|
+
import { createInterface } from "node:readline/promises";
|
|
331
|
+
var canPrompt = () => Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
332
|
+
function requireYesIfHeadless(yes, retry) {
|
|
333
|
+
if (!yes && !canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${retry} --yes`);
|
|
334
|
+
}
|
|
335
|
+
async function confirm(question, opts) {
|
|
336
|
+
if (opts.yes) return;
|
|
337
|
+
if (!canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${opts.retry} --yes`);
|
|
338
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
339
|
+
try {
|
|
340
|
+
const ask = opts.expect ? `${question}
|
|
341
|
+
Type ${bold(opts.expect)} to confirm: ` : `${question} [y/N] `;
|
|
342
|
+
const a = (await rl.question(ask)).trim();
|
|
343
|
+
const ok = opts.expect ? a === opts.expect : /^y(es)?$/i.test(a);
|
|
344
|
+
if (!ok) throw usage("Cancelled.");
|
|
345
|
+
} finally {
|
|
346
|
+
rl.close();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function distance(a, b) {
|
|
350
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
351
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
352
|
+
for (let i = 1; i <= a.length; i++)
|
|
353
|
+
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));
|
|
354
|
+
return d[a.length][b.length];
|
|
355
|
+
}
|
|
356
|
+
function suggest(input, known) {
|
|
357
|
+
const best = known.map((k) => [k, distance(input, k)]).sort((x, y) => x[1] - y[1])[0];
|
|
358
|
+
if (best && best[1] <= 2) return best[0];
|
|
359
|
+
return known.find((k) => k.startsWith(input) && input.length >= 2);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/commands/agents.ts
|
|
326
363
|
var enc = encodeURIComponent;
|
|
327
364
|
var agentPath = (id, rest = "") => `/api/v2/agents/${enc(id)}${rest}`;
|
|
328
365
|
var need = (v, what, example) => {
|
|
@@ -383,12 +420,33 @@ async function agentsSet(id, v) {
|
|
|
383
420
|
for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
|
|
384
421
|
for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
|
|
385
422
|
if (Object.keys(connectors).length) body.connectors = connectors;
|
|
386
|
-
|
|
423
|
+
const voice = {};
|
|
424
|
+
if (v.voice) {
|
|
425
|
+
const [engine, ...rest] = v.voice.split(":");
|
|
426
|
+
if (engine !== "elevenlabs" && engine !== "kokoro" || !rest.join(":")) throw usage(`Bad --voice: ${v.voice}.`, "Use elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora");
|
|
427
|
+
voice.engine = engine;
|
|
428
|
+
voice.voiceId = rest.join(":");
|
|
429
|
+
}
|
|
430
|
+
if (v["voice-replies"]) {
|
|
431
|
+
if (v["voice-replies"] !== "auto" && v["voice-replies"] !== "off") throw usage("--voice-replies takes auto or off.");
|
|
432
|
+
voice.replies = v["voice-replies"];
|
|
433
|
+
}
|
|
434
|
+
if (Object.keys(voice).length) body.voice = voice;
|
|
435
|
+
if (v["auto-stage"]) {
|
|
436
|
+
if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
|
|
437
|
+
body.autoStage = v["auto-stage"] === "on";
|
|
438
|
+
}
|
|
439
|
+
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
440
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
388
441
|
out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
|
|
389
442
|
}
|
|
390
|
-
async function agentsRm(id) {
|
|
443
|
+
async function agentsRm(id, v = {}) {
|
|
391
444
|
id = need(id, "agent id", "ghosty agents rm <agent-id>");
|
|
445
|
+
requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
|
|
446
|
+
if (!v.yes) {
|
|
447
|
+
const a = await api(agentPath(id, "?fields=name"));
|
|
448
|
+
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}` });
|
|
449
|
+
}
|
|
392
450
|
out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
|
|
393
451
|
}
|
|
394
452
|
async function agentsRestart(id) {
|
|
@@ -437,6 +495,7 @@ ${table(r.files ?? [], cols)}`);
|
|
|
437
495
|
}
|
|
438
496
|
case "rm": {
|
|
439
497
|
need(path, "path", ex);
|
|
498
|
+
await confirm(`Delete ${p} from the agent's machine?`, { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
|
|
440
499
|
out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} Deleted ${p}`);
|
|
441
500
|
return;
|
|
442
501
|
}
|
|
@@ -450,11 +509,38 @@ async function readStdin() {
|
|
|
450
509
|
for await (const c of process.stdin) parts.push(c);
|
|
451
510
|
return Buffer.concat(parts);
|
|
452
511
|
}
|
|
512
|
+
async function writeSkill(dir, sk) {
|
|
513
|
+
let bytes = 0;
|
|
514
|
+
for (const f of sk.files) {
|
|
515
|
+
const full = join2(dir, f.path);
|
|
516
|
+
if (relative(dir, full).startsWith("..")) continue;
|
|
517
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
518
|
+
const b = Buffer.from(f.contentBase64, "base64");
|
|
519
|
+
await writeFile2(full, b);
|
|
520
|
+
bytes += b.length;
|
|
521
|
+
}
|
|
522
|
+
return bytes;
|
|
523
|
+
}
|
|
524
|
+
async function readSkillDir(dir) {
|
|
525
|
+
const files = (await readdir(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile());
|
|
526
|
+
let markdown;
|
|
527
|
+
const assets = [];
|
|
528
|
+
for (const e of files) {
|
|
529
|
+
const full = join2(e.parentPath, e.name);
|
|
530
|
+
const rel = relative(dir, full).split("\\").join("/");
|
|
531
|
+
if (rel === "SKILL.md") markdown = await readFile2(full, "utf8");
|
|
532
|
+
else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await readFile2(full)).toString("base64") });
|
|
533
|
+
}
|
|
534
|
+
if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
|
|
535
|
+
return { markdown, assets };
|
|
536
|
+
}
|
|
537
|
+
var trashDir = (id, slug) => join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
453
538
|
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>";
|
|
539
|
+
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
540
|
id = need(id, "agent id", ex);
|
|
456
|
-
switch (sub) {
|
|
457
|
-
case "ls":
|
|
541
|
+
switch (sub ?? "ls") {
|
|
542
|
+
case "ls":
|
|
543
|
+
case "list": {
|
|
458
544
|
const r = await api(agentPath(id, "/skills"));
|
|
459
545
|
const short2 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
|
|
460
546
|
const rows = [
|
|
@@ -463,22 +549,42 @@ async function skillsCmd(sub, id, slug, v) {
|
|
|
463
549
|
];
|
|
464
550
|
const more = r.community.filter((c) => !c.installed).length;
|
|
465
551
|
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)`)}`);
|
|
552
|
+
${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
case "get": {
|
|
556
|
+
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
557
|
+
const sk = await api(agentPath(id, `/skills/${enc(slug)}`));
|
|
558
|
+
const dir = v.out ?? slug;
|
|
559
|
+
const bytes = await writeSkill(dir, sk);
|
|
560
|
+
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
561
|
return;
|
|
468
562
|
}
|
|
469
563
|
case "add": {
|
|
470
564
|
slug = need(slug, "skill slug", ex);
|
|
471
|
-
|
|
565
|
+
if (v.file && v.dir) throw usage("Use --file or --dir, not both.");
|
|
566
|
+
const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await readFile2(v.file, "utf8") } : { fromCatalog: true };
|
|
472
567
|
const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "PUT", body });
|
|
473
568
|
out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
|
|
474
569
|
return;
|
|
475
570
|
}
|
|
476
571
|
case "rm":
|
|
572
|
+
case "remove": {
|
|
477
573
|
slug = need(slug, "skill slug", ex);
|
|
478
|
-
|
|
574
|
+
requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
|
|
575
|
+
const sk = await api(agentPath(id, `/skills/${enc(slug)}`)).catch(() => null);
|
|
576
|
+
await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
|
|
577
|
+
const backup = sk ? trashDir(id, slug) : null;
|
|
578
|
+
if (sk && backup) await writeSkill(backup, sk);
|
|
579
|
+
const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
|
|
580
|
+
const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
|
|
581
|
+
out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
|
|
582
|
+
${dim(`Backup: ${backup}
|
|
583
|
+
Undo: ${restore}`)}` : ""}`);
|
|
479
584
|
return;
|
|
585
|
+
}
|
|
480
586
|
default:
|
|
481
|
-
throw usage(`Unknown skills subcommand: ${sub
|
|
587
|
+
throw usage(`Unknown skills subcommand: ${sub}.`, ex);
|
|
482
588
|
}
|
|
483
589
|
}
|
|
484
590
|
async function mcpCmd(sub, id, v) {
|
|
@@ -502,9 +608,11 @@ async function agentsDemo(id, v) {
|
|
|
502
608
|
if (v.on) body.enabled = true;
|
|
503
609
|
if (v.off) body.enabled = false;
|
|
504
610
|
if (v.turns) body.maxTurns = Number(v.turns);
|
|
611
|
+
if (v.hours) body.trialHours = Number(v.hours);
|
|
505
612
|
if (v["per-ip"]) body.perIpDay = Number(v["per-ip"]);
|
|
506
613
|
if (v.welcome !== void 0) body.welcome = v.welcome;
|
|
507
614
|
if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
|
|
615
|
+
if (v.starter !== void 0) body.starter = v.starter || null;
|
|
508
616
|
if (v.sponsor !== void 0) {
|
|
509
617
|
if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
|
|
510
618
|
body.sponsor = v.sponsor === "on";
|
|
@@ -514,8 +622,9 @@ async function agentsDemo(id, v) {
|
|
|
514
622
|
d,
|
|
515
623
|
() => [
|
|
516
624
|
`${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
|
|
517
|
-
dim(`
|
|
625
|
+
dim(`trial: ${d.trialHours} h from the first message \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
|
|
518
626
|
d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
|
|
627
|
+
d.starter ? `starter: \u2728 ${d.starter}` : dim("starter: (none)"),
|
|
519
628
|
d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
|
|
520
629
|
`tried ${d.stats.pruebas} \xB7 messages ${d.stats.mensajes} \xB7 kept ${d.stats.clones} (${d.stats.patrocinados} on your key)`,
|
|
521
630
|
...d.lastError ? [red(`last failure ${d.lastError.at}: ${d.lastError.message ?? d.lastError.error}`)] : []
|
|
@@ -525,7 +634,7 @@ async function agentsDemo(id, v) {
|
|
|
525
634
|
|
|
526
635
|
// src/commands/chat.ts
|
|
527
636
|
import { randomUUID } from "node:crypto";
|
|
528
|
-
import { createInterface } from "node:readline/promises";
|
|
637
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
529
638
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
530
639
|
import { basename } from "node:path";
|
|
531
640
|
|
|
@@ -635,7 +744,7 @@ async function chat(agent, message, v) {
|
|
|
635
744
|
await turn(agent, sid, piped, first);
|
|
636
745
|
return;
|
|
637
746
|
}
|
|
638
|
-
const rl =
|
|
747
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
639
748
|
info(dim("Type your message. Ctrl-C cancels a turn, Ctrl-D exits."));
|
|
640
749
|
for (; ; ) {
|
|
641
750
|
let line;
|
|
@@ -844,7 +953,7 @@ async function call(path, v, init = {}) {
|
|
|
844
953
|
"x-ghosty-ts": ts,
|
|
845
954
|
"x-ghosty-sig": sig,
|
|
846
955
|
"content-type": "application/json",
|
|
847
|
-
"user-agent": `ghosty-cli/${"0.
|
|
956
|
+
"user-agent": `ghosty-cli/${"0.8.0"}`
|
|
848
957
|
},
|
|
849
958
|
body: method === "GET" ? void 0 : raw
|
|
850
959
|
});
|
|
@@ -1009,6 +1118,484 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
1009
1118
|
}
|
|
1010
1119
|
}
|
|
1011
1120
|
|
|
1121
|
+
// src/commands/data.ts
|
|
1122
|
+
var enc5 = encodeURIComponent;
|
|
1123
|
+
var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc5(id)}${rest}`;
|
|
1124
|
+
var need2 = (v, what, example) => {
|
|
1125
|
+
if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
|
|
1126
|
+
return v;
|
|
1127
|
+
};
|
|
1128
|
+
var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
1129
|
+
function planText(plan) {
|
|
1130
|
+
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");
|
|
1131
|
+
}
|
|
1132
|
+
async function dbsImport(id, db, v) {
|
|
1133
|
+
const ex = "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--dry-run]";
|
|
1134
|
+
const m = /^easybits:(.+)$/.exec(v.from ?? "");
|
|
1135
|
+
if (!m) throw usage("Missing or bad --from.", `Example: ${ex}`);
|
|
1136
|
+
const easybitsKey = process.env.EASYBITS_API_KEY;
|
|
1137
|
+
if (!easybitsKey) throw usage("EASYBITS_API_KEY is not set.", "Export your EasyBits API key (https://www.easybits.cloud/dash/developer) and run again.");
|
|
1138
|
+
const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {} };
|
|
1139
|
+
const path = agentPath2(id, `/dbs/${enc5(db)}/import`);
|
|
1140
|
+
const dry = await api(path, { body: { ...body, dryRun: true } });
|
|
1141
|
+
if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
|
|
1142
|
+
${planText(dry.plan)}
|
|
1143
|
+
${dim("Dry run: nothing changed.")}`);
|
|
1144
|
+
const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
|
|
1145
|
+
if (destructive) await confirm(`${planText(dry.plan)}
|
|
1146
|
+
Replace the rows above in ${dry.db}?`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
|
|
1147
|
+
const r = await api(path, { body });
|
|
1148
|
+
return out(r, () => `${planText(r.plan)}
|
|
1149
|
+
${lavender("\u2713")} Imported into ${r.db}. ${dim("Run it again any time: with replace it ends in the same state.")}`);
|
|
1150
|
+
}
|
|
1151
|
+
async function dbsCmd(sub, id, db, rest, v) {
|
|
1152
|
+
const ex = "ghosty dbs ls <agent-id>";
|
|
1153
|
+
id = need2(id, "agent id", ex);
|
|
1154
|
+
switch (sub ?? "ls") {
|
|
1155
|
+
case "ls":
|
|
1156
|
+
case "list": {
|
|
1157
|
+
const r = await api(agentPath2(id, "/dbs"));
|
|
1158
|
+
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 }));
|
|
1159
|
+
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."));
|
|
1160
|
+
}
|
|
1161
|
+
case "tables": {
|
|
1162
|
+
db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
|
|
1163
|
+
const r = await api(agentPath2(id, `/dbs/${enc5(db)}`));
|
|
1164
|
+
const ext = new Set(r.externalWriteTables);
|
|
1165
|
+
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(", ") }));
|
|
1166
|
+
return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
|
|
1167
|
+
${table(rows, ["table", "rows", "customer writes", "columns"])}`);
|
|
1168
|
+
}
|
|
1169
|
+
case "query":
|
|
1170
|
+
case "sql": {
|
|
1171
|
+
db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
|
|
1172
|
+
const sql = need2(rest.join(" ").trim() || void 0, "SQL", 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
|
|
1173
|
+
const r = await api(agentPath2(id, `/dbs/${enc5(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
|
|
1174
|
+
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.");
|
|
1175
|
+
throw e;
|
|
1176
|
+
});
|
|
1177
|
+
return out(r, () => {
|
|
1178
|
+
if (!r.cols.length) return `${lavender("\u2713")} ${r.affected} row(s) affected ${dim(`(${Math.round(r.ms)} ms)`)}`;
|
|
1179
|
+
const rows = r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]])));
|
|
1180
|
+
return `${table(rows, r.cols)}
|
|
1181
|
+
${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
case "grant": {
|
|
1185
|
+
db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
|
|
1186
|
+
const body = v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {};
|
|
1187
|
+
const r = await api(agentPath2(id, `/dbs/${enc5(db)}`), { method: "PUT", body });
|
|
1188
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
|
|
1189
|
+
}
|
|
1190
|
+
case "import": {
|
|
1191
|
+
db = need2(db, "database (id or name)", "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
|
|
1192
|
+
return dbsImport(id, db, v);
|
|
1193
|
+
}
|
|
1194
|
+
case "revoke": {
|
|
1195
|
+
db = need2(db, "database (id or name)", "ghosty dbs revoke <agent-id> <db>");
|
|
1196
|
+
const r = await api(agentPath2(id, `/dbs/${enc5(db)}`), { method: "DELETE" });
|
|
1197
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
|
|
1198
|
+
}
|
|
1199
|
+
default:
|
|
1200
|
+
throw usage(`Unknown dbs subcommand: ${sub}.`, "Try: ghosty dbs ls | tables | query | import | grant | revoke (ghosty dbs --help)");
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
async function agentDocsCmd(sub, id, doc, v) {
|
|
1204
|
+
id = need2(id, "agent id", "ghosty agents docs ls <agent-id>");
|
|
1205
|
+
switch (sub ?? "ls") {
|
|
1206
|
+
case "ls":
|
|
1207
|
+
case "list": {
|
|
1208
|
+
const r = await api(agentPath2(id, "/documents"));
|
|
1209
|
+
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 }));
|
|
1210
|
+
return out(r.documents, () => rows.length ? table(rows, ["name", "access", "delivers", "id"]) : dim("This account has no documents yet: upload them at /app/datos."));
|
|
1211
|
+
}
|
|
1212
|
+
case "grant": {
|
|
1213
|
+
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf"');
|
|
1214
|
+
const body = {};
|
|
1215
|
+
if (v.write) body.mode = "write";
|
|
1216
|
+
if (v["no-deliver"]) body.deliverable = false;
|
|
1217
|
+
else if (v.deliver) body.deliverable = true;
|
|
1218
|
+
const r = await api(agentPath2(id, `/documents/${enc5(doc)}`), { method: "PUT", body });
|
|
1219
|
+
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"}.`);
|
|
1220
|
+
}
|
|
1221
|
+
case "revoke": {
|
|
1222
|
+
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
|
|
1223
|
+
const r = await api(agentPath2(id, `/documents/${enc5(doc)}`), { method: "DELETE" });
|
|
1224
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
|
|
1225
|
+
}
|
|
1226
|
+
default:
|
|
1227
|
+
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function turnsCmd(sub, id, v) {
|
|
1231
|
+
if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h");
|
|
1232
|
+
id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
|
|
1233
|
+
const q = new URLSearchParams();
|
|
1234
|
+
const status = v.errors ? "error" : v.status;
|
|
1235
|
+
if (status) q.set("state", status);
|
|
1236
|
+
if (v.since) q.set("since", v.since);
|
|
1237
|
+
q.set("limit", v.limit ?? "20");
|
|
1238
|
+
const r = await api(agentPath2(id, `/turns?${q}`));
|
|
1239
|
+
const when = (s) => s.slice(0, 16).replace("T", " ");
|
|
1240
|
+
const rows = r.turns.map((t) => ({
|
|
1241
|
+
started: when(t.startedAt),
|
|
1242
|
+
state: t.state === "error" ? red(t.state) : t.state,
|
|
1243
|
+
secs: t.ms == null ? "" : Math.round(t.ms / 1e3),
|
|
1244
|
+
from: t.surface ?? "",
|
|
1245
|
+
session: t.sessionId.slice(0, 24),
|
|
1246
|
+
error: (t.error ?? "").replace(/\s+/g, " ").slice(0, 90)
|
|
1247
|
+
}));
|
|
1248
|
+
const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
|
|
1249
|
+
out(r, () => rows.length ? `${table(rows, ["started", "state", "secs", "from", "session", "error"])}
|
|
1250
|
+
${dim(`${v.since ? `since ${v.since}: ` : ""}${tally}`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
|
|
1251
|
+
}
|
|
1252
|
+
async function agentsDoctor(id, v) {
|
|
1253
|
+
id = need2(id, "agent id", "ghosty agents doctor <agent-id>");
|
|
1254
|
+
const r = await api(agentPath2(id, "/doctor"));
|
|
1255
|
+
const problems = r.summary.warn + r.summary.error;
|
|
1256
|
+
if (globals.json) out(r);
|
|
1257
|
+
else {
|
|
1258
|
+
const mark = { ok: lavender("\u2713"), warn: "\x1B[33m!\x1B[0m", error: red("\u2717") };
|
|
1259
|
+
const lines = [];
|
|
1260
|
+
for (const c of r.checks) {
|
|
1261
|
+
if (c.level === "ok" && !v.verbose) continue;
|
|
1262
|
+
lines.push(`${process.stdout.isTTY ? mark[c.level] : { ok: "\u2713", warn: "!", error: "\u2717" }[c.level]} ${c.title}`);
|
|
1263
|
+
if (c.detail && c.level !== "ok") lines.push(...c.detail.split("\n").map((l) => ` ${dim(l)}`));
|
|
1264
|
+
if (c.fix && c.level !== "ok") lines.push(` ${dim("fix:")} ${c.fix}`);
|
|
1265
|
+
}
|
|
1266
|
+
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).`);
|
|
1267
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1268
|
+
}
|
|
1269
|
+
if (problems) {
|
|
1270
|
+
if (!globals.json) info(dim("Exit 1: problems found."));
|
|
1271
|
+
process.exit(EXIT.API);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/commands/credentials.ts
|
|
1276
|
+
async function hiddenPrompt(label) {
|
|
1277
|
+
process.stderr.write(label);
|
|
1278
|
+
const stdin = process.stdin;
|
|
1279
|
+
stdin.setRawMode(true);
|
|
1280
|
+
stdin.resume();
|
|
1281
|
+
stdin.setEncoding("utf8");
|
|
1282
|
+
return new Promise((resolve, reject) => {
|
|
1283
|
+
let value = "";
|
|
1284
|
+
const onData = (chunk) => {
|
|
1285
|
+
for (const ch of chunk) {
|
|
1286
|
+
if (ch === "\r" || ch === "\n") {
|
|
1287
|
+
stdin.setRawMode(false);
|
|
1288
|
+
stdin.pause();
|
|
1289
|
+
stdin.off("data", onData);
|
|
1290
|
+
process.stderr.write("\n");
|
|
1291
|
+
return resolve(value);
|
|
1292
|
+
}
|
|
1293
|
+
if (ch === "") {
|
|
1294
|
+
stdin.setRawMode(false);
|
|
1295
|
+
process.stderr.write("\n");
|
|
1296
|
+
return reject(usage("Cancelled."));
|
|
1297
|
+
}
|
|
1298
|
+
if (ch === "\x7F") value = value.slice(0, -1);
|
|
1299
|
+
else value += ch;
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
stdin.on("data", onData);
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
async function readSecret(provider, fromEnv) {
|
|
1306
|
+
if (fromEnv) {
|
|
1307
|
+
const v = process.env[fromEnv];
|
|
1308
|
+
if (!v) throw usage(`$${fromEnv} is empty or not set.`, `Example: export ${fromEnv}=\u2026 && ghosty credentials set ${provider} --from-env ${fromEnv}`);
|
|
1309
|
+
return v;
|
|
1310
|
+
}
|
|
1311
|
+
if (!process.stdin.isTTY) {
|
|
1312
|
+
const parts = [];
|
|
1313
|
+
for await (const c of process.stdin) parts.push(c);
|
|
1314
|
+
return Buffer.concat(parts).toString("utf8").replace(/\r?\n$/, "");
|
|
1315
|
+
}
|
|
1316
|
+
if (!canPrompt()) throw usage("No value to save.", `Pipe it (\u2026 | ghosty credentials set ${provider}) or use --from-env VAR.`);
|
|
1317
|
+
return hiddenPrompt(`${provider} key (hidden): `);
|
|
1318
|
+
}
|
|
1319
|
+
async function credentialsCmd2(sub, provider, v) {
|
|
1320
|
+
switch (sub ?? "ls") {
|
|
1321
|
+
case "ls":
|
|
1322
|
+
case "list": {
|
|
1323
|
+
const r = await api("/api/v2/me/credentials");
|
|
1324
|
+
const rows = r.credentials.map((c) => ({ provider: c.provider, set: c.configured ? `\u2026${c.lastFour ?? ""}` : "\u2014", updated: c.updatedAt?.slice(0, 10) ?? "" }));
|
|
1325
|
+
return out(r.credentials, () => table(rows, ["provider", "set", "updated"]));
|
|
1326
|
+
}
|
|
1327
|
+
case "set": {
|
|
1328
|
+
if (!provider) throw usage("Missing provider.", "Example: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls lists them)");
|
|
1329
|
+
const key = (await readSecret(provider, v["from-env"])).trim();
|
|
1330
|
+
if (!key) throw usage("Empty key: nothing saved.");
|
|
1331
|
+
const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "PUT", body: { key } });
|
|
1332
|
+
return out(r, () => `${lavender("\u2713")} ${provider} saved ${dim(`(\u2026${r.lastFour ?? ""}). Agents with a machine pick it up on their next turn.`)}`);
|
|
1333
|
+
}
|
|
1334
|
+
case "rm":
|
|
1335
|
+
case "remove": {
|
|
1336
|
+
if (!provider) throw usage("Missing provider.", "Example: ghosty credentials rm elevenlabs");
|
|
1337
|
+
await confirm(`Remove your ${provider} key? Agents using it stop working until you set another.`, { yes: v.yes, retry: `ghosty credentials rm ${provider}` });
|
|
1338
|
+
const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "DELETE" });
|
|
1339
|
+
return out(r, () => `${lavender("\u2713")} ${provider} removed.`);
|
|
1340
|
+
}
|
|
1341
|
+
default:
|
|
1342
|
+
throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty credentials ls | set | rm (ghosty credentials --help)");
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// src/commands/channels.ts
|
|
1347
|
+
var enc6 = encodeURIComponent;
|
|
1348
|
+
var digits = (s) => s.replace(/\D/g, "");
|
|
1349
|
+
function find(nums, ref) {
|
|
1350
|
+
const d = digits(ref);
|
|
1351
|
+
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);
|
|
1352
|
+
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).");
|
|
1353
|
+
return hit;
|
|
1354
|
+
}
|
|
1355
|
+
async function channelsCmd(kind, sub, id, ref, v) {
|
|
1356
|
+
if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
|
|
1357
|
+
if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
|
|
1358
|
+
if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
|
|
1359
|
+
const path = `/api/v2/agents/${enc6(id)}/whatsapp`;
|
|
1360
|
+
const cur = await api(path);
|
|
1361
|
+
if (!sub || sub === "ls" || sub === "list") {
|
|
1362
|
+
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 }));
|
|
1363
|
+
return out(cur, () => rows.length ? table(rows, ["number", "answers", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
|
|
1364
|
+
}
|
|
1365
|
+
if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable <agent-id> <number>");
|
|
1366
|
+
if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
|
|
1367
|
+
const n = find(cur.numbers, ref);
|
|
1368
|
+
const on = sub === "enable";
|
|
1369
|
+
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"}.`));
|
|
1370
|
+
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)`;
|
|
1371
|
+
if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
|
|
1372
|
+
${dim("Dry run: nothing changed.")}`);
|
|
1373
|
+
if (on) await confirm(`${bold(plan)}
|
|
1374
|
+
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}` });
|
|
1375
|
+
const wanted = cur.numbers.filter((x) => x.id === n.id ? on : x.mine).map((x) => x.id);
|
|
1376
|
+
const r = await api(path, { method: "PUT", body: { numbers: wanted } });
|
|
1377
|
+
return out(r, () => `${lavender("\u2713")} ${plan.slice(2)}`);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/board.ts
|
|
1381
|
+
async function boardCmd(sub, id, v) {
|
|
1382
|
+
if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1383
|
+
if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1384
|
+
const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
|
|
1385
|
+
if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
|
|
1386
|
+
const retry = `ghosty board archive ${id} ${Object.entries(filter).map(([k, x]) => `--${k} ${JSON.stringify(x)}`).join(" ")}`;
|
|
1387
|
+
if (!v["dry-run"]) requireYesIfHeadless(v.yes, retry);
|
|
1388
|
+
const path = `/api/v2/agents/${encodeURIComponent(id)}/board/archive`;
|
|
1389
|
+
const plan = await api(path, { body: { ...filter, dryRun: true } });
|
|
1390
|
+
const sample = () => table(plan.sample.map((c) => ({ name: c.name ?? "", phone: c.phone, column: c.estado_label ?? "" })), ["name", "phone", "column"]);
|
|
1391
|
+
if (v["dry-run"] || plan.matched === 0) {
|
|
1392
|
+
return out(plan, () => plan.matched ? `${sample()}
|
|
1393
|
+
${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.`));
|
|
1394
|
+
}
|
|
1395
|
+
await confirm(`${sample()}
|
|
1396
|
+
Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new message brings a card back)`, { yes: v.yes, retry });
|
|
1397
|
+
const r = await api(path, { body: filter });
|
|
1398
|
+
out(r, () => `${lavender("\u2713")} Archived ${r.archived} card(s) on \xAB${r.board}\xBB.`);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/help.ts
|
|
1402
|
+
var DOCS = "https://www.ghosty.studio/docs/cli";
|
|
1403
|
+
var HELPS = {
|
|
1404
|
+
agents: {
|
|
1405
|
+
summary: "Create, inspect, change, check and delete agents.",
|
|
1406
|
+
usage: [
|
|
1407
|
+
"ghosty agents ls",
|
|
1408
|
+
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
|
|
1409
|
+
"ghosty agents get <agent-id> [--fields name,model,prompt]",
|
|
1410
|
+
"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]",
|
|
1411
|
+
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1412
|
+
"ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
|
|
1413
|
+
"ghosty agents restart <agent-id>",
|
|
1414
|
+
"ghosty agents demo <agent-id> [--slug NAME] [--on | --off] \u2026",
|
|
1415
|
+
"ghosty agents rm <agent-id> [--yes]"
|
|
1416
|
+
],
|
|
1417
|
+
flags: [
|
|
1418
|
+
["--fields a,b", "get: return only these fields"],
|
|
1419
|
+
["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
|
|
1420
|
+
["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
|
|
1421
|
+
["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
|
|
1422
|
+
["--verbose", "doctor: also list the checks that passed"],
|
|
1423
|
+
["--write", "docs grant: the agent may replace the document (default: read only)"],
|
|
1424
|
+
["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
|
|
1425
|
+
["--yes", "rm: skip the confirmation (required without a terminal)"]
|
|
1426
|
+
],
|
|
1427
|
+
examples: [
|
|
1428
|
+
"ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
|
|
1429
|
+
"ghosty agents set <id> --prompt-file PROMPT.md",
|
|
1430
|
+
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1431
|
+
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1432
|
+
"ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
|
|
1433
|
+
],
|
|
1434
|
+
notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
|
|
1435
|
+
learn: `${DOCS}#agents`
|
|
1436
|
+
},
|
|
1437
|
+
skills: {
|
|
1438
|
+
summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
|
|
1439
|
+
usage: [
|
|
1440
|
+
"ghosty skills ls <agent-id>",
|
|
1441
|
+
"ghosty skills get <agent-id> <slug> [--out DIR]",
|
|
1442
|
+
"ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
|
|
1443
|
+
"ghosty skills rm <agent-id> <slug> [--yes]"
|
|
1444
|
+
],
|
|
1445
|
+
flags: [
|
|
1446
|
+
["--out DIR", "get: folder to write into (default ./<slug>)"],
|
|
1447
|
+
["--file FILE", "add: upload one SKILL.md"],
|
|
1448
|
+
["--dir FOLDER", "add: upload a whole skill folder (SKILL.md + assets); restores a backup"],
|
|
1449
|
+
["--yes", "rm: skip the confirmation (required without a terminal)"]
|
|
1450
|
+
],
|
|
1451
|
+
examples: [
|
|
1452
|
+
"ghosty skills get <id> cotizar --out ./backup/cotizar",
|
|
1453
|
+
"ghosty skills add <id> cotizar --dir ./backup/cotizar",
|
|
1454
|
+
"ghosty skills add <id> pdf-doc # from the community catalog",
|
|
1455
|
+
"ghosty skills rm <id> cotizacion # keeps a local copy first"
|
|
1456
|
+
],
|
|
1457
|
+
notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
|
|
1458
|
+
learn: `${DOCS}#skills`
|
|
1459
|
+
},
|
|
1460
|
+
dbs: {
|
|
1461
|
+
summary: "Databases an agent can use, and what it may write from customer channels.",
|
|
1462
|
+
usage: [
|
|
1463
|
+
"ghosty dbs ls <agent-id>",
|
|
1464
|
+
"ghosty dbs tables <agent-id> <db>",
|
|
1465
|
+
'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
|
|
1466
|
+
"ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--dry-run] [--yes]",
|
|
1467
|
+
"ghosty dbs grant <agent-id> <db> [--external-write t1,t2]",
|
|
1468
|
+
"ghosty dbs revoke <agent-id> <db>"
|
|
1469
|
+
],
|
|
1470
|
+
flags: [
|
|
1471
|
+
["--arg V", "query: value for a ? placeholder (repeatable)"],
|
|
1472
|
+
["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
|
|
1473
|
+
["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
|
|
1474
|
+
["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
|
|
1475
|
+
["--tables a,b", "import: only these tables (default: all)"],
|
|
1476
|
+
["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
|
|
1477
|
+
["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing"]
|
|
1478
|
+
],
|
|
1479
|
+
examples: [
|
|
1480
|
+
"ghosty dbs ls <id>",
|
|
1481
|
+
"ghosty dbs tables <id> totequim",
|
|
1482
|
+
'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
|
|
1483
|
+
"ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
|
|
1484
|
+
"ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
|
|
1485
|
+
],
|
|
1486
|
+
notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
|
|
1487
|
+
learn: `${DOCS}#dbs`
|
|
1488
|
+
},
|
|
1489
|
+
turns: {
|
|
1490
|
+
summary: "An agent's recent turns: what answered, what failed and why.",
|
|
1491
|
+
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]"],
|
|
1492
|
+
flags: [
|
|
1493
|
+
["--errors", "only failed turns (same as --status error)"],
|
|
1494
|
+
["--status S", "running | done | stopped | error"],
|
|
1495
|
+
["--since T", "30m, 1h, 2d or an ISO date"],
|
|
1496
|
+
["--limit N", "max rows (default 20, max 500)"]
|
|
1497
|
+
],
|
|
1498
|
+
examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
|
|
1499
|
+
learn: `${DOCS}#turns`
|
|
1500
|
+
},
|
|
1501
|
+
files: {
|
|
1502
|
+
summary: "Files in the agent's machine.",
|
|
1503
|
+
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]"],
|
|
1504
|
+
examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
|
|
1505
|
+
learn: `${DOCS}#files`
|
|
1506
|
+
},
|
|
1507
|
+
mcp: {
|
|
1508
|
+
summary: "MCP servers of an agent. set REPLACES the whole list.",
|
|
1509
|
+
usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
|
|
1510
|
+
examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
|
|
1511
|
+
learn: `${DOCS}#mcp`
|
|
1512
|
+
},
|
|
1513
|
+
try: {
|
|
1514
|
+
summary: "One test turn; prints the whole answer.",
|
|
1515
|
+
usage: ['ghosty try <agent-id> "message" [--session ID] [--reset] [--whatsapp]'],
|
|
1516
|
+
examples: ['ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"', "ghosty try <id> --reset --session s1"],
|
|
1517
|
+
learn: `${DOCS}#try`
|
|
1518
|
+
},
|
|
1519
|
+
chat: {
|
|
1520
|
+
summary: "Stream a conversation; with no message it is interactive.",
|
|
1521
|
+
usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
|
|
1522
|
+
examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
|
|
1523
|
+
learn: `${DOCS}#chat`
|
|
1524
|
+
},
|
|
1525
|
+
schedule: {
|
|
1526
|
+
summary: "Turns that run later, on the agent's own clock.",
|
|
1527
|
+
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>"],
|
|
1528
|
+
examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
|
|
1529
|
+
learn: `${DOCS}#schedule`
|
|
1530
|
+
},
|
|
1531
|
+
me: {
|
|
1532
|
+
summary: "Your account: files and usage.",
|
|
1533
|
+
usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
|
|
1534
|
+
examples: ["ghosty me files ls --kind document"],
|
|
1535
|
+
learn: `${DOCS}#me`
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
HELPS.credentials = {
|
|
1539
|
+
summary: "Provider keys in your account's vault (encrypted; only the last 4 characters come back).",
|
|
1540
|
+
usage: ["ghosty credentials ls", "ghosty credentials set <provider> [--from-env VAR | < stdin]", "ghosty credentials rm <provider> [--yes]"],
|
|
1541
|
+
flags: [
|
|
1542
|
+
["--from-env VAR", "set: read the key from this environment variable (pass its NAME)"],
|
|
1543
|
+
["--yes", "rm: skip the confirmation"]
|
|
1544
|
+
],
|
|
1545
|
+
examples: [
|
|
1546
|
+
"ghosty credentials set elevenlabs # hidden prompt",
|
|
1547
|
+
"ghosty credentials set mercadopago --from-env MP_ACCESS_TOKEN",
|
|
1548
|
+
"op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
|
|
1549
|
+
],
|
|
1550
|
+
notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
|
|
1551
|
+
learn: `${DOCS}#credentials`
|
|
1552
|
+
};
|
|
1553
|
+
HELPS.creds = HELPS.credentials;
|
|
1554
|
+
HELPS.channels = {
|
|
1555
|
+
summary: "Which WhatsApp Business numbers an agent answers. Pair new numbers in the web.",
|
|
1556
|
+
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]"],
|
|
1557
|
+
flags: [
|
|
1558
|
+
["-n, --dry-run", "print the plan (+ starts answering, - stops) and change nothing"],
|
|
1559
|
+
["--yes", "enable: skip typing the last 4 digits (required without a terminal)"]
|
|
1560
|
+
],
|
|
1561
|
+
examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
|
|
1562
|
+
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."],
|
|
1563
|
+
learn: `${DOCS}#channels`
|
|
1564
|
+
};
|
|
1565
|
+
HELPS.board = {
|
|
1566
|
+
summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
|
|
1567
|
+
usage: ["ghosty board archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"],
|
|
1568
|
+
flags: [
|
|
1569
|
+
["--integration ID", "cards from this channel/integration (e.g. prueba)"],
|
|
1570
|
+
["--column NAME", "cards in this column"],
|
|
1571
|
+
["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
|
|
1572
|
+
["-n, --dry-run", "count and show a sample, change nothing"],
|
|
1573
|
+
["--yes", "skip the confirmation (required without a terminal)"]
|
|
1574
|
+
],
|
|
1575
|
+
examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
1576
|
+
notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
1577
|
+
learn: `${DOCS}#board`
|
|
1578
|
+
};
|
|
1579
|
+
HELPS.conversations = HELPS.chat;
|
|
1580
|
+
HELPS.usage = HELPS.me;
|
|
1581
|
+
function helpText(cmd) {
|
|
1582
|
+
const h = HELPS[cmd];
|
|
1583
|
+
if (!h) return void 0;
|
|
1584
|
+
const section = (t, lines) => [bold(t), ...lines.map((l) => ` ${l}`), ""];
|
|
1585
|
+
const w = Math.max(0, ...(h.flags ?? []).map(([f]) => f.length)) + 2;
|
|
1586
|
+
return [
|
|
1587
|
+
h.summary,
|
|
1588
|
+
"",
|
|
1589
|
+
...section("USAGE", h.usage),
|
|
1590
|
+
...h.flags?.length ? section("FLAGS", h.flags.map(([f, d]) => `${f.padEnd(w)}${d}`)) : [],
|
|
1591
|
+
...section("EXAMPLES", h.examples.map((e) => `$ ${e}`)),
|
|
1592
|
+
...h.notes?.length ? section("NOTES", h.notes) : [],
|
|
1593
|
+
bold("LEARN MORE"),
|
|
1594
|
+
` Add --json for machine output. Exit codes: 0 ok \xB7 1 API error \xB7 2 usage \xB7 3 not signed in.`,
|
|
1595
|
+
` ${dim(h.learn ?? DOCS)}`
|
|
1596
|
+
].join("\n");
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1012
1599
|
// src/index.ts
|
|
1013
1600
|
var HELP = `${bold("Usage:")} ghosty <command> [options]
|
|
1014
1601
|
|
|
@@ -1025,20 +1612,27 @@ ${bold("Agents")}
|
|
|
1025
1612
|
agents get <id> Show an agent's config [--fields name,model,prompt]
|
|
1026
1613
|
agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
1027
1614
|
[--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
|
|
1028
|
-
|
|
1615
|
+
[--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
|
|
1616
|
+
agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
|
|
1617
|
+
agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
|
|
1618
|
+
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
1029
1619
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
1030
1620
|
agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
|
|
1031
|
-
[--slug NAME] [--on | --off] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--sponsor on|off]
|
|
1621
|
+
[--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off]
|
|
1032
1622
|
|
|
1033
1623
|
${bold("Configure an agent")}
|
|
1034
1624
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
1035
|
-
skills ls|add|rm <id> [slug]
|
|
1625
|
+
skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; rm keeps a copy)
|
|
1626
|
+
dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
|
|
1036
1627
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
1628
|
+
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
1629
|
+
board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
1037
1630
|
|
|
1038
1631
|
${bold("Talk to an agent")}
|
|
1039
1632
|
try <id> "message" One test turn, prints the full answer [--session] [--reset] [--whatsapp]
|
|
1040
1633
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
1041
1634
|
conversations ls <id> List conversations
|
|
1635
|
+
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
1042
1636
|
|
|
1043
1637
|
${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
|
|
1044
1638
|
schedule ls <id> Pending turns and recent runs
|
|
@@ -1050,18 +1644,26 @@ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see:
|
|
|
1050
1644
|
|
|
1051
1645
|
${bold("Your account")}
|
|
1052
1646
|
me files ls|upload|rm Files in your account [--kind image|video|audio|document|other]
|
|
1647
|
+
credentials ls|set|rm [provider] Provider keys in your vault (hidden prompt, stdin or --from-env VAR; never as an argument)
|
|
1053
1648
|
docs [topic] Open the docs
|
|
1054
1649
|
|
|
1055
1650
|
${bold("Global options")}
|
|
1056
1651
|
--json Machine-readable output (stdout is JSON only)
|
|
1057
1652
|
--token <token> Use this token (or env GHOSTY_TOKEN). A gat_\u2026 agent token reaches only that agent.
|
|
1058
1653
|
--profile <name> Use a separate saved session (or env GHOSTY_PROFILE), e.g. one per account
|
|
1059
|
-
|
|
1654
|
+
--yes Skip confirmations (required for rm without a terminal)
|
|
1655
|
+
-h, --help Show help (also: ghosty <command> --help, ghosty help <command>)
|
|
1060
1656
|
-v, --version Show version
|
|
1061
1657
|
|
|
1062
1658
|
${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signed in
|
|
1063
1659
|
${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.")}
|
|
1064
1660
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
1661
|
+
var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
|
|
1662
|
+
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo"];
|
|
1663
|
+
function unknown(what, input, known, helpCmd) {
|
|
1664
|
+
const s = suggest(input, known);
|
|
1665
|
+
return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
|
|
1666
|
+
}
|
|
1065
1667
|
function showHelp() {
|
|
1066
1668
|
if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
|
|
1067
1669
|
process.stdout.write(HELP + "\n");
|
|
@@ -1108,19 +1710,47 @@ async function main() {
|
|
|
1108
1710
|
on: { type: "boolean" },
|
|
1109
1711
|
off: { type: "boolean" },
|
|
1110
1712
|
turns: { type: "string" },
|
|
1713
|
+
hours: { type: "string" },
|
|
1111
1714
|
"per-ip": { type: "string" },
|
|
1112
1715
|
welcome: { type: "string" },
|
|
1113
1716
|
chips: { type: "string" },
|
|
1114
|
-
|
|
1717
|
+
starter: { type: "string" },
|
|
1718
|
+
sponsor: { type: "string" },
|
|
1719
|
+
yes: { type: "boolean", short: "y" },
|
|
1720
|
+
dir: { type: "string" },
|
|
1721
|
+
arg: { type: "string", multiple: true },
|
|
1722
|
+
write: { type: "boolean" },
|
|
1723
|
+
"external-write": { type: "string" },
|
|
1724
|
+
"no-deliver": { type: "boolean" },
|
|
1725
|
+
deliver: { type: "boolean" },
|
|
1726
|
+
errors: { type: "boolean" },
|
|
1727
|
+
status: { type: "string" },
|
|
1728
|
+
since: { type: "string" },
|
|
1729
|
+
limit: { type: "string" },
|
|
1730
|
+
verbose: { type: "boolean" },
|
|
1731
|
+
voice: { type: "string" },
|
|
1732
|
+
"voice-replies": { type: "string" },
|
|
1733
|
+
"auto-stage": { type: "string" },
|
|
1734
|
+
"from-env": { type: "string" },
|
|
1735
|
+
"dry-run": { type: "boolean", short: "n" },
|
|
1736
|
+
integration: { type: "string" },
|
|
1737
|
+
from: { type: "string" },
|
|
1738
|
+
tables: { type: "string" },
|
|
1739
|
+
append: { type: "boolean" },
|
|
1740
|
+
column: { type: "string" },
|
|
1741
|
+
before: { type: "string" }
|
|
1115
1742
|
}
|
|
1116
1743
|
});
|
|
1117
1744
|
globals.json = Boolean(v.json);
|
|
1118
1745
|
const tokens = splitTokens(pos, v);
|
|
1119
1746
|
globals.token = tokens.session;
|
|
1120
1747
|
globals.profile = v.profile;
|
|
1121
|
-
if (v.version) return out({ version: "0.
|
|
1748
|
+
if (v.version) return out({ version: "0.8.0" }, () => "0.8.0");
|
|
1122
1749
|
const [cmd, sub, a1, a2] = pos;
|
|
1123
1750
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1751
|
+
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
1752
|
+
if (topic === "partner") return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1753
|
+
if (topic && helpText(topic)) return out({ help: helpText(topic) }, () => helpText(topic));
|
|
1124
1754
|
if (!cmd || v.help || cmd === "help") return showHelp();
|
|
1125
1755
|
switch (cmd) {
|
|
1126
1756
|
case "login": {
|
|
@@ -1146,16 +1776,30 @@ async function main() {
|
|
|
1146
1776
|
if (sub === "create" || sub === "new") return agentsCreate(v);
|
|
1147
1777
|
if (sub === "get") return agentsGet(a1, v.fields);
|
|
1148
1778
|
if (sub === "set") return agentsSet(a1, v);
|
|
1149
|
-
if (sub === "rm" || sub === "delete") return agentsRm(a1);
|
|
1779
|
+
if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
|
|
1780
|
+
if (sub === "doctor") return agentsDoctor(a1, v);
|
|
1781
|
+
if (sub === "docs" || sub === "documents") return agentDocsCmd(a1, a2, pos[4], v);
|
|
1150
1782
|
if (sub === "restart") return agentsRestart(a1);
|
|
1151
1783
|
if (sub === "demo") return agentsDemo(a1, v);
|
|
1152
|
-
throw
|
|
1784
|
+
throw unknown(`agents ${sub}`, sub, AGENTS_SUBS, "ghosty agents --help");
|
|
1153
1785
|
case "files":
|
|
1154
1786
|
return filesCmd(sub, a1, a2, v);
|
|
1155
1787
|
case "skills":
|
|
1156
1788
|
return skillsCmd(sub, a1, a2, v);
|
|
1157
1789
|
case "mcp":
|
|
1158
1790
|
return mcpCmd(sub, a1, v);
|
|
1791
|
+
case "dbs":
|
|
1792
|
+
case "db":
|
|
1793
|
+
return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
|
|
1794
|
+
case "turns":
|
|
1795
|
+
return turnsCmd(sub, a1, v);
|
|
1796
|
+
case "board":
|
|
1797
|
+
return boardCmd(sub, a1, v);
|
|
1798
|
+
case "channels":
|
|
1799
|
+
return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
|
|
1800
|
+
case "credentials":
|
|
1801
|
+
case "creds":
|
|
1802
|
+
return credentialsCmd2(sub, a1, v);
|
|
1159
1803
|
case "try":
|
|
1160
1804
|
return tryTurn(sub, a1, v);
|
|
1161
1805
|
case "chat":
|
|
@@ -1180,7 +1824,7 @@ async function main() {
|
|
|
1180
1824
|
return openUrl(url);
|
|
1181
1825
|
}
|
|
1182
1826
|
default:
|
|
1183
|
-
throw
|
|
1827
|
+
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
1184
1828
|
}
|
|
1185
1829
|
}
|
|
1186
1830
|
main().then(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostystudio/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --define:__VERSION__=\\\"$npm_package_version\\\"",
|
|
18
18
|
"prepack": "npm run build",
|
|
19
|
-
"test": "esbuild src
|
|
19
|
+
"test": "for t in flags prompt; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"ghosty",
|