@ghostystudio/cli 0.13.2 → 0.15.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 +27 -5
- package/dist/index.js +292 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,11 @@ npm i -g @ghostystudio/cli && ghosty login
|
|
|
16
16
|
ghosty whoami
|
|
17
17
|
ghosty usage # plan and usage this week / month (also: ghosty me usage)
|
|
18
18
|
ghosty agents ls
|
|
19
|
-
ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…]
|
|
20
|
-
ghosty agents get <id> [--fields name,model,prompt]
|
|
21
|
-
ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…]
|
|
19
|
+
ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…] [--workspace SLUG] [--channels teams=off]
|
|
20
|
+
ghosty agents get <id> [--fields name,model,prompt] [--prompt-out FILE]
|
|
21
|
+
ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…] [--toolsets +crm]
|
|
22
|
+
ghosty agents set <id> --prompt-base <base-id> --prompt-overlay overlay.json [--dry-run] # prompt = another agent's + edits, kept in sync
|
|
23
|
+
ghosty agents share|unshare <id> <email> [--role editor|admin] # share without handing it over · agents shares <id> lists them
|
|
22
24
|
ghosty agents rm <id> [--yes]
|
|
23
25
|
ghosty agents restart <id>
|
|
24
26
|
ghosty agents box <id> [--check PATH]… [--contains PATH=TEXT]… [--recycle] # is its machine on the new image?
|
|
@@ -26,24 +28,29 @@ ghosty agents demo <id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"
|
|
|
26
28
|
[--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate] # public /p/ link, no account needed
|
|
27
29
|
ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
|
|
28
30
|
ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
|
|
31
|
+
ghosty agents docs add <id> --url URL | --file PATH [--description TEXT] [--grant read|write|no]
|
|
29
32
|
ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
|
|
30
33
|
|
|
31
34
|
ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
|
|
32
35
|
ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR] # get without --out only lists
|
|
36
|
+
ghosty skills on|off <id> <slug> # turn one of its skills (or one of Ghosty's) on or off
|
|
33
37
|
ghosty skills templates <id> # pdf-doc templates: its own and Ghosty's, and which folder wins
|
|
34
38
|
ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--external-rows t:col+col] [--from easybits:ID --tables a,b --append --dry-run]
|
|
35
39
|
ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
|
|
36
|
-
ghosty turns show <id> [turn-id|last] #
|
|
40
|
+
ghosty turns show <id> [turn-id|last] # what came in, what it answered, where the time went [--full]
|
|
37
41
|
ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
|
|
38
42
|
ghosty channels whatsapp link|unlink <id> --integration ID --answer on|off [--forward] [--dry-run] # bring a WABA number from Formmy
|
|
43
|
+
ghosty channels whatsapp history <id> --integration ID [--days 7] [--dry-run] # its recent cards, messages and orders (no duplicates)
|
|
39
44
|
ghosty channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no [--dry-run] # personal WhatsApp session, no QR
|
|
40
45
|
ghosty channels wa pair|status|pause <id> [--phone N]
|
|
41
46
|
ghosty channels wa groups ls|set|create <id> [group] [--role equipo|cliente] [--on|--off] [--dry-run] # personal WhatsApp groups
|
|
47
|
+
ghosty board ls <id> [--q TEXT] [--limit 20] # recent cards on its sales board
|
|
48
|
+
ghosty board show <id> <folio|phone|id> [--limit 20] # a card's thread, attachments and whether gs kept a copy
|
|
42
49
|
ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
|
|
43
50
|
ghosty mcp get|set <id> [--file servers.json]
|
|
44
51
|
|
|
45
52
|
ghosty try <id> "message" [--session] [--reset]
|
|
46
|
-
ghosty try <id> "message" --waba --session PHONE [--media FILE] [--history FILE] # real WhatsApp path, nothing sent
|
|
53
|
+
ghosty try <id> "message" --waba --session PHONE [--media FILE [--as-formmy]] [--history FILE] # real WhatsApp path, nothing sent
|
|
47
54
|
ghosty try <id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]
|
|
48
55
|
ghosty chat <id> ["message"] [--conversation ID]
|
|
49
56
|
ghosty conversations ls <id>
|
|
@@ -79,6 +86,21 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
|
79
86
|
- `try --replay FILE --out DIR`: replays real conversations from your machine. The report is written after every step, survives Ghosty deploys (it honors `Retry-After`), and `--resume` continues a partial run.
|
|
80
87
|
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
81
88
|
|
|
89
|
+
### Changes in 0.15.0
|
|
90
|
+
|
|
91
|
+
- `try --waba` lists everything the agent would send, tools included: 📍 location, 📄 files, 🎤 voice, ↩️ quotes, 🔘 buttons, 👤 contacts, reactions. Quotes and reactions now find the test message to point at.
|
|
92
|
+
- `try --waba --media FILE --as-formmy`: the attachment arrives like a real Formmy forward (proxy with credential, copy to gs, transcription) and the output says whether the copy worked.
|
|
93
|
+
- `board ls` (recent cards) and `board show <folio|phone|id>` (a card's thread: who wrote, text, attachment, copy in gs).
|
|
94
|
+
|
|
95
|
+
### Changes in 0.14.0
|
|
96
|
+
|
|
97
|
+
- Fix: `--json` output over a pipe no longer gets cut at 64 KB (an 80 KB prompt came out as invalid JSON). The CLI now waits for stdout to drain before exiting.
|
|
98
|
+
- `agents get --prompt-out FILE`; `agents create --workspace SLUG --channels …` (born in a workspace, active in its Teams); `agents set --toolsets`; `agents share|unshare|shares`; `agents docs add --url|--file`; `skills on|off`.
|
|
99
|
+
- `turns show` prints what came in and what the agent answered, on any engine or channel (`--full` for the whole text).
|
|
100
|
+
- `agents doctor` flags databases the prompt names that the agent doesn't have (EasyBits ids left over from a migration).
|
|
101
|
+
- `agents set --prompt-base <agent> --prompt-overlay overlay.json`: a prompt derived from another agent's plus a JSON overlay of edits; it regenerates when the base changes (`--prompt-base none` unlinks).
|
|
102
|
+
- `channels whatsapp history`: bring a WhatsApp Business number's recent cards, messages and orders from Formmy to the board.
|
|
103
|
+
|
|
82
104
|
### Changes in 0.13.2
|
|
83
105
|
|
|
84
106
|
- `channels whatsapp ls` shows the real number, "records only" numbers, and whether Formmy forwards to gs. `whatsapp link` stores the number's name and phone.
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,12 @@ function table(rows, cols) {
|
|
|
52
52
|
const line = (vals) => vals.map((v, i) => v.padEnd(w[i])).join(" ").trimEnd();
|
|
53
53
|
return [bold(line(cols.map((c) => c.toUpperCase()))), ...rows.map((r) => line(cols.map((c) => cell(r[c]))))].join("\n");
|
|
54
54
|
}
|
|
55
|
+
function exitAfterFlush(code) {
|
|
56
|
+
process.exitCode = code;
|
|
57
|
+
process.stdout.write("", () => process.exit(code));
|
|
58
|
+
return new Promise(() => {
|
|
59
|
+
});
|
|
60
|
+
}
|
|
55
61
|
function fail(e) {
|
|
56
62
|
const err = e instanceof CliError ? e : new CliError(e instanceof Error ? e.message : String(e));
|
|
57
63
|
if (globals.json) {
|
|
@@ -60,7 +66,7 @@ function fail(e) {
|
|
|
60
66
|
info(red("Error: ") + err.message);
|
|
61
67
|
if (err.hint) info(dim(err.hint));
|
|
62
68
|
}
|
|
63
|
-
|
|
69
|
+
return exitAfterFlush(err.code);
|
|
64
70
|
}
|
|
65
71
|
async function openUrl(url) {
|
|
66
72
|
const { spawn } = await import("node:child_process");
|
|
@@ -282,7 +288,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
282
288
|
const req = httpsRequest(`${base}${path}`, {
|
|
283
289
|
method: "POST",
|
|
284
290
|
timeout: timeoutMs,
|
|
285
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.
|
|
291
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.15.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
286
292
|
}, (res) => {
|
|
287
293
|
let buf = "";
|
|
288
294
|
res.on("data", (c) => buf += c);
|
|
@@ -324,7 +330,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
324
330
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
325
331
|
headers: {
|
|
326
332
|
authorization: `Bearer ${token}`,
|
|
327
|
-
"user-agent": `ghosty-cli/${"0.
|
|
333
|
+
"user-agent": `ghosty-cli/${"0.15.0"}`,
|
|
328
334
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
329
335
|
...opts.headers
|
|
330
336
|
},
|
|
@@ -422,13 +428,34 @@ async function uploadMedia(file) {
|
|
|
422
428
|
const mime = MIME[ext] ?? "application/octet-stream";
|
|
423
429
|
return { url: r.url, mime, filename: r.name, type: mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document" };
|
|
424
430
|
}
|
|
431
|
+
function richLine(s) {
|
|
432
|
+
const d = s.data ?? {};
|
|
433
|
+
const str = (k) => typeof d[k] === "string" ? d[k] : "";
|
|
434
|
+
switch (s.rich) {
|
|
435
|
+
case "ubicacion":
|
|
436
|
+
return `\u{1F4CD} ${str("nombre") || "location"} \xB7 ${Number(d.latitud).toFixed(4)},${Number(d.longitud).toFixed(4)}${str("direccion") ? dim(` ${str("direccion")}`) : ""}`;
|
|
437
|
+
case "boton":
|
|
438
|
+
return `\u{1F518} ${str("boton")} ${dim(str("url"))} ${str("texto")}`;
|
|
439
|
+
case "contacto":
|
|
440
|
+
return `\u{1F464} ${str("nombre")} ${str("telefono")}`;
|
|
441
|
+
case "encuesta":
|
|
442
|
+
return `\u{1F4CA} ${str("pregunta")}`;
|
|
443
|
+
case "reaccion":
|
|
444
|
+
return `${str("emoji")} ${dim(`reaction to ${str("mensajeId") || "the last message"}`)}`;
|
|
445
|
+
case "cita":
|
|
446
|
+
return `\u21A9\uFE0F ${str("texto")} ${dim(`quoting ${str("mensajeId") || "the last message"}`)}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
425
449
|
function sentText(sent) {
|
|
426
|
-
return sent.map(
|
|
450
|
+
return sent.map(
|
|
451
|
+
(s) => s.kind === "text" ? `${lavender("\u2192")} ${s.text}` : s.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s.text}` : s.kind === "file" ? `${lavender(/^audio\//.test(s.mime ?? "") ? "\u{1F3A4}" : "\u{1F4C4}")} ${s.name} ${dim(s.url)}` : richLine(s)
|
|
452
|
+
).join("\n\n");
|
|
427
453
|
}
|
|
428
454
|
async function tryWaba(id, text, v) {
|
|
429
455
|
const phone = v.session ?? "";
|
|
430
456
|
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
457
|
if ((v.media?.length ?? 0) > 1) throw usage("One --media per turn (WhatsApp sends one file per message).", "Send several turns, one file each.");
|
|
458
|
+
if (v["as-formmy"] && !v.media?.length) throw usage("--as-formmy goes with --media FILE.", 'Example: ghosty try <agent-id> "" --waba --session 5210000000009 --media nota.ogg --as-formmy');
|
|
432
459
|
if (!text && !v.media?.length && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
433
460
|
let history;
|
|
434
461
|
if (v.history) {
|
|
@@ -439,9 +466,11 @@ async function tryWaba(id, text, v) {
|
|
|
439
466
|
}
|
|
440
467
|
const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
|
|
441
468
|
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 } : {} });
|
|
469
|
+
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {}, ...v["as-formmy"] ? { asFormmy: true } : {} });
|
|
443
470
|
if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ?? "turn failed"}`), { code: 1 });
|
|
444
|
-
|
|
471
|
+
const via = r.body.media ? `
|
|
472
|
+
${r.body.media.copied ? dim("attachment: downloaded through the Formmy-like proxy and copied to gs") : red("attachment: NOT copied to gs (the proxy download failed)")}` : "";
|
|
473
|
+
out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim("(the agent sent nothing)") : dim("Session reset.")) + via + (r.body.error ? `
|
|
445
474
|
${dim(`(warning: ${r.body.error})`)}` : ""));
|
|
446
475
|
}
|
|
447
476
|
async function tryReplay(id, v) {
|
|
@@ -522,8 +551,18 @@ function parseEnv(raw) {
|
|
|
522
551
|
}
|
|
523
552
|
return env;
|
|
524
553
|
}
|
|
554
|
+
function parseChannels(raw) {
|
|
555
|
+
if (!raw) return void 0;
|
|
556
|
+
const ch = {};
|
|
557
|
+
for (const pair of raw.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
558
|
+
const [k, val] = pair.split("=");
|
|
559
|
+
if (!["teams", "web", "whatsapp", "messenger", "wa"].includes(k) || val !== "on" && val !== "off") throw usage(`Bad --channels entry: ${pair}.`, "Use teams|web|whatsapp|messenger|wa=on|off, comma-separated (wa = personal WhatsApp)");
|
|
560
|
+
ch[k] = val === "on";
|
|
561
|
+
}
|
|
562
|
+
return ch;
|
|
563
|
+
}
|
|
525
564
|
async function agentsCreate(v) {
|
|
526
|
-
const name = need(v.name, "--name", "ghosty agents create --name Moon --engine ghosty-lite --model gpt-5.6-luna");
|
|
565
|
+
const name = need(v.name, "--name", "ghosty agents create --name Moon --engine ghosty-lite --model gpt-5.6-luna [--workspace SLUG]");
|
|
527
566
|
const body = { name };
|
|
528
567
|
if (v.engine) body.engine = v.engine;
|
|
529
568
|
if (v.model) body.model = v.model;
|
|
@@ -531,31 +570,64 @@ async function agentsCreate(v) {
|
|
|
531
570
|
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
532
571
|
const env = parseEnv(v.env);
|
|
533
572
|
if (env) body.env = env;
|
|
573
|
+
if (v.workspace) body.workspace = v.workspace;
|
|
574
|
+
const channels = parseChannels(v.channels);
|
|
575
|
+
if (channels) body.channels = channels;
|
|
534
576
|
const r = await api("/api/v2/me/agents", { method: "POST", body });
|
|
577
|
+
const where = r.workspace ? `
|
|
578
|
+
workspace: ${r.workspace}${r.handle ? ` \xB7 @${r.handle} in Teams` : dim(" \xB7 not active in Teams")}` : "";
|
|
535
579
|
out(r, () => `${lavender("\u2713")} Created ${r.name} ${dim(`(${r.engine} \xB7 ${r.model ?? "default"})`)}
|
|
536
|
-
id: ${r.id}`);
|
|
580
|
+
id: ${r.id}${where}`);
|
|
537
581
|
}
|
|
538
|
-
async function agentsGet(id, fields) {
|
|
582
|
+
async function agentsGet(id, fields, promptOut) {
|
|
539
583
|
id = need(id, "agent id", "ghosty agents get <agent-id>");
|
|
584
|
+
if (promptOut) {
|
|
585
|
+
const r = await api(agentPath(id, "?fields=prompt"));
|
|
586
|
+
const prompt = r.prompt ?? "";
|
|
587
|
+
await writeFile3(promptOut, prompt);
|
|
588
|
+
return out({ saved: resolve(promptOut), bytes: Buffer.byteLength(prompt) }, () => `${lavender("\u2713")} Prompt \u2192 ${promptOut} ${dim(`(${Buffer.byteLength(prompt)} bytes) \xB7 back: ghosty agents set ${id} --prompt-file ${promptOut}`)}`);
|
|
589
|
+
}
|
|
540
590
|
const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
|
|
541
591
|
out(await api(agentPath(id, q)));
|
|
542
592
|
}
|
|
543
593
|
async function agentsSet(id, v) {
|
|
544
594
|
id = need(id, "agent id", "ghosty agents set <agent-id> --prompt-file PROMPT.md");
|
|
595
|
+
if (v["prompt-base"]) {
|
|
596
|
+
const unlink = v["prompt-base"] === "none";
|
|
597
|
+
if (!unlink && !v["prompt-overlay"]) throw usage("Missing --prompt-overlay FILE.", 'Its format: {"edits":[{"what":"name","find":"Tania","replace":"Pia","all":true}]} (regex: true for patterns)');
|
|
598
|
+
const promptBase = unlink ? null : { agentId: v["prompt-base"], overlay: JSON.parse(await readFile3(v["prompt-overlay"], "utf8")) };
|
|
599
|
+
const r2 = await api(agentPath(id), { method: "PATCH", body: { promptBase, ...v["dry-run"] ? { dryRun: true } : {} } });
|
|
600
|
+
if (v["dry-run"] && r2.prompt) {
|
|
601
|
+
const out_ = v.out;
|
|
602
|
+
if (out_) await writeFile3(out_, r2.prompt);
|
|
603
|
+
return out({ ...r2, ...out_ ? { prompt: void 0, saved: resolve(out_) } : {} }, () => `${r2.chars} chars (base ${r2.baseChars}): every edit applies.${out_ ? ` Written to ${out_}.` : dim(" --out FILE to read it.")}
|
|
604
|
+
${dim("Dry run: nothing changed.")}`);
|
|
605
|
+
}
|
|
606
|
+
return out(r2, () => r2.unlinked ? `${lavender("\u2713")} Unlinked: the prompt stays as it is and is edited by hand again.` : `${lavender("\u2713")} Prompt = ${v["prompt-base"]} + ${v["prompt-overlay"]} (${r2.chars} chars). It regenerates when the base changes; ghosty agents doctor ${id} tells if the overlay stops applying.`);
|
|
607
|
+
}
|
|
545
608
|
const body = {};
|
|
546
609
|
if (v.name) body.name = v.name;
|
|
547
610
|
if (v.engine) body.engine = v.engine;
|
|
548
611
|
if (v.model) body.model = v.model;
|
|
549
612
|
if (v.prompt) body.prompt = v.prompt;
|
|
550
613
|
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
614
|
+
const channels = parseChannels(v.channels);
|
|
615
|
+
if (channels) body.channels = channels;
|
|
616
|
+
if (v.toolsets) {
|
|
617
|
+
const items = v.toolsets.split(",").map((x) => x.trim()).filter(Boolean);
|
|
618
|
+
const cur = await api(agentPath(id, "?fields=tools"));
|
|
619
|
+
const known = Object.keys(cur.tools?.toolsets ?? {});
|
|
620
|
+
const bad = items.map((x) => x.replace(/^[+-]/, "")).filter((x) => !known.includes(x));
|
|
621
|
+
if (bad.length) throw usage(`Unknown toolset: ${bad.join(", ")}.`, `Known: ${known.join(", ")}`);
|
|
622
|
+
const delta = items.every((x) => /^[+-]/.test(x));
|
|
623
|
+
if (!delta && items.some((x) => /^[+-]/.test(x))) throw usage("Mix of plain and +/- toolsets.", "Either a full list (crm,sales) or changes (+crm,-sales).");
|
|
624
|
+
const next = new Set(delta ? known.filter((k) => cur.tools?.toolsets?.[k]?.granted) : []);
|
|
625
|
+
for (const x of items) {
|
|
626
|
+
if (x.startsWith("-")) next.delete(x.slice(1));
|
|
627
|
+
else next.add(x.replace(/^\+/, ""));
|
|
557
628
|
}
|
|
558
|
-
|
|
629
|
+
next.delete("core");
|
|
630
|
+
body.tools = { toolsets: [...next] };
|
|
559
631
|
}
|
|
560
632
|
const env = parseEnv(v.env);
|
|
561
633
|
if (env) body.env = env;
|
|
@@ -579,7 +651,7 @@ async function agentsSet(id, v) {
|
|
|
579
651
|
if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
|
|
580
652
|
body.autoStage = v["auto-stage"] === "on";
|
|
581
653
|
}
|
|
582
|
-
if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
|
|
654
|
+
if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
|
|
583
655
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
584
656
|
out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
|
|
585
657
|
}
|
|
@@ -592,6 +664,25 @@ async function agentsRm(id, v = {}) {
|
|
|
592
664
|
}
|
|
593
665
|
out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
|
|
594
666
|
}
|
|
667
|
+
async function agentsShare(sub, id, email, v) {
|
|
668
|
+
id = need(id, "agent id", "ghosty agents share <agent-id> <email> [--role admin|editor]");
|
|
669
|
+
if (sub === "shares") {
|
|
670
|
+
const r2 = await api(agentPath(id, "/shares"));
|
|
671
|
+
const rows = r2.shares.map((x) => ({ email: x.email, role: x.role, accepted: x.acceptedAt ? "yes" : "not yet" }));
|
|
672
|
+
return out(r2, () => `${dim(`owner: ${r2.owner ?? "?"}`)}
|
|
673
|
+
${rows.length ? table(rows, ["email", "role", "accepted"]) : dim("Not shared with anyone.")}`);
|
|
674
|
+
}
|
|
675
|
+
email = need(email, "email", `ghosty agents ${sub} ${id} ana@empresa.com`);
|
|
676
|
+
if (sub === "unshare") {
|
|
677
|
+
requireYesIfHeadless(v.yes, `ghosty agents unshare ${id} ${email}`);
|
|
678
|
+
if (!v.yes) await confirm(`Remove ${email}'s access? On ACP agents this rotates the connection token (apps reconnect).`, { retry: `ghosty agents unshare ${id} ${email}` });
|
|
679
|
+
const r2 = await api(agentPath(id, "/shares"), { method: "DELETE", body: { email } });
|
|
680
|
+
return out(r2, () => `${lavender("\u2713")} ${email} no longer has access.${r2.tokenRotated ? dim(" Token rotated.") : ""}`);
|
|
681
|
+
}
|
|
682
|
+
if (v.role && v.role !== "admin" && v.role !== "editor") throw usage("--role takes admin or editor.");
|
|
683
|
+
const r = await api(agentPath(id, "/shares"), { method: "POST", body: { email, ...v.role ? { role: v.role } : {} } });
|
|
684
|
+
out(r, () => `${lavender("\u2713")} ${r.already ? `Already shared with ${r.email}` : `Shared with ${r.email}; they got an email`} ${dim(`(${r.role})`)}.`);
|
|
685
|
+
}
|
|
595
686
|
async function agentsRestart(id) {
|
|
596
687
|
id = need(id, "agent id", "ghosty agents restart <agent-id>");
|
|
597
688
|
out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} Restarting \u2014 the next turn uses the new config.`);
|
|
@@ -681,7 +772,7 @@ async function readSkillDir(dir) {
|
|
|
681
772
|
}
|
|
682
773
|
var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
683
774
|
async function skillsCmd(sub, id, slug, v) {
|
|
684
|
-
const ex = "ghosty skills ls <agent-id> | get <agent-id> <slug> [--out DIR] | templates <agent-id> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
|
|
775
|
+
const ex = "ghosty skills ls <agent-id> | on|off <agent-id> <slug> | get <agent-id> <slug> [--out DIR] | templates <agent-id> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
|
|
685
776
|
id = need(id, "agent id", ex);
|
|
686
777
|
switch (sub ?? "ls") {
|
|
687
778
|
case "ls":
|
|
@@ -697,6 +788,13 @@ async function skillsCmd(sub, id, slug, v) {
|
|
|
697
788
|
${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
|
|
698
789
|
return;
|
|
699
790
|
}
|
|
791
|
+
case "on":
|
|
792
|
+
case "off": {
|
|
793
|
+
slug = need(slug, "skill slug", `ghosty skills ${sub} <agent-id> <slug>`);
|
|
794
|
+
const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PATCH", body: { enabled: sub === "on" } });
|
|
795
|
+
out(r, () => `${lavender("\u2713")} ${slug} ${sub === "on" ? "on" : "off"} ${dim(`(${r.source === "house" ? "Ghosty's" : "own"}${r.nota ? `; ${r.nota}` : ""})`)}`);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
700
798
|
case "get": {
|
|
701
799
|
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
702
800
|
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
|
|
@@ -1145,7 +1243,7 @@ async function call(path, v, init = {}) {
|
|
|
1145
1243
|
"x-ghosty-ts": ts,
|
|
1146
1244
|
"x-ghosty-sig": sig,
|
|
1147
1245
|
"content-type": "application/json",
|
|
1148
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1246
|
+
"user-agent": `ghosty-cli/${"0.15.0"}`
|
|
1149
1247
|
},
|
|
1150
1248
|
body: method === "GET" ? void 0 : raw
|
|
1151
1249
|
});
|
|
@@ -1310,6 +1408,10 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
1310
1408
|
}
|
|
1311
1409
|
}
|
|
1312
1410
|
|
|
1411
|
+
// src/commands/data.ts
|
|
1412
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1413
|
+
import { basename as basename3 } from "node:path";
|
|
1414
|
+
|
|
1313
1415
|
// src/rows.ts
|
|
1314
1416
|
var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1315
1417
|
function parseRowScope(raw) {
|
|
@@ -1439,17 +1541,30 @@ async function agentDocsCmd(sub, id, doc, v) {
|
|
|
1439
1541
|
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
|
|
1440
1542
|
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"}.`);
|
|
1441
1543
|
}
|
|
1544
|
+
case "add": {
|
|
1545
|
+
if (!v.url === !v.file) throw usage("Use --url URL or --file PATH (one of them).", 'Example: ghosty agents docs add <agent-id> --url https://\u2026/lista.pdf --description "Precios 2026"');
|
|
1546
|
+
if (v.grant && !["read", "write", "no"].includes(v.grant)) throw usage("--grant takes read, write or no.");
|
|
1547
|
+
const body = {
|
|
1548
|
+
...v.url ? { url: v.url } : { contentBase64: (await readFile6(v.file)).toString("base64"), name: v.name ?? basename3(v.file) },
|
|
1549
|
+
...v.url && v.name ? { name: v.name } : {},
|
|
1550
|
+
...v.description ? { description: v.description } : {},
|
|
1551
|
+
grant: v.grant === "no" ? false : v.grant === "write" || v.write ? "write" : "read",
|
|
1552
|
+
...v["no-deliver"] ? { deliverable: false } : {}
|
|
1553
|
+
};
|
|
1554
|
+
const r = await api(agentPath2(id, "/documents"), { method: "POST", body });
|
|
1555
|
+
return out(r, () => `${lavender("\u2713")} ${r.name} uploaded ${dim(`(${r.size} bytes)`)}${r.granted ? `, ${r.mode === "write" ? "it reads and replaces it" : "it reads it"}.` : ". Not granted: ghosty agents docs grant " + id + ` "${r.name}"`}`);
|
|
1556
|
+
}
|
|
1442
1557
|
case "revoke": {
|
|
1443
1558
|
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
|
|
1444
1559
|
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
|
|
1445
1560
|
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
|
|
1446
1561
|
}
|
|
1447
1562
|
default:
|
|
1448
|
-
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
|
|
1563
|
+
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | add | grant | revoke");
|
|
1449
1564
|
}
|
|
1450
1565
|
}
|
|
1451
1566
|
async function turnsCmd(sub, id, turnId, v) {
|
|
1452
|
-
if (sub === "show") return turnShow(id, turnId);
|
|
1567
|
+
if (sub === "show") return turnShow(id, turnId, !!v.full);
|
|
1453
1568
|
if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h \xB7 ghosty turns show <agent-id> [turn-id|last]");
|
|
1454
1569
|
id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
|
|
1455
1570
|
const q = new URLSearchParams();
|
|
@@ -1472,12 +1587,19 @@ async function turnsCmd(sub, id, turnId, v) {
|
|
|
1472
1587
|
out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
|
|
1473
1588
|
${dim(`${v.since ? `since ${v.since}: ` : ""}${tally} \xB7 where the time went: ghosty turns show ${id} <turn-id>`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
|
|
1474
1589
|
}
|
|
1475
|
-
async function turnShow(id, turnId) {
|
|
1476
|
-
id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last]");
|
|
1590
|
+
async function turnShow(id, turnId, full = false) {
|
|
1591
|
+
id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last] [--full]");
|
|
1477
1592
|
const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
|
|
1478
1593
|
out(r, () => {
|
|
1479
1594
|
const t = r.turn;
|
|
1480
|
-
const
|
|
1595
|
+
const clip = (s) => full || s.length <= 600 ? s : `${s.slice(0, 600)}\u2026 ${dim(`(+${s.length - 600} chars, --full)`)}`;
|
|
1596
|
+
const io = [
|
|
1597
|
+
`${lavender("in ")} ${r.input ? clip(r.input) : dim("(no copy of the message)")}`,
|
|
1598
|
+
`${lavender("out")} ${r.output ? clip(r.output) : dim("(no answer saved)")}`
|
|
1599
|
+
];
|
|
1600
|
+
const state = `${bold(t.id)} ${t.state === "error" ? red(t.state) : t.state} ${t.secs ?? "?"} s${t.session ? dim(` ${t.session}`) : ""}`;
|
|
1601
|
+
if (!r.totals) return [state, ...io, dim(`no timeline: ${r.why ?? "no transcript"}`)].join("\n");
|
|
1602
|
+
const head = `${state} ${dim(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`)}`;
|
|
1481
1603
|
const line = (s) => {
|
|
1482
1604
|
const at = `+${s.at.toFixed(1)}s`.padStart(8);
|
|
1483
1605
|
if (s.kind === "tool") return `${at} ${bold(s.tool ?? "?")} ${dim(s.input ?? "")} ${s.secs === void 0 ? red("(no result)") : `${s.secs} s`}${s.error ? red(" error") : ""}`;
|
|
@@ -1485,7 +1607,7 @@ async function turnShow(id, turnId) {
|
|
|
1485
1607
|
return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
|
|
1486
1608
|
};
|
|
1487
1609
|
const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
|
|
1488
|
-
return [head, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
|
|
1610
|
+
return [head, ...io, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
|
|
1489
1611
|
});
|
|
1490
1612
|
}
|
|
1491
1613
|
async function agentsDoctor(id, v) {
|
|
@@ -1507,7 +1629,7 @@ async function agentsDoctor(id, v) {
|
|
|
1507
1629
|
}
|
|
1508
1630
|
if (problems) {
|
|
1509
1631
|
if (!globals.json) info(dim("Exit 1: problems found."));
|
|
1510
|
-
|
|
1632
|
+
return exitAfterFlush(EXIT.API);
|
|
1511
1633
|
}
|
|
1512
1634
|
}
|
|
1513
1635
|
|
|
@@ -1583,10 +1705,10 @@ async function credentialsCmd2(sub, provider, v) {
|
|
|
1583
1705
|
}
|
|
1584
1706
|
|
|
1585
1707
|
// src/commands/channels.ts
|
|
1586
|
-
import { readFile as
|
|
1708
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
1587
1709
|
|
|
1588
1710
|
// src/wa-auth.ts
|
|
1589
|
-
import { readdir as readdir2, readFile as
|
|
1711
|
+
import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
|
|
1590
1712
|
import { join as join4 } from "node:path";
|
|
1591
1713
|
var TYPES = ["app-state-sync-version", "app-state-sync-key", "sender-key-memory", "sender-key", "lid-mapping", "device-list", "identity-key", "pre-key", "session", "tctoken"];
|
|
1592
1714
|
function keyOfFileName(name) {
|
|
@@ -1601,7 +1723,7 @@ function keyOfFileName(name) {
|
|
|
1601
1723
|
async function readMultiFileAuth(dir) {
|
|
1602
1724
|
const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
|
|
1603
1725
|
if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
|
|
1604
|
-
const authCreds = JSON.parse(await
|
|
1726
|
+
const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
|
|
1605
1727
|
const authKeys = {};
|
|
1606
1728
|
const counts = {};
|
|
1607
1729
|
const skipped = [];
|
|
@@ -1612,7 +1734,7 @@ async function readMultiFileAuth(dir) {
|
|
|
1612
1734
|
skipped.push(f);
|
|
1613
1735
|
continue;
|
|
1614
1736
|
}
|
|
1615
|
-
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await
|
|
1737
|
+
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join4(dir, f), "utf8"));
|
|
1616
1738
|
counts[k.type] = (counts[k.type] ?? 0) + 1;
|
|
1617
1739
|
}
|
|
1618
1740
|
return { authCreds, authKeys, counts, skipped };
|
|
@@ -1631,6 +1753,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
|
|
|
1631
1753
|
if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
|
|
1632
1754
|
if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
|
|
1633
1755
|
if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
|
|
1756
|
+
if (sub === "history") return historyCmd(id, v);
|
|
1634
1757
|
if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
|
|
1635
1758
|
if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
|
|
1636
1759
|
const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
|
|
@@ -1646,7 +1769,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
|
|
|
1646
1769
|
}));
|
|
1647
1770
|
return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
|
|
1648
1771
|
}
|
|
1649
|
-
if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable | link | unlink \xB7 ghosty channels wa import | status | pause | groups");
|
|
1772
|
+
if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable | link | unlink | history \xB7 ghosty channels wa import | status | pause | groups");
|
|
1650
1773
|
if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
|
|
1651
1774
|
const n = find(cur.numbers, ref);
|
|
1652
1775
|
const on = sub === "enable";
|
|
@@ -1738,7 +1861,7 @@ ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone numbe
|
|
|
1738
1861
|
const m = await readMultiFileAuth(v["from-dir"]);
|
|
1739
1862
|
info(dim(`${v["from-dir"]}: ${Object.entries(m.counts).map(([t, n]) => `${t} ${n}`).join(" \xB7 ") || "no keys"}${m.skipped.length ? ` \xB7 skipped ${m.skipped.length} unknown file(s)` : ""}`));
|
|
1740
1863
|
data = { authCreds: m.authCreds, authKeys: m.authKeys };
|
|
1741
|
-
} else data = JSON.parse(await
|
|
1864
|
+
} else data = JSON.parse(await readFile8(file, "utf8"));
|
|
1742
1865
|
const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
|
|
1743
1866
|
const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
|
|
1744
1867
|
if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
|
|
@@ -1765,10 +1888,63 @@ ${sub === "link" ? "From now on this number's messages reach this agent in gs."
|
|
|
1765
1888
|
const r = await api(path, { method: "POST", body });
|
|
1766
1889
|
return out(r, () => `${lavender("\u2713")} ${sub === "link" ? `Linked (link ${r.linkId}). Check: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m` : "Unlinked: Formmy points where it did before."}`);
|
|
1767
1890
|
}
|
|
1891
|
+
async function historyCmd(id, v) {
|
|
1892
|
+
if (!v.integration) throw usage("Missing --integration.", `Example: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
|
|
1893
|
+
const days = Number(v.days ?? 7);
|
|
1894
|
+
if (!Number.isInteger(days) || days < 1 || days > 30) throw usage("--days takes 1 to 30.");
|
|
1895
|
+
const path = `/api/v2/agents/${enc7(id)}/whatsapp/history`;
|
|
1896
|
+
const c = await api(path, { method: "POST", body: { integrationId: v.integration, days, dryRun: true } });
|
|
1897
|
+
const plan = `${c.conversations} conversation(s), ${c.messages} message(s), ${c.orders} order(s) from the last ${days} day(s)${c.phoneNumber ? ` of +${c.phoneNumber}` : ""} \u2192 board ${c.board}`;
|
|
1898
|
+
if (v["dry-run"]) return out(c, () => `${plan}
|
|
1899
|
+
${dim("Dry run: nothing imported. Re-running later never duplicates.")}`);
|
|
1900
|
+
if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(`Nothing to import: ${plan}.`));
|
|
1901
|
+
await confirm(`${bold(plan)}
|
|
1902
|
+
Cards appear on the team's board (no "new lead" alerts).`, { yes: v.yes, retry: `ghosty channels whatsapp history ${id} --integration ${v.integration} --days ${days}` });
|
|
1903
|
+
const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0 };
|
|
1904
|
+
let cursor = 0;
|
|
1905
|
+
let done = 0;
|
|
1906
|
+
while (cursor !== null) {
|
|
1907
|
+
const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
|
|
1908
|
+
for (const k of Object.keys(sum)) sum[k] += p[k];
|
|
1909
|
+
done += p.page;
|
|
1910
|
+
if (!globals.json) info(dim(` ${done}/${p.total} conversations`));
|
|
1911
|
+
cursor = p.nextCursor;
|
|
1912
|
+
}
|
|
1913
|
+
out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)${sum.suppressed ? dim(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`) : ""}`);
|
|
1914
|
+
}
|
|
1768
1915
|
|
|
1769
1916
|
// src/commands/board.ts
|
|
1770
|
-
|
|
1771
|
-
|
|
1917
|
+
var WHO = { user: "customer", assistant: "agent", operator: "operator" };
|
|
1918
|
+
async function boardLs(id, v) {
|
|
1919
|
+
const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {} });
|
|
1920
|
+
const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations?${qs}`);
|
|
1921
|
+
const rows = r.conversations.map((c) => ({ folio: c.folio ? `#${c.folio}` : "", name: c.name ?? "", phone: c.phone, column: c.column ?? "", msgs: c.messages, last: (c.lastActivity ?? "").slice(0, 16).replace("T", " ") }));
|
|
1922
|
+
out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last"])}
|
|
1923
|
+
${dim(`\xAB${r.board}\xBB \xB7 thread: ghosty board show ${id} <folio|phone>`)}` : dim(`No cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`));
|
|
1924
|
+
}
|
|
1925
|
+
async function boardShow(id, ref, v) {
|
|
1926
|
+
if (!ref) throw usage("Missing conversation.", `Example: ghosty board show ${id} "#12" (folio, phone, session or id; list them with ghosty board ls ${id})`);
|
|
1927
|
+
const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations/${encodeURIComponent(ref)}${v.limit ? `?limit=${encodeURIComponent(v.limit)}` : ""}`);
|
|
1928
|
+
const c = r.conversation;
|
|
1929
|
+
const head = `${bold(c.name ?? c.phone)} ${dim(`${c.folio ? `#${c.folio} \xB7 ` : ""}+${c.phone} \xB7 ${c.column ?? "no column"} \xB7 ${c.session}`)}`;
|
|
1930
|
+
const line = (m) => {
|
|
1931
|
+
const at = m.createdAt.slice(5, 16).replace("T", " ");
|
|
1932
|
+
const media = m.mediaType || m.mediaFilename ? `
|
|
1933
|
+
${dim(`\u{1F4CE} ${m.mediaType ?? "file"} ${m.mediaFilename ?? ""} \xB7 ${m.mediaCopied ? "copy in gs" : "no copy"}`)}` : "";
|
|
1934
|
+
return `${dim(at)} ${lavender((WHO[m.role] ?? m.role).padEnd(8))} ${m.content.replace(/\n/g, "\n ")}${media}`;
|
|
1935
|
+
};
|
|
1936
|
+
out(r, () => [head, ...r.messages.map(line)].join("\n"));
|
|
1937
|
+
}
|
|
1938
|
+
async function boardCmd(sub, id, v, ref) {
|
|
1939
|
+
if (sub === "ls" || sub === "list") {
|
|
1940
|
+
if (!id) throw usage("Missing agent id.", "Example: ghosty board ls <agent-id> [--q text] [--limit 20]");
|
|
1941
|
+
return boardLs(id, v);
|
|
1942
|
+
}
|
|
1943
|
+
if (sub === "show") {
|
|
1944
|
+
if (!id) throw usage("Missing agent id.", 'Example: ghosty board show <agent-id> "#12"');
|
|
1945
|
+
return boardShow(id, ref, v);
|
|
1946
|
+
}
|
|
1947
|
+
if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board ls <agent-id> \xB7 ghosty board show <agent-id> <folio|phone> \xB7 ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1772
1948
|
if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1773
1949
|
const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
|
|
1774
1950
|
if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
|
|
@@ -1795,11 +1971,16 @@ var HELPS = {
|
|
|
1795
1971
|
summary: "Create, inspect, change, check and delete agents.",
|
|
1796
1972
|
usage: [
|
|
1797
1973
|
"ghosty agents ls",
|
|
1798
|
-
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
|
|
1799
|
-
"ghosty agents get <agent-id> [--fields name,model,prompt]",
|
|
1800
|
-
"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]",
|
|
1974
|
+
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026] [--workspace SLUG] [--channels teams=off,\u2026]",
|
|
1975
|
+
"ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
|
|
1976
|
+
"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] [--toolsets crm,sales | +crm,-sales]",
|
|
1977
|
+
"ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
|
|
1801
1978
|
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1802
1979
|
"ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
|
|
1980
|
+
"ghosty agents docs add <agent-id> --url URL | --file PATH [--name N] [--description TEXT] [--grant read|write|no] [--no-deliver]",
|
|
1981
|
+
"ghosty agents share <agent-id> <email> [--role editor|admin]",
|
|
1982
|
+
"ghosty agents unshare <agent-id> <email> [--yes]",
|
|
1983
|
+
"ghosty agents shares <agent-id>",
|
|
1803
1984
|
"ghosty agents restart <agent-id>",
|
|
1804
1985
|
"ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
|
|
1805
1986
|
'ghosty agents demo <agent-id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate]',
|
|
@@ -1807,13 +1988,21 @@ var HELPS = {
|
|
|
1807
1988
|
],
|
|
1808
1989
|
flags: [
|
|
1809
1990
|
["--fields a,b", "get: return only these fields"],
|
|
1810
|
-
["--
|
|
1991
|
+
["--prompt-out FILE", "get: write the prompt to FILE (edit it and come back with set --prompt-file)"],
|
|
1992
|
+
["--workspace SLUG", "create: born in that workspace (you own or administer it), owned by its owner and active in its Teams"],
|
|
1993
|
+
["--channels k=on|off,\u2026", "create/set: turn channels on/off: teams, web, whatsapp, messenger, wa (personal WhatsApp)"],
|
|
1994
|
+
["--toolsets LIST", "set: gs tool groups it gets (crm, sales\u2026): a list replaces; +x / -x add or remove"],
|
|
1995
|
+
["--prompt-base ID", "set: this agent's prompt = that agent's prompt + --prompt-overlay; regenerates when the base changes. none = unlink (keeps today's prompt)"],
|
|
1996
|
+
["--prompt-overlay FILE", 'set: JSON {"edits":[{"what","find","replace","regex"?,"all"?}]}, applied in order; each must find its place or nothing changes'],
|
|
1811
1997
|
["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
|
|
1812
1998
|
["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
|
|
1813
1999
|
["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
|
|
1814
2000
|
["--verbose", "doctor: also list the checks that passed"],
|
|
1815
2001
|
["--write", "docs grant: the agent may replace the document (default: read only)"],
|
|
1816
|
-
["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
|
|
2002
|
+
["--no-deliver", "docs grant/add: it can read it but never send it to whoever writes"],
|
|
2003
|
+
["--url URL | --file PATH", "docs add: where the document comes from (public URL, up to 25 MB, or a local file)"],
|
|
2004
|
+
["--grant read|write|no", "docs add: access for this agent (default read; no = only upload to the account)"],
|
|
2005
|
+
["--role R", "share: editor (configures and uses it) or admin (also shares it); again on a shared email changes the role"],
|
|
1817
2006
|
["--yes", "rm: skip the confirmation (required without a terminal)"],
|
|
1818
2007
|
["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
|
|
1819
2008
|
["--on | --off", "demo: turn the public link on or off"],
|
|
@@ -1835,16 +2024,23 @@ var HELPS = {
|
|
|
1835
2024
|
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1836
2025
|
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1837
2026
|
"ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
|
|
2027
|
+
'ghosty agents docs add <id> --url https://example.com/lista.pdf --description "Precios 2026"',
|
|
2028
|
+
"ghosty agents create --name Pia --engine claude --workspace ventas --channels teams=off",
|
|
2029
|
+
"ghosty agents set <id> --toolsets +crm",
|
|
2030
|
+
"ghosty agents share <id> ana@empresa.com --role admin",
|
|
2031
|
+
"ghosty agents get <id> --prompt-out PROMPT.md",
|
|
2032
|
+
"ghosty agents set <pia-id> --prompt-base <tania-id> --prompt-overlay capa.json --dry-run --out /tmp/pia.md",
|
|
1838
2033
|
'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
|
|
1839
2034
|
"ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
|
|
1840
2035
|
],
|
|
1841
|
-
notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI.", "demo with no flags only prints its status: link, expiry, caps and numbers."],
|
|
2036
|
+
notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI.", "A derived prompt can't be edited directly (409): change the base or the overlay, or unlink it with --prompt-base none.", "demo with no flags only prints its status: link, expiry, caps and numbers."],
|
|
1842
2037
|
learn: `${REF}#agentes`
|
|
1843
2038
|
},
|
|
1844
2039
|
skills: {
|
|
1845
2040
|
summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
|
|
1846
2041
|
usage: [
|
|
1847
2042
|
"ghosty skills ls <agent-id>",
|
|
2043
|
+
"ghosty skills on|off <agent-id> <slug>",
|
|
1848
2044
|
"ghosty skills get <agent-id> <slug> [--out DIR]",
|
|
1849
2045
|
"ghosty skills templates <agent-id>",
|
|
1850
2046
|
"ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
|
|
@@ -1901,15 +2097,16 @@ var HELPS = {
|
|
|
1901
2097
|
},
|
|
1902
2098
|
turns: {
|
|
1903
2099
|
summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
|
|
1904
|
-
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last]"],
|
|
2100
|
+
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
|
|
1905
2101
|
flags: [
|
|
1906
2102
|
["--errors", "only failed turns (same as --status error)"],
|
|
1907
2103
|
["--status S", "running | done | stopped | error"],
|
|
1908
2104
|
["--since T", "30m, 1h, 2d or an ISO date"],
|
|
1909
|
-
["--limit N", "max rows (default 20, max 500)"]
|
|
2105
|
+
["--limit N", "max rows (default 20, max 500)"],
|
|
2106
|
+
["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"]
|
|
1910
2107
|
],
|
|
1911
2108
|
examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100", "ghosty turns show <id> # last turn: each tool, its seconds, output tokens"],
|
|
1912
|
-
notes: ["show reads the worker's transcript
|
|
2109
|
+
notes: ["show prints what came in and what went out on any engine or channel (WhatsApp groups too); the timeline reads the worker's transcript, so pool engines only (not ACP/goose)."],
|
|
1913
2110
|
learn: `${REF}#turnos`
|
|
1914
2111
|
},
|
|
1915
2112
|
files: {
|
|
@@ -1928,13 +2125,14 @@ var HELPS = {
|
|
|
1928
2125
|
summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
|
|
1929
2126
|
usage: [
|
|
1930
2127
|
'ghosty try <agent-id> "message" [--session ID] [--reset]',
|
|
1931
|
-
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
|
|
2128
|
+
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE [--as-formmy]] [--history FILE] [--reset]',
|
|
1932
2129
|
"ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
|
|
1933
2130
|
],
|
|
1934
2131
|
flags: [
|
|
1935
2132
|
["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
|
|
1936
2133
|
["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
|
|
1937
2134
|
["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
|
|
2135
|
+
["--as-formmy", "with --media: it arrives like a real Formmy forward (proxy with credential, copy to gs, transcription) and says if the copy worked"],
|
|
1938
2136
|
["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
|
|
1939
2137
|
["--reset", "forget that test session (with --waba also its test thread)"],
|
|
1940
2138
|
["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
|
|
@@ -1951,7 +2149,8 @@ var HELPS = {
|
|
|
1951
2149
|
],
|
|
1952
2150
|
notes: [
|
|
1953
2151
|
"Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
|
|
1954
|
-
"--whatsapp is now an alias of --waba."
|
|
2152
|
+
"--whatsapp is now an alias of --waba.",
|
|
2153
|
+
"--waba lists everything it would send, tools included: \u{1F4CD} location, \u{1F4C4} files, \u{1F3A4} voice, \u21A9\uFE0F quotes, \u{1F518} buttons, \u{1F464} contacts, reactions."
|
|
1955
2154
|
],
|
|
1956
2155
|
learn: `${REF}#turnos-de-prueba`
|
|
1957
2156
|
},
|
|
@@ -1998,6 +2197,7 @@ HELPS.channels = {
|
|
|
1998
2197
|
"ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
|
|
1999
2198
|
"ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
|
|
2000
2199
|
"ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
|
|
2200
|
+
"ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--dry-run] [--yes]",
|
|
2001
2201
|
"ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
|
|
2002
2202
|
"ghosty channels wa pair <agent-id> --phone 5215512345678",
|
|
2003
2203
|
"ghosty channels wa status|pause <agent-id>",
|
|
@@ -2011,7 +2211,8 @@ HELPS.channels = {
|
|
|
2011
2211
|
["--role R", "groups: equipo (the team: writes to databases, admin tools) or cliente (like any customer)"],
|
|
2012
2212
|
["--on | --off", "groups set: the agent answers in that group, or stops"],
|
|
2013
2213
|
["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
|
|
2014
|
-
["--integration ID", "link/unlink: the WhatsApp Business number's Integration id in Formmy"],
|
|
2214
|
+
["--integration ID", "link/unlink/history: the WhatsApp Business number's Integration id in Formmy"],
|
|
2215
|
+
["--days N", "history: conversations active in the last N days (1-30, default 7)"],
|
|
2015
2216
|
["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
|
|
2016
2217
|
["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
|
|
2017
2218
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
|
|
@@ -2027,6 +2228,7 @@ HELPS.channels = {
|
|
|
2027
2228
|
"ghosty channels wa groups ls <id>",
|
|
2028
2229
|
'ghosty channels wa groups set <id> "Pia-cotizadora" --role equipo --on --dry-run',
|
|
2029
2230
|
"ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
|
|
2231
|
+
"ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
|
|
2030
2232
|
"ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
|
|
2031
2233
|
],
|
|
2032
2234
|
notes: [
|
|
@@ -2034,22 +2236,29 @@ HELPS.channels = {
|
|
|
2034
2236
|
"Disabling keeps logging messages; nobody answers them.",
|
|
2035
2237
|
"Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
|
|
2036
2238
|
"link moves a number that already lives in Formmy (EasyBits, a droplet) without redoing Meta's signup; unlink sends it back. Only the owner, an admin of the agent or Ghosty staff.",
|
|
2239
|
+
"history brings the number's recent cards, messages and orders from Formmy to the board (link it first). Re-running never duplicates: run it before the cutover and again right at it.",
|
|
2037
2240
|
"import: turn off the EasyBits socket first WITHOUT logout (two sockets knock each other off; a logout invalidates the keys). Delete the file afterwards."
|
|
2038
2241
|
],
|
|
2039
2242
|
learn: `${REF}#canales-de-whatsapp`
|
|
2040
2243
|
};
|
|
2041
2244
|
HELPS.board = {
|
|
2042
2245
|
summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
|
|
2043
|
-
usage: [
|
|
2246
|
+
usage: [
|
|
2247
|
+
"ghosty board ls <agent-id> [--q TEXT] [--limit 20]",
|
|
2248
|
+
"ghosty board show <agent-id> <folio|phone|session|id> [--limit 20]",
|
|
2249
|
+
"ghosty board archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"
|
|
2250
|
+
],
|
|
2044
2251
|
flags: [
|
|
2252
|
+
["--q TEXT", "ls: cards whose name, phone or last message contains TEXT"],
|
|
2253
|
+
["--limit N", "ls: cards (default 20) \xB7 show: last N messages (default 20, max 500)"],
|
|
2045
2254
|
["--integration ID", "cards from this channel/integration (e.g. prueba)"],
|
|
2046
2255
|
["--column NAME", "cards in this column"],
|
|
2047
2256
|
["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
|
|
2048
2257
|
["-n, --dry-run", "count and show a sample, change nothing"],
|
|
2049
2258
|
["--yes", "skip the confirmation (required without a terminal)"]
|
|
2050
2259
|
],
|
|
2051
|
-
examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
2052
|
-
notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
2260
|
+
examples: ["ghosty board ls <id> --q 7712", 'ghosty board show <id> "#12"', "ghosty board show <id> 5210000009922 --limit 50", "ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
2261
|
+
notes: ["show prints each message with who wrote it (customer, agent, operator), its attachment and whether gs keeps a copy of it.", "Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
2053
2262
|
learn: `${REF}#tablero`
|
|
2054
2263
|
};
|
|
2055
2264
|
HELPS.conversations = HELPS.chat;
|
|
@@ -2073,7 +2282,7 @@ function helpText(cmd) {
|
|
|
2073
2282
|
}
|
|
2074
2283
|
|
|
2075
2284
|
// src/update.ts
|
|
2076
|
-
import { mkdir as mkdir4, readFile as
|
|
2285
|
+
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
|
|
2077
2286
|
import { homedir as homedir3 } from "node:os";
|
|
2078
2287
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2079
2288
|
var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
|
|
@@ -2089,7 +2298,7 @@ async function latestVersion() {
|
|
|
2089
2298
|
if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
|
|
2090
2299
|
let cache = {};
|
|
2091
2300
|
try {
|
|
2092
|
-
cache = JSON.parse(await
|
|
2301
|
+
cache = JSON.parse(await readFile9(FILE, "utf8"));
|
|
2093
2302
|
} catch {
|
|
2094
2303
|
}
|
|
2095
2304
|
if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
|
|
@@ -2122,13 +2331,18 @@ ${bold("Session")}
|
|
|
2122
2331
|
${bold("Agents")}
|
|
2123
2332
|
agents ls List your agents
|
|
2124
2333
|
agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
2125
|
-
|
|
2334
|
+
[--workspace SLUG] [--channels teams=off,\u2026] born in a workspace, active in its Teams
|
|
2335
|
+
agents get <id> Show an agent's config [--fields name,model,prompt] [--prompt-out FILE]
|
|
2126
2336
|
agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
2127
2337
|
[--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
|
|
2128
2338
|
[--channels wa=on,teams=off] turn channels on/off (wa = personal WhatsApp)
|
|
2129
2339
|
[--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
|
|
2340
|
+
[--toolsets crm,sales | +crm,-sales] gs tool groups it gets
|
|
2341
|
+
[--prompt-base AGENT|none --prompt-overlay FILE [--dry-run]] prompt = another agent's + edits, kept in sync
|
|
2130
2342
|
agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
|
|
2131
2343
|
agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
|
|
2344
|
+
agents docs add <id> Upload a document and grant it --url URL | --file PATH [--description] [--grant read|write|no]
|
|
2345
|
+
agents share|unshare <id> <email> Share it without handing it over [--role editor|admin] \xB7 agents shares <id> lists them
|
|
2132
2346
|
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
2133
2347
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
2134
2348
|
agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
|
|
@@ -2137,27 +2351,31 @@ ${bold("Agents")}
|
|
|
2137
2351
|
|
|
2138
2352
|
${bold("Configure an agent")}
|
|
2139
2353
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
2354
|
+
skills on|off <id> <slug> Turn one of its skills (or one of Ghosty's) on or off
|
|
2140
2355
|
skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; get lists, --out DIR saves; rm keeps a copy)
|
|
2141
2356
|
skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
|
|
2142
2357
|
dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run]
|
|
2143
2358
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
2144
2359
|
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
2145
2360
|
channels whatsapp link|unlink <id> --integration ID --answer on|off Bring a WhatsApp Business number from Formmy/EasyBits to this agent, or send it back [--forward] [--dry-run]
|
|
2361
|
+
channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages and orders from Formmy (no duplicates) [--dry-run]
|
|
2146
2362
|
channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
|
|
2147
2363
|
channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
|
|
2148
2364
|
channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
|
|
2149
2365
|
channels wa groups ls|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--on|--off] [--dry-run]
|
|
2366
|
+
board ls <id> Recent cards on its sales board [--q TEXT] [--limit 20]
|
|
2367
|
+
board show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
|
|
2150
2368
|
board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
2151
2369
|
|
|
2152
2370
|
${bold("Talk to an agent")}
|
|
2153
2371
|
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
2154
|
-
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
|
|
2372
|
+
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent; lists what it would send (tools too) [--media FILE [--as-formmy]] [--history FILE]
|
|
2155
2373
|
--replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
|
|
2156
2374
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
2157
2375
|
conversations ls <id> List conversations
|
|
2158
2376
|
conversations show <id> <conversation> What was said [--limit 20] (0 = all)
|
|
2159
2377
|
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
2160
|
-
turns show <id> [turn]
|
|
2378
|
+
turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
|
|
2161
2379
|
|
|
2162
2380
|
${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
|
|
2163
2381
|
schedule ls <id> Pending turns and recent runs
|
|
@@ -2184,7 +2402,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
|
|
|
2184
2402
|
${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.")}
|
|
2185
2403
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
2186
2404
|
var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
|
|
2187
|
-
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo", "box"];
|
|
2405
|
+
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
|
|
2188
2406
|
function unknown(what, input, known, helpCmd) {
|
|
2189
2407
|
const s = suggest(input, known);
|
|
2190
2408
|
return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
|
|
@@ -2262,6 +2480,17 @@ async function main() {
|
|
|
2262
2480
|
since: { type: "string" },
|
|
2263
2481
|
limit: { type: "string" },
|
|
2264
2482
|
verbose: { type: "boolean" },
|
|
2483
|
+
full: { type: "boolean" },
|
|
2484
|
+
"as-formmy": { type: "boolean" },
|
|
2485
|
+
q: { type: "string" },
|
|
2486
|
+
"prompt-out": { type: "string" },
|
|
2487
|
+
toolsets: { type: "string" },
|
|
2488
|
+
"prompt-base": { type: "string" },
|
|
2489
|
+
"prompt-overlay": { type: "string" },
|
|
2490
|
+
url: { type: "string" },
|
|
2491
|
+
description: { type: "string" },
|
|
2492
|
+
grant: { type: "string" },
|
|
2493
|
+
days: { type: "string" },
|
|
2265
2494
|
voice: { type: "string" },
|
|
2266
2495
|
"voice-replies": { type: "string" },
|
|
2267
2496
|
"auto-stage": { type: "string" },
|
|
@@ -2293,7 +2522,7 @@ async function main() {
|
|
|
2293
2522
|
const tokens = splitTokens(pos, v);
|
|
2294
2523
|
globals.token = tokens.session;
|
|
2295
2524
|
globals.profile = v.profile;
|
|
2296
|
-
if (v.version) return out({ version: "0.
|
|
2525
|
+
if (v.version) return out({ version: "0.15.0" }, () => "0.15.0");
|
|
2297
2526
|
const [cmd, sub, a1, a2] = pos;
|
|
2298
2527
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
2299
2528
|
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
@@ -2322,7 +2551,8 @@ async function main() {
|
|
|
2322
2551
|
case "agents":
|
|
2323
2552
|
if (sub === "ls" || sub === "list" || !sub) return agentsLs();
|
|
2324
2553
|
if (sub === "create" || sub === "new") return agentsCreate(v);
|
|
2325
|
-
if (sub === "get") return agentsGet(a1, v.fields);
|
|
2554
|
+
if (sub === "get") return agentsGet(a1, v.fields, v["prompt-out"]);
|
|
2555
|
+
if (sub === "share" || sub === "unshare" || sub === "shares") return agentsShare(sub, a1, a2, v);
|
|
2326
2556
|
if (sub === "set") return agentsSet(a1, v);
|
|
2327
2557
|
if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
|
|
2328
2558
|
if (sub === "doctor") return agentsDoctor(a1, v);
|
|
@@ -2343,7 +2573,7 @@ async function main() {
|
|
|
2343
2573
|
case "turns":
|
|
2344
2574
|
return turnsCmd(sub, a1, a2, v);
|
|
2345
2575
|
case "board":
|
|
2346
|
-
return boardCmd(sub, a1, v);
|
|
2576
|
+
return boardCmd(sub, a1, v, a2);
|
|
2347
2577
|
case "channels":
|
|
2348
2578
|
if (a1 === "groups") return groupsCmd(pos.slice(3).filter((x) => x !== void 0), v);
|
|
2349
2579
|
return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
|
|
@@ -2378,19 +2608,19 @@ async function main() {
|
|
|
2378
2608
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
2379
2609
|
}
|
|
2380
2610
|
}
|
|
2381
|
-
var notice = updateNotice("0.
|
|
2611
|
+
var notice = updateNotice("0.15.0").catch(() => null);
|
|
2382
2612
|
main().then(
|
|
2383
2613
|
async () => {
|
|
2384
2614
|
const n = await notice;
|
|
2385
2615
|
if (n) info(dim(n));
|
|
2386
|
-
|
|
2616
|
+
return exitAfterFlush(EXIT.OK);
|
|
2387
2617
|
},
|
|
2388
2618
|
async (e) => {
|
|
2389
2619
|
if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
|
|
2390
2620
|
const n = await notice;
|
|
2391
|
-
fail(usage(e.message, n ? `${n}
|
|
2621
|
+
return fail(usage(e.message, n ? `${n}
|
|
2392
2622
|
This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
|
|
2393
2623
|
}
|
|
2394
|
-
fail(e);
|
|
2624
|
+
return fail(e);
|
|
2395
2625
|
}
|
|
2396
2626
|
);
|
package/package.json
CHANGED