@ghostystudio/cli 0.13.1 → 0.14.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 +23 -4
- package/dist/index.js +231 -46
- 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,16 +28,19 @@ 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
|
|
@@ -79,6 +84,20 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
|
79
84
|
- `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
85
|
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
81
86
|
|
|
87
|
+
### Changes in 0.14.0
|
|
88
|
+
|
|
89
|
+
- 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.
|
|
90
|
+
- `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`.
|
|
91
|
+
- `turns show` prints what came in and what the agent answered, on any engine or channel (`--full` for the whole text).
|
|
92
|
+
- `agents doctor` flags databases the prompt names that the agent doesn't have (EasyBits ids left over from a migration).
|
|
93
|
+
- `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).
|
|
94
|
+
- `channels whatsapp history`: bring a WhatsApp Business number's recent cards, messages and orders from Formmy to the board.
|
|
95
|
+
|
|
96
|
+
### Changes in 0.13.2
|
|
97
|
+
|
|
98
|
+
- `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.
|
|
99
|
+
- `agents set --channels wa=on,teams=off` (wa = personal WhatsApp).
|
|
100
|
+
|
|
82
101
|
### Changes in 0.13.1
|
|
83
102
|
|
|
84
103
|
- `channels wa import --from-dir DIR`: import a Baileys multi-file auth folder (`creds.json` + one file per key, e.g. nanoclaw's `store/auth`); it undoes Baileys' file-name escaping per key type.
|
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.14.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.14.0"}`,
|
|
328
334
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
329
335
|
...opts.headers
|
|
330
336
|
},
|
|
@@ -522,8 +528,18 @@ function parseEnv(raw) {
|
|
|
522
528
|
}
|
|
523
529
|
return env;
|
|
524
530
|
}
|
|
531
|
+
function parseChannels(raw) {
|
|
532
|
+
if (!raw) return void 0;
|
|
533
|
+
const ch = {};
|
|
534
|
+
for (const pair of raw.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
535
|
+
const [k, val] = pair.split("=");
|
|
536
|
+
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)");
|
|
537
|
+
ch[k] = val === "on";
|
|
538
|
+
}
|
|
539
|
+
return ch;
|
|
540
|
+
}
|
|
525
541
|
async function agentsCreate(v) {
|
|
526
|
-
const name = need(v.name, "--name", "ghosty agents create --name Moon --engine ghosty-lite --model gpt-5.6-luna");
|
|
542
|
+
const name = need(v.name, "--name", "ghosty agents create --name Moon --engine ghosty-lite --model gpt-5.6-luna [--workspace SLUG]");
|
|
527
543
|
const body = { name };
|
|
528
544
|
if (v.engine) body.engine = v.engine;
|
|
529
545
|
if (v.model) body.model = v.model;
|
|
@@ -531,23 +547,65 @@ async function agentsCreate(v) {
|
|
|
531
547
|
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
532
548
|
const env = parseEnv(v.env);
|
|
533
549
|
if (env) body.env = env;
|
|
550
|
+
if (v.workspace) body.workspace = v.workspace;
|
|
551
|
+
const channels = parseChannels(v.channels);
|
|
552
|
+
if (channels) body.channels = channels;
|
|
534
553
|
const r = await api("/api/v2/me/agents", { method: "POST", body });
|
|
554
|
+
const where = r.workspace ? `
|
|
555
|
+
workspace: ${r.workspace}${r.handle ? ` \xB7 @${r.handle} in Teams` : dim(" \xB7 not active in Teams")}` : "";
|
|
535
556
|
out(r, () => `${lavender("\u2713")} Created ${r.name} ${dim(`(${r.engine} \xB7 ${r.model ?? "default"})`)}
|
|
536
|
-
id: ${r.id}`);
|
|
557
|
+
id: ${r.id}${where}`);
|
|
537
558
|
}
|
|
538
|
-
async function agentsGet(id, fields) {
|
|
559
|
+
async function agentsGet(id, fields, promptOut) {
|
|
539
560
|
id = need(id, "agent id", "ghosty agents get <agent-id>");
|
|
561
|
+
if (promptOut) {
|
|
562
|
+
const r = await api(agentPath(id, "?fields=prompt"));
|
|
563
|
+
const prompt = r.prompt ?? "";
|
|
564
|
+
await writeFile3(promptOut, prompt);
|
|
565
|
+
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}`)}`);
|
|
566
|
+
}
|
|
540
567
|
const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
|
|
541
568
|
out(await api(agentPath(id, q)));
|
|
542
569
|
}
|
|
543
570
|
async function agentsSet(id, v) {
|
|
544
571
|
id = need(id, "agent id", "ghosty agents set <agent-id> --prompt-file PROMPT.md");
|
|
572
|
+
if (v["prompt-base"]) {
|
|
573
|
+
const unlink = v["prompt-base"] === "none";
|
|
574
|
+
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)');
|
|
575
|
+
const promptBase = unlink ? null : { agentId: v["prompt-base"], overlay: JSON.parse(await readFile3(v["prompt-overlay"], "utf8")) };
|
|
576
|
+
const r2 = await api(agentPath(id), { method: "PATCH", body: { promptBase, ...v["dry-run"] ? { dryRun: true } : {} } });
|
|
577
|
+
if (v["dry-run"] && r2.prompt) {
|
|
578
|
+
const out_ = v.out;
|
|
579
|
+
if (out_) await writeFile3(out_, r2.prompt);
|
|
580
|
+
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.")}
|
|
581
|
+
${dim("Dry run: nothing changed.")}`);
|
|
582
|
+
}
|
|
583
|
+
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.`);
|
|
584
|
+
}
|
|
545
585
|
const body = {};
|
|
546
586
|
if (v.name) body.name = v.name;
|
|
547
587
|
if (v.engine) body.engine = v.engine;
|
|
548
588
|
if (v.model) body.model = v.model;
|
|
549
589
|
if (v.prompt) body.prompt = v.prompt;
|
|
550
590
|
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
591
|
+
const channels = parseChannels(v.channels);
|
|
592
|
+
if (channels) body.channels = channels;
|
|
593
|
+
if (v.toolsets) {
|
|
594
|
+
const items = v.toolsets.split(",").map((x) => x.trim()).filter(Boolean);
|
|
595
|
+
const cur = await api(agentPath(id, "?fields=tools"));
|
|
596
|
+
const known = Object.keys(cur.tools?.toolsets ?? {});
|
|
597
|
+
const bad = items.map((x) => x.replace(/^[+-]/, "")).filter((x) => !known.includes(x));
|
|
598
|
+
if (bad.length) throw usage(`Unknown toolset: ${bad.join(", ")}.`, `Known: ${known.join(", ")}`);
|
|
599
|
+
const delta = items.every((x) => /^[+-]/.test(x));
|
|
600
|
+
if (!delta && items.some((x) => /^[+-]/.test(x))) throw usage("Mix of plain and +/- toolsets.", "Either a full list (crm,sales) or changes (+crm,-sales).");
|
|
601
|
+
const next = new Set(delta ? known.filter((k) => cur.tools?.toolsets?.[k]?.granted) : []);
|
|
602
|
+
for (const x of items) {
|
|
603
|
+
if (x.startsWith("-")) next.delete(x.slice(1));
|
|
604
|
+
else next.add(x.replace(/^\+/, ""));
|
|
605
|
+
}
|
|
606
|
+
next.delete("core");
|
|
607
|
+
body.tools = { toolsets: [...next] };
|
|
608
|
+
}
|
|
551
609
|
const env = parseEnv(v.env);
|
|
552
610
|
if (env) body.env = env;
|
|
553
611
|
const connectors = {};
|
|
@@ -570,7 +628,7 @@ async function agentsSet(id, v) {
|
|
|
570
628
|
if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
|
|
571
629
|
body.autoStage = v["auto-stage"] === "on";
|
|
572
630
|
}
|
|
573
|
-
if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
|
|
631
|
+
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.");
|
|
574
632
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
575
633
|
out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
|
|
576
634
|
}
|
|
@@ -583,6 +641,25 @@ async function agentsRm(id, v = {}) {
|
|
|
583
641
|
}
|
|
584
642
|
out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
|
|
585
643
|
}
|
|
644
|
+
async function agentsShare(sub, id, email, v) {
|
|
645
|
+
id = need(id, "agent id", "ghosty agents share <agent-id> <email> [--role admin|editor]");
|
|
646
|
+
if (sub === "shares") {
|
|
647
|
+
const r2 = await api(agentPath(id, "/shares"));
|
|
648
|
+
const rows = r2.shares.map((x) => ({ email: x.email, role: x.role, accepted: x.acceptedAt ? "yes" : "not yet" }));
|
|
649
|
+
return out(r2, () => `${dim(`owner: ${r2.owner ?? "?"}`)}
|
|
650
|
+
${rows.length ? table(rows, ["email", "role", "accepted"]) : dim("Not shared with anyone.")}`);
|
|
651
|
+
}
|
|
652
|
+
email = need(email, "email", `ghosty agents ${sub} ${id} ana@empresa.com`);
|
|
653
|
+
if (sub === "unshare") {
|
|
654
|
+
requireYesIfHeadless(v.yes, `ghosty agents unshare ${id} ${email}`);
|
|
655
|
+
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}` });
|
|
656
|
+
const r2 = await api(agentPath(id, "/shares"), { method: "DELETE", body: { email } });
|
|
657
|
+
return out(r2, () => `${lavender("\u2713")} ${email} no longer has access.${r2.tokenRotated ? dim(" Token rotated.") : ""}`);
|
|
658
|
+
}
|
|
659
|
+
if (v.role && v.role !== "admin" && v.role !== "editor") throw usage("--role takes admin or editor.");
|
|
660
|
+
const r = await api(agentPath(id, "/shares"), { method: "POST", body: { email, ...v.role ? { role: v.role } : {} } });
|
|
661
|
+
out(r, () => `${lavender("\u2713")} ${r.already ? `Already shared with ${r.email}` : `Shared with ${r.email}; they got an email`} ${dim(`(${r.role})`)}.`);
|
|
662
|
+
}
|
|
586
663
|
async function agentsRestart(id) {
|
|
587
664
|
id = need(id, "agent id", "ghosty agents restart <agent-id>");
|
|
588
665
|
out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} Restarting \u2014 the next turn uses the new config.`);
|
|
@@ -672,7 +749,7 @@ async function readSkillDir(dir) {
|
|
|
672
749
|
}
|
|
673
750
|
var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
674
751
|
async function skillsCmd(sub, id, slug, v) {
|
|
675
|
-
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>";
|
|
752
|
+
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>";
|
|
676
753
|
id = need(id, "agent id", ex);
|
|
677
754
|
switch (sub ?? "ls") {
|
|
678
755
|
case "ls":
|
|
@@ -688,6 +765,13 @@ async function skillsCmd(sub, id, slug, v) {
|
|
|
688
765
|
${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
|
|
689
766
|
return;
|
|
690
767
|
}
|
|
768
|
+
case "on":
|
|
769
|
+
case "off": {
|
|
770
|
+
slug = need(slug, "skill slug", `ghosty skills ${sub} <agent-id> <slug>`);
|
|
771
|
+
const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PATCH", body: { enabled: sub === "on" } });
|
|
772
|
+
out(r, () => `${lavender("\u2713")} ${slug} ${sub === "on" ? "on" : "off"} ${dim(`(${r.source === "house" ? "Ghosty's" : "own"}${r.nota ? `; ${r.nota}` : ""})`)}`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
691
775
|
case "get": {
|
|
692
776
|
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
693
777
|
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
|
|
@@ -1136,7 +1220,7 @@ async function call(path, v, init = {}) {
|
|
|
1136
1220
|
"x-ghosty-ts": ts,
|
|
1137
1221
|
"x-ghosty-sig": sig,
|
|
1138
1222
|
"content-type": "application/json",
|
|
1139
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1223
|
+
"user-agent": `ghosty-cli/${"0.14.0"}`
|
|
1140
1224
|
},
|
|
1141
1225
|
body: method === "GET" ? void 0 : raw
|
|
1142
1226
|
});
|
|
@@ -1301,6 +1385,10 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
1301
1385
|
}
|
|
1302
1386
|
}
|
|
1303
1387
|
|
|
1388
|
+
// src/commands/data.ts
|
|
1389
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1390
|
+
import { basename as basename3 } from "node:path";
|
|
1391
|
+
|
|
1304
1392
|
// src/rows.ts
|
|
1305
1393
|
var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1306
1394
|
function parseRowScope(raw) {
|
|
@@ -1430,17 +1518,30 @@ async function agentDocsCmd(sub, id, doc, v) {
|
|
|
1430
1518
|
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
|
|
1431
1519
|
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"}.`);
|
|
1432
1520
|
}
|
|
1521
|
+
case "add": {
|
|
1522
|
+
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"');
|
|
1523
|
+
if (v.grant && !["read", "write", "no"].includes(v.grant)) throw usage("--grant takes read, write or no.");
|
|
1524
|
+
const body = {
|
|
1525
|
+
...v.url ? { url: v.url } : { contentBase64: (await readFile6(v.file)).toString("base64"), name: v.name ?? basename3(v.file) },
|
|
1526
|
+
...v.url && v.name ? { name: v.name } : {},
|
|
1527
|
+
...v.description ? { description: v.description } : {},
|
|
1528
|
+
grant: v.grant === "no" ? false : v.grant === "write" || v.write ? "write" : "read",
|
|
1529
|
+
...v["no-deliver"] ? { deliverable: false } : {}
|
|
1530
|
+
};
|
|
1531
|
+
const r = await api(agentPath2(id, "/documents"), { method: "POST", body });
|
|
1532
|
+
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}"`}`);
|
|
1533
|
+
}
|
|
1433
1534
|
case "revoke": {
|
|
1434
1535
|
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
|
|
1435
1536
|
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
|
|
1436
1537
|
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
|
|
1437
1538
|
}
|
|
1438
1539
|
default:
|
|
1439
|
-
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
|
|
1540
|
+
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | add | grant | revoke");
|
|
1440
1541
|
}
|
|
1441
1542
|
}
|
|
1442
1543
|
async function turnsCmd(sub, id, turnId, v) {
|
|
1443
|
-
if (sub === "show") return turnShow(id, turnId);
|
|
1544
|
+
if (sub === "show") return turnShow(id, turnId, !!v.full);
|
|
1444
1545
|
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]");
|
|
1445
1546
|
id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
|
|
1446
1547
|
const q = new URLSearchParams();
|
|
@@ -1463,12 +1564,19 @@ async function turnsCmd(sub, id, turnId, v) {
|
|
|
1463
1564
|
out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
|
|
1464
1565
|
${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}` : ""}.`));
|
|
1465
1566
|
}
|
|
1466
|
-
async function turnShow(id, turnId) {
|
|
1467
|
-
id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last]");
|
|
1567
|
+
async function turnShow(id, turnId, full = false) {
|
|
1568
|
+
id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last] [--full]");
|
|
1468
1569
|
const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
|
|
1469
1570
|
out(r, () => {
|
|
1470
1571
|
const t = r.turn;
|
|
1471
|
-
const
|
|
1572
|
+
const clip = (s) => full || s.length <= 600 ? s : `${s.slice(0, 600)}\u2026 ${dim(`(+${s.length - 600} chars, --full)`)}`;
|
|
1573
|
+
const io = [
|
|
1574
|
+
`${lavender("in ")} ${r.input ? clip(r.input) : dim("(no copy of the message)")}`,
|
|
1575
|
+
`${lavender("out")} ${r.output ? clip(r.output) : dim("(no answer saved)")}`
|
|
1576
|
+
];
|
|
1577
|
+
const state = `${bold(t.id)} ${t.state === "error" ? red(t.state) : t.state} ${t.secs ?? "?"} s${t.session ? dim(` ${t.session}`) : ""}`;
|
|
1578
|
+
if (!r.totals) return [state, ...io, dim(`no timeline: ${r.why ?? "no transcript"}`)].join("\n");
|
|
1579
|
+
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`)}`;
|
|
1472
1580
|
const line = (s) => {
|
|
1473
1581
|
const at = `+${s.at.toFixed(1)}s`.padStart(8);
|
|
1474
1582
|
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") : ""}`;
|
|
@@ -1476,7 +1584,7 @@ async function turnShow(id, turnId) {
|
|
|
1476
1584
|
return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
|
|
1477
1585
|
};
|
|
1478
1586
|
const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
|
|
1479
|
-
return [head, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
|
|
1587
|
+
return [head, ...io, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
|
|
1480
1588
|
});
|
|
1481
1589
|
}
|
|
1482
1590
|
async function agentsDoctor(id, v) {
|
|
@@ -1498,7 +1606,7 @@ async function agentsDoctor(id, v) {
|
|
|
1498
1606
|
}
|
|
1499
1607
|
if (problems) {
|
|
1500
1608
|
if (!globals.json) info(dim("Exit 1: problems found."));
|
|
1501
|
-
|
|
1609
|
+
return exitAfterFlush(EXIT.API);
|
|
1502
1610
|
}
|
|
1503
1611
|
}
|
|
1504
1612
|
|
|
@@ -1574,10 +1682,10 @@ async function credentialsCmd2(sub, provider, v) {
|
|
|
1574
1682
|
}
|
|
1575
1683
|
|
|
1576
1684
|
// src/commands/channels.ts
|
|
1577
|
-
import { readFile as
|
|
1685
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
1578
1686
|
|
|
1579
1687
|
// src/wa-auth.ts
|
|
1580
|
-
import { readdir as readdir2, readFile as
|
|
1688
|
+
import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
|
|
1581
1689
|
import { join as join4 } from "node:path";
|
|
1582
1690
|
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"];
|
|
1583
1691
|
function keyOfFileName(name) {
|
|
@@ -1592,7 +1700,7 @@ function keyOfFileName(name) {
|
|
|
1592
1700
|
async function readMultiFileAuth(dir) {
|
|
1593
1701
|
const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
|
|
1594
1702
|
if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
|
|
1595
|
-
const authCreds = JSON.parse(await
|
|
1703
|
+
const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
|
|
1596
1704
|
const authKeys = {};
|
|
1597
1705
|
const counts = {};
|
|
1598
1706
|
const skipped = [];
|
|
@@ -1603,7 +1711,7 @@ async function readMultiFileAuth(dir) {
|
|
|
1603
1711
|
skipped.push(f);
|
|
1604
1712
|
continue;
|
|
1605
1713
|
}
|
|
1606
|
-
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await
|
|
1714
|
+
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join4(dir, f), "utf8"));
|
|
1607
1715
|
counts[k.type] = (counts[k.type] ?? 0) + 1;
|
|
1608
1716
|
}
|
|
1609
1717
|
return { authCreds, authKeys, counts, skipped };
|
|
@@ -1622,15 +1730,23 @@ async function channelsCmd(kind, sub, id, ref, v) {
|
|
|
1622
1730
|
if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
|
|
1623
1731
|
if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
|
|
1624
1732
|
if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
|
|
1733
|
+
if (sub === "history") return historyCmd(id, v);
|
|
1625
1734
|
if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
|
|
1626
1735
|
if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
|
|
1627
1736
|
const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1737
|
+
const listing = !sub || sub === "ls" || sub === "list";
|
|
1738
|
+
const cur = await api(listing ? `${path}?formmy=1` : path);
|
|
1739
|
+
if (listing) {
|
|
1740
|
+
const fwd = (n2) => !n2.formmy ? dim("\u2014") : "error" in n2.formmy ? dim("?") : n2.formmy.forwards ? "yes" : lavender("NO");
|
|
1741
|
+
const rows = cur.numbers.map((n2) => ({
|
|
1742
|
+
number: n2.phoneNumber ? `+${n2.phoneNumber}${n2.label !== n2.phoneNumber && n2.label !== n2.id ? ` ${n2.label}` : ""}` : n2.label,
|
|
1743
|
+
answers: n2.mine ? lavender("this agent") : n2.recordsOnly ? "this agent (records only)" : n2.takenByOther ? "another agent" : dim("nobody (silent)"),
|
|
1744
|
+
"formmy forwards": fwd(n2),
|
|
1745
|
+
id: n2.id
|
|
1746
|
+
}));
|
|
1747
|
+
return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
|
|
1632
1748
|
}
|
|
1633
|
-
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");
|
|
1749
|
+
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");
|
|
1634
1750
|
if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
|
|
1635
1751
|
const n = find(cur.numbers, ref);
|
|
1636
1752
|
const on = sub === "enable";
|
|
@@ -1722,7 +1838,7 @@ ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone numbe
|
|
|
1722
1838
|
const m = await readMultiFileAuth(v["from-dir"]);
|
|
1723
1839
|
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)` : ""}`));
|
|
1724
1840
|
data = { authCreds: m.authCreds, authKeys: m.authKeys };
|
|
1725
|
-
} else data = JSON.parse(await
|
|
1841
|
+
} else data = JSON.parse(await readFile8(file, "utf8"));
|
|
1726
1842
|
const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
|
|
1727
1843
|
const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
|
|
1728
1844
|
if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
|
|
@@ -1749,6 +1865,30 @@ ${sub === "link" ? "From now on this number's messages reach this agent in gs."
|
|
|
1749
1865
|
const r = await api(path, { method: "POST", body });
|
|
1750
1866
|
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."}`);
|
|
1751
1867
|
}
|
|
1868
|
+
async function historyCmd(id, v) {
|
|
1869
|
+
if (!v.integration) throw usage("Missing --integration.", `Example: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
|
|
1870
|
+
const days = Number(v.days ?? 7);
|
|
1871
|
+
if (!Number.isInteger(days) || days < 1 || days > 30) throw usage("--days takes 1 to 30.");
|
|
1872
|
+
const path = `/api/v2/agents/${enc7(id)}/whatsapp/history`;
|
|
1873
|
+
const c = await api(path, { method: "POST", body: { integrationId: v.integration, days, dryRun: true } });
|
|
1874
|
+
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}`;
|
|
1875
|
+
if (v["dry-run"]) return out(c, () => `${plan}
|
|
1876
|
+
${dim("Dry run: nothing imported. Re-running later never duplicates.")}`);
|
|
1877
|
+
if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(`Nothing to import: ${plan}.`));
|
|
1878
|
+
await confirm(`${bold(plan)}
|
|
1879
|
+
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}` });
|
|
1880
|
+
const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0 };
|
|
1881
|
+
let cursor = 0;
|
|
1882
|
+
let done = 0;
|
|
1883
|
+
while (cursor !== null) {
|
|
1884
|
+
const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
|
|
1885
|
+
for (const k of Object.keys(sum)) sum[k] += p[k];
|
|
1886
|
+
done += p.page;
|
|
1887
|
+
if (!globals.json) info(dim(` ${done}/${p.total} conversations`));
|
|
1888
|
+
cursor = p.nextCursor;
|
|
1889
|
+
}
|
|
1890
|
+
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`) : ""}`);
|
|
1891
|
+
}
|
|
1752
1892
|
|
|
1753
1893
|
// src/commands/board.ts
|
|
1754
1894
|
async function boardCmd(sub, id, v) {
|
|
@@ -1779,11 +1919,16 @@ var HELPS = {
|
|
|
1779
1919
|
summary: "Create, inspect, change, check and delete agents.",
|
|
1780
1920
|
usage: [
|
|
1781
1921
|
"ghosty agents ls",
|
|
1782
|
-
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
|
|
1783
|
-
"ghosty agents get <agent-id> [--fields name,model,prompt]",
|
|
1784
|
-
"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]",
|
|
1922
|
+
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026] [--workspace SLUG] [--channels teams=off,\u2026]",
|
|
1923
|
+
"ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
|
|
1924
|
+
"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]",
|
|
1925
|
+
"ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
|
|
1785
1926
|
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1786
1927
|
"ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
|
|
1928
|
+
"ghosty agents docs add <agent-id> --url URL | --file PATH [--name N] [--description TEXT] [--grant read|write|no] [--no-deliver]",
|
|
1929
|
+
"ghosty agents share <agent-id> <email> [--role editor|admin]",
|
|
1930
|
+
"ghosty agents unshare <agent-id> <email> [--yes]",
|
|
1931
|
+
"ghosty agents shares <agent-id>",
|
|
1787
1932
|
"ghosty agents restart <agent-id>",
|
|
1788
1933
|
"ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
|
|
1789
1934
|
'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]',
|
|
@@ -1791,12 +1936,21 @@ var HELPS = {
|
|
|
1791
1936
|
],
|
|
1792
1937
|
flags: [
|
|
1793
1938
|
["--fields a,b", "get: return only these fields"],
|
|
1939
|
+
["--prompt-out FILE", "get: write the prompt to FILE (edit it and come back with set --prompt-file)"],
|
|
1940
|
+
["--workspace SLUG", "create: born in that workspace (you own or administer it), owned by its owner and active in its Teams"],
|
|
1941
|
+
["--channels k=on|off,\u2026", "create/set: turn channels on/off: teams, web, whatsapp, messenger, wa (personal WhatsApp)"],
|
|
1942
|
+
["--toolsets LIST", "set: gs tool groups it gets (crm, sales\u2026): a list replaces; +x / -x add or remove"],
|
|
1943
|
+
["--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)"],
|
|
1944
|
+
["--prompt-overlay FILE", 'set: JSON {"edits":[{"what","find","replace","regex"?,"all"?}]}, applied in order; each must find its place or nothing changes'],
|
|
1794
1945
|
["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
|
|
1795
1946
|
["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
|
|
1796
1947
|
["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
|
|
1797
1948
|
["--verbose", "doctor: also list the checks that passed"],
|
|
1798
1949
|
["--write", "docs grant: the agent may replace the document (default: read only)"],
|
|
1799
|
-
["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
|
|
1950
|
+
["--no-deliver", "docs grant/add: it can read it but never send it to whoever writes"],
|
|
1951
|
+
["--url URL | --file PATH", "docs add: where the document comes from (public URL, up to 25 MB, or a local file)"],
|
|
1952
|
+
["--grant read|write|no", "docs add: access for this agent (default read; no = only upload to the account)"],
|
|
1953
|
+
["--role R", "share: editor (configures and uses it) or admin (also shares it); again on a shared email changes the role"],
|
|
1800
1954
|
["--yes", "rm: skip the confirmation (required without a terminal)"],
|
|
1801
1955
|
["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
|
|
1802
1956
|
["--on | --off", "demo: turn the public link on or off"],
|
|
@@ -1818,16 +1972,23 @@ var HELPS = {
|
|
|
1818
1972
|
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1819
1973
|
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1820
1974
|
"ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
|
|
1975
|
+
'ghosty agents docs add <id> --url https://example.com/lista.pdf --description "Precios 2026"',
|
|
1976
|
+
"ghosty agents create --name Pia --engine claude --workspace ventas --channels teams=off",
|
|
1977
|
+
"ghosty agents set <id> --toolsets +crm",
|
|
1978
|
+
"ghosty agents share <id> ana@empresa.com --role admin",
|
|
1979
|
+
"ghosty agents get <id> --prompt-out PROMPT.md",
|
|
1980
|
+
"ghosty agents set <pia-id> --prompt-base <tania-id> --prompt-overlay capa.json --dry-run --out /tmp/pia.md",
|
|
1821
1981
|
'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
|
|
1822
1982
|
"ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
|
|
1823
1983
|
],
|
|
1824
|
-
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."],
|
|
1984
|
+
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."],
|
|
1825
1985
|
learn: `${REF}#agentes`
|
|
1826
1986
|
},
|
|
1827
1987
|
skills: {
|
|
1828
1988
|
summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
|
|
1829
1989
|
usage: [
|
|
1830
1990
|
"ghosty skills ls <agent-id>",
|
|
1991
|
+
"ghosty skills on|off <agent-id> <slug>",
|
|
1831
1992
|
"ghosty skills get <agent-id> <slug> [--out DIR]",
|
|
1832
1993
|
"ghosty skills templates <agent-id>",
|
|
1833
1994
|
"ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
|
|
@@ -1884,15 +2045,16 @@ var HELPS = {
|
|
|
1884
2045
|
},
|
|
1885
2046
|
turns: {
|
|
1886
2047
|
summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
|
|
1887
|
-
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last]"],
|
|
2048
|
+
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
|
|
1888
2049
|
flags: [
|
|
1889
2050
|
["--errors", "only failed turns (same as --status error)"],
|
|
1890
2051
|
["--status S", "running | done | stopped | error"],
|
|
1891
2052
|
["--since T", "30m, 1h, 2d or an ISO date"],
|
|
1892
|
-
["--limit N", "max rows (default 20, max 500)"]
|
|
2053
|
+
["--limit N", "max rows (default 20, max 500)"],
|
|
2054
|
+
["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"]
|
|
1893
2055
|
],
|
|
1894
2056
|
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"],
|
|
1895
|
-
notes: ["show reads the worker's transcript
|
|
2057
|
+
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)."],
|
|
1896
2058
|
learn: `${REF}#turnos`
|
|
1897
2059
|
},
|
|
1898
2060
|
files: {
|
|
@@ -1981,6 +2143,7 @@ HELPS.channels = {
|
|
|
1981
2143
|
"ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
|
|
1982
2144
|
"ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
|
|
1983
2145
|
"ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
|
|
2146
|
+
"ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--dry-run] [--yes]",
|
|
1984
2147
|
"ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
|
|
1985
2148
|
"ghosty channels wa pair <agent-id> --phone 5215512345678",
|
|
1986
2149
|
"ghosty channels wa status|pause <agent-id>",
|
|
@@ -1994,7 +2157,8 @@ HELPS.channels = {
|
|
|
1994
2157
|
["--role R", "groups: equipo (the team: writes to databases, admin tools) or cliente (like any customer)"],
|
|
1995
2158
|
["--on | --off", "groups set: the agent answers in that group, or stops"],
|
|
1996
2159
|
["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
|
|
1997
|
-
["--integration ID", "link/unlink: the WhatsApp Business number's Integration id in Formmy"],
|
|
2160
|
+
["--integration ID", "link/unlink/history: the WhatsApp Business number's Integration id in Formmy"],
|
|
2161
|
+
["--days N", "history: conversations active in the last N days (1-30, default 7)"],
|
|
1998
2162
|
["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
|
|
1999
2163
|
["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
|
|
2000
2164
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
|
|
@@ -2010,6 +2174,7 @@ HELPS.channels = {
|
|
|
2010
2174
|
"ghosty channels wa groups ls <id>",
|
|
2011
2175
|
'ghosty channels wa groups set <id> "Pia-cotizadora" --role equipo --on --dry-run',
|
|
2012
2176
|
"ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
|
|
2177
|
+
"ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
|
|
2013
2178
|
"ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
|
|
2014
2179
|
],
|
|
2015
2180
|
notes: [
|
|
@@ -2017,6 +2182,7 @@ HELPS.channels = {
|
|
|
2017
2182
|
"Disabling keeps logging messages; nobody answers them.",
|
|
2018
2183
|
"Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
|
|
2019
2184
|
"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.",
|
|
2185
|
+
"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.",
|
|
2020
2186
|
"import: turn off the EasyBits socket first WITHOUT logout (two sockets knock each other off; a logout invalidates the keys). Delete the file afterwards."
|
|
2021
2187
|
],
|
|
2022
2188
|
learn: `${REF}#canales-de-whatsapp`
|
|
@@ -2056,7 +2222,7 @@ function helpText(cmd) {
|
|
|
2056
2222
|
}
|
|
2057
2223
|
|
|
2058
2224
|
// src/update.ts
|
|
2059
|
-
import { mkdir as mkdir4, readFile as
|
|
2225
|
+
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
|
|
2060
2226
|
import { homedir as homedir3 } from "node:os";
|
|
2061
2227
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2062
2228
|
var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
|
|
@@ -2072,7 +2238,7 @@ async function latestVersion() {
|
|
|
2072
2238
|
if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
|
|
2073
2239
|
let cache = {};
|
|
2074
2240
|
try {
|
|
2075
|
-
cache = JSON.parse(await
|
|
2241
|
+
cache = JSON.parse(await readFile9(FILE, "utf8"));
|
|
2076
2242
|
} catch {
|
|
2077
2243
|
}
|
|
2078
2244
|
if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
|
|
@@ -2105,12 +2271,18 @@ ${bold("Session")}
|
|
|
2105
2271
|
${bold("Agents")}
|
|
2106
2272
|
agents ls List your agents
|
|
2107
2273
|
agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
2108
|
-
|
|
2274
|
+
[--workspace SLUG] [--channels teams=off,\u2026] born in a workspace, active in its Teams
|
|
2275
|
+
agents get <id> Show an agent's config [--fields name,model,prompt] [--prompt-out FILE]
|
|
2109
2276
|
agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
2110
2277
|
[--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
|
|
2278
|
+
[--channels wa=on,teams=off] turn channels on/off (wa = personal WhatsApp)
|
|
2111
2279
|
[--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
|
|
2280
|
+
[--toolsets crm,sales | +crm,-sales] gs tool groups it gets
|
|
2281
|
+
[--prompt-base AGENT|none --prompt-overlay FILE [--dry-run]] prompt = another agent's + edits, kept in sync
|
|
2112
2282
|
agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
|
|
2113
2283
|
agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
|
|
2284
|
+
agents docs add <id> Upload a document and grant it --url URL | --file PATH [--description] [--grant read|write|no]
|
|
2285
|
+
agents share|unshare <id> <email> Share it without handing it over [--role editor|admin] \xB7 agents shares <id> lists them
|
|
2114
2286
|
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
2115
2287
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
2116
2288
|
agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
|
|
@@ -2119,12 +2291,14 @@ ${bold("Agents")}
|
|
|
2119
2291
|
|
|
2120
2292
|
${bold("Configure an agent")}
|
|
2121
2293
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
2294
|
+
skills on|off <id> <slug> Turn one of its skills (or one of Ghosty's) on or off
|
|
2122
2295
|
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)
|
|
2123
2296
|
skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
|
|
2124
2297
|
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]
|
|
2125
2298
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
2126
2299
|
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
2127
2300
|
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]
|
|
2301
|
+
channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages and orders from Formmy (no duplicates) [--dry-run]
|
|
2128
2302
|
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]
|
|
2129
2303
|
channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
|
|
2130
2304
|
channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
|
|
@@ -2139,7 +2313,7 @@ ${bold("Talk to an agent")}
|
|
|
2139
2313
|
conversations ls <id> List conversations
|
|
2140
2314
|
conversations show <id> <conversation> What was said [--limit 20] (0 = all)
|
|
2141
2315
|
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
2142
|
-
turns show <id> [turn]
|
|
2316
|
+
turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
|
|
2143
2317
|
|
|
2144
2318
|
${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
|
|
2145
2319
|
schedule ls <id> Pending turns and recent runs
|
|
@@ -2166,7 +2340,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
|
|
|
2166
2340
|
${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.")}
|
|
2167
2341
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
2168
2342
|
var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
|
|
2169
|
-
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo", "box"];
|
|
2343
|
+
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
|
|
2170
2344
|
function unknown(what, input, known, helpCmd) {
|
|
2171
2345
|
const s = suggest(input, known);
|
|
2172
2346
|
return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
|
|
@@ -2244,6 +2418,15 @@ async function main() {
|
|
|
2244
2418
|
since: { type: "string" },
|
|
2245
2419
|
limit: { type: "string" },
|
|
2246
2420
|
verbose: { type: "boolean" },
|
|
2421
|
+
full: { type: "boolean" },
|
|
2422
|
+
"prompt-out": { type: "string" },
|
|
2423
|
+
toolsets: { type: "string" },
|
|
2424
|
+
"prompt-base": { type: "string" },
|
|
2425
|
+
"prompt-overlay": { type: "string" },
|
|
2426
|
+
url: { type: "string" },
|
|
2427
|
+
description: { type: "string" },
|
|
2428
|
+
grant: { type: "string" },
|
|
2429
|
+
days: { type: "string" },
|
|
2247
2430
|
voice: { type: "string" },
|
|
2248
2431
|
"voice-replies": { type: "string" },
|
|
2249
2432
|
"auto-stage": { type: "string" },
|
|
@@ -2262,6 +2445,7 @@ async function main() {
|
|
|
2262
2445
|
"own-number": { type: "string" },
|
|
2263
2446
|
answer: { type: "string" },
|
|
2264
2447
|
phone: { type: "string" },
|
|
2448
|
+
channels: { type: "string" },
|
|
2265
2449
|
forward: { type: "boolean" },
|
|
2266
2450
|
replace: { type: "boolean" },
|
|
2267
2451
|
"no-connect": { type: "boolean" },
|
|
@@ -2274,7 +2458,7 @@ async function main() {
|
|
|
2274
2458
|
const tokens = splitTokens(pos, v);
|
|
2275
2459
|
globals.token = tokens.session;
|
|
2276
2460
|
globals.profile = v.profile;
|
|
2277
|
-
if (v.version) return out({ version: "0.
|
|
2461
|
+
if (v.version) return out({ version: "0.14.0" }, () => "0.14.0");
|
|
2278
2462
|
const [cmd, sub, a1, a2] = pos;
|
|
2279
2463
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
2280
2464
|
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
@@ -2303,7 +2487,8 @@ async function main() {
|
|
|
2303
2487
|
case "agents":
|
|
2304
2488
|
if (sub === "ls" || sub === "list" || !sub) return agentsLs();
|
|
2305
2489
|
if (sub === "create" || sub === "new") return agentsCreate(v);
|
|
2306
|
-
if (sub === "get") return agentsGet(a1, v.fields);
|
|
2490
|
+
if (sub === "get") return agentsGet(a1, v.fields, v["prompt-out"]);
|
|
2491
|
+
if (sub === "share" || sub === "unshare" || sub === "shares") return agentsShare(sub, a1, a2, v);
|
|
2307
2492
|
if (sub === "set") return agentsSet(a1, v);
|
|
2308
2493
|
if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
|
|
2309
2494
|
if (sub === "doctor") return agentsDoctor(a1, v);
|
|
@@ -2359,19 +2544,19 @@ async function main() {
|
|
|
2359
2544
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
2360
2545
|
}
|
|
2361
2546
|
}
|
|
2362
|
-
var notice = updateNotice("0.
|
|
2547
|
+
var notice = updateNotice("0.14.0").catch(() => null);
|
|
2363
2548
|
main().then(
|
|
2364
2549
|
async () => {
|
|
2365
2550
|
const n = await notice;
|
|
2366
2551
|
if (n) info(dim(n));
|
|
2367
|
-
|
|
2552
|
+
return exitAfterFlush(EXIT.OK);
|
|
2368
2553
|
},
|
|
2369
2554
|
async (e) => {
|
|
2370
2555
|
if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
|
|
2371
2556
|
const n = await notice;
|
|
2372
|
-
fail(usage(e.message, n ? `${n}
|
|
2557
|
+
return fail(usage(e.message, n ? `${n}
|
|
2373
2558
|
This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
|
|
2374
2559
|
}
|
|
2375
|
-
fail(e);
|
|
2560
|
+
return fail(e);
|
|
2376
2561
|
}
|
|
2377
2562
|
);
|
package/package.json
CHANGED