@ghostystudio/cli 0.13.2 → 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 +18 -4
- package/dist/index.js +216 -50
- 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,15 @@ 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
|
+
|
|
82
96
|
### Changes in 0.13.2
|
|
83
97
|
|
|
84
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.
|
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,31 +547,64 @@ 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");
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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(/^\+/, ""));
|
|
557
605
|
}
|
|
558
|
-
|
|
606
|
+
next.delete("core");
|
|
607
|
+
body.tools = { toolsets: [...next] };
|
|
559
608
|
}
|
|
560
609
|
const env = parseEnv(v.env);
|
|
561
610
|
if (env) body.env = env;
|
|
@@ -579,7 +628,7 @@ async function agentsSet(id, v) {
|
|
|
579
628
|
if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
|
|
580
629
|
body.autoStage = v["auto-stage"] === "on";
|
|
581
630
|
}
|
|
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.");
|
|
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.");
|
|
583
632
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
584
633
|
out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
|
|
585
634
|
}
|
|
@@ -592,6 +641,25 @@ async function agentsRm(id, v = {}) {
|
|
|
592
641
|
}
|
|
593
642
|
out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
|
|
594
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
|
+
}
|
|
595
663
|
async function agentsRestart(id) {
|
|
596
664
|
id = need(id, "agent id", "ghosty agents restart <agent-id>");
|
|
597
665
|
out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} Restarting \u2014 the next turn uses the new config.`);
|
|
@@ -681,7 +749,7 @@ async function readSkillDir(dir) {
|
|
|
681
749
|
}
|
|
682
750
|
var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
683
751
|
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>";
|
|
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>";
|
|
685
753
|
id = need(id, "agent id", ex);
|
|
686
754
|
switch (sub ?? "ls") {
|
|
687
755
|
case "ls":
|
|
@@ -697,6 +765,13 @@ async function skillsCmd(sub, id, slug, v) {
|
|
|
697
765
|
${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
|
|
698
766
|
return;
|
|
699
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
|
+
}
|
|
700
775
|
case "get": {
|
|
701
776
|
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
702
777
|
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
|
|
@@ -1145,7 +1220,7 @@ async function call(path, v, init = {}) {
|
|
|
1145
1220
|
"x-ghosty-ts": ts,
|
|
1146
1221
|
"x-ghosty-sig": sig,
|
|
1147
1222
|
"content-type": "application/json",
|
|
1148
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1223
|
+
"user-agent": `ghosty-cli/${"0.14.0"}`
|
|
1149
1224
|
},
|
|
1150
1225
|
body: method === "GET" ? void 0 : raw
|
|
1151
1226
|
});
|
|
@@ -1310,6 +1385,10 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
1310
1385
|
}
|
|
1311
1386
|
}
|
|
1312
1387
|
|
|
1388
|
+
// src/commands/data.ts
|
|
1389
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1390
|
+
import { basename as basename3 } from "node:path";
|
|
1391
|
+
|
|
1313
1392
|
// src/rows.ts
|
|
1314
1393
|
var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1315
1394
|
function parseRowScope(raw) {
|
|
@@ -1439,17 +1518,30 @@ async function agentDocsCmd(sub, id, doc, v) {
|
|
|
1439
1518
|
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
|
|
1440
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"}.`);
|
|
1441
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
|
+
}
|
|
1442
1534
|
case "revoke": {
|
|
1443
1535
|
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
|
|
1444
1536
|
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
|
|
1445
1537
|
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
|
|
1446
1538
|
}
|
|
1447
1539
|
default:
|
|
1448
|
-
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");
|
|
1449
1541
|
}
|
|
1450
1542
|
}
|
|
1451
1543
|
async function turnsCmd(sub, id, turnId, v) {
|
|
1452
|
-
if (sub === "show") return turnShow(id, turnId);
|
|
1544
|
+
if (sub === "show") return turnShow(id, turnId, !!v.full);
|
|
1453
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]");
|
|
1454
1546
|
id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
|
|
1455
1547
|
const q = new URLSearchParams();
|
|
@@ -1472,12 +1564,19 @@ async function turnsCmd(sub, id, turnId, v) {
|
|
|
1472
1564
|
out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
|
|
1473
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}` : ""}.`));
|
|
1474
1566
|
}
|
|
1475
|
-
async function turnShow(id, turnId) {
|
|
1476
|
-
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]");
|
|
1477
1569
|
const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
|
|
1478
1570
|
out(r, () => {
|
|
1479
1571
|
const t = r.turn;
|
|
1480
|
-
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`)}`;
|
|
1481
1580
|
const line = (s) => {
|
|
1482
1581
|
const at = `+${s.at.toFixed(1)}s`.padStart(8);
|
|
1483
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") : ""}`;
|
|
@@ -1485,7 +1584,7 @@ async function turnShow(id, turnId) {
|
|
|
1485
1584
|
return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
|
|
1486
1585
|
};
|
|
1487
1586
|
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");
|
|
1587
|
+
return [head, ...io, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
|
|
1489
1588
|
});
|
|
1490
1589
|
}
|
|
1491
1590
|
async function agentsDoctor(id, v) {
|
|
@@ -1507,7 +1606,7 @@ async function agentsDoctor(id, v) {
|
|
|
1507
1606
|
}
|
|
1508
1607
|
if (problems) {
|
|
1509
1608
|
if (!globals.json) info(dim("Exit 1: problems found."));
|
|
1510
|
-
|
|
1609
|
+
return exitAfterFlush(EXIT.API);
|
|
1511
1610
|
}
|
|
1512
1611
|
}
|
|
1513
1612
|
|
|
@@ -1583,10 +1682,10 @@ async function credentialsCmd2(sub, provider, v) {
|
|
|
1583
1682
|
}
|
|
1584
1683
|
|
|
1585
1684
|
// src/commands/channels.ts
|
|
1586
|
-
import { readFile as
|
|
1685
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
1587
1686
|
|
|
1588
1687
|
// src/wa-auth.ts
|
|
1589
|
-
import { readdir as readdir2, readFile as
|
|
1688
|
+
import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
|
|
1590
1689
|
import { join as join4 } from "node:path";
|
|
1591
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"];
|
|
1592
1691
|
function keyOfFileName(name) {
|
|
@@ -1601,7 +1700,7 @@ function keyOfFileName(name) {
|
|
|
1601
1700
|
async function readMultiFileAuth(dir) {
|
|
1602
1701
|
const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
|
|
1603
1702
|
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
|
|
1703
|
+
const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
|
|
1605
1704
|
const authKeys = {};
|
|
1606
1705
|
const counts = {};
|
|
1607
1706
|
const skipped = [];
|
|
@@ -1612,7 +1711,7 @@ async function readMultiFileAuth(dir) {
|
|
|
1612
1711
|
skipped.push(f);
|
|
1613
1712
|
continue;
|
|
1614
1713
|
}
|
|
1615
|
-
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await
|
|
1714
|
+
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join4(dir, f), "utf8"));
|
|
1616
1715
|
counts[k.type] = (counts[k.type] ?? 0) + 1;
|
|
1617
1716
|
}
|
|
1618
1717
|
return { authCreds, authKeys, counts, skipped };
|
|
@@ -1631,6 +1730,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
|
|
|
1631
1730
|
if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
|
|
1632
1731
|
if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
|
|
1633
1732
|
if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
|
|
1733
|
+
if (sub === "history") return historyCmd(id, v);
|
|
1634
1734
|
if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
|
|
1635
1735
|
if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
|
|
1636
1736
|
const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
|
|
@@ -1646,7 +1746,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
|
|
|
1646
1746
|
}));
|
|
1647
1747
|
return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
|
|
1648
1748
|
}
|
|
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");
|
|
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");
|
|
1650
1750
|
if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
|
|
1651
1751
|
const n = find(cur.numbers, ref);
|
|
1652
1752
|
const on = sub === "enable";
|
|
@@ -1738,7 +1838,7 @@ ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone numbe
|
|
|
1738
1838
|
const m = await readMultiFileAuth(v["from-dir"]);
|
|
1739
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)` : ""}`));
|
|
1740
1840
|
data = { authCreds: m.authCreds, authKeys: m.authKeys };
|
|
1741
|
-
} else data = JSON.parse(await
|
|
1841
|
+
} else data = JSON.parse(await readFile8(file, "utf8"));
|
|
1742
1842
|
const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
|
|
1743
1843
|
const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
|
|
1744
1844
|
if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
|
|
@@ -1765,6 +1865,30 @@ ${sub === "link" ? "From now on this number's messages reach this agent in gs."
|
|
|
1765
1865
|
const r = await api(path, { method: "POST", body });
|
|
1766
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."}`);
|
|
1767
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
|
+
}
|
|
1768
1892
|
|
|
1769
1893
|
// src/commands/board.ts
|
|
1770
1894
|
async function boardCmd(sub, id, v) {
|
|
@@ -1795,11 +1919,16 @@ var HELPS = {
|
|
|
1795
1919
|
summary: "Create, inspect, change, check and delete agents.",
|
|
1796
1920
|
usage: [
|
|
1797
1921
|
"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]",
|
|
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",
|
|
1801
1926
|
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1802
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>",
|
|
1803
1932
|
"ghosty agents restart <agent-id>",
|
|
1804
1933
|
"ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
|
|
1805
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]',
|
|
@@ -1807,13 +1936,21 @@ var HELPS = {
|
|
|
1807
1936
|
],
|
|
1808
1937
|
flags: [
|
|
1809
1938
|
["--fields a,b", "get: return only these fields"],
|
|
1810
|
-
["--
|
|
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'],
|
|
1811
1945
|
["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
|
|
1812
1946
|
["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
|
|
1813
1947
|
["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
|
|
1814
1948
|
["--verbose", "doctor: also list the checks that passed"],
|
|
1815
1949
|
["--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"],
|
|
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"],
|
|
1817
1954
|
["--yes", "rm: skip the confirmation (required without a terminal)"],
|
|
1818
1955
|
["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
|
|
1819
1956
|
["--on | --off", "demo: turn the public link on or off"],
|
|
@@ -1835,16 +1972,23 @@ var HELPS = {
|
|
|
1835
1972
|
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1836
1973
|
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1837
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",
|
|
1838
1981
|
'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
|
|
1839
1982
|
"ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
|
|
1840
1983
|
],
|
|
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."],
|
|
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."],
|
|
1842
1985
|
learn: `${REF}#agentes`
|
|
1843
1986
|
},
|
|
1844
1987
|
skills: {
|
|
1845
1988
|
summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
|
|
1846
1989
|
usage: [
|
|
1847
1990
|
"ghosty skills ls <agent-id>",
|
|
1991
|
+
"ghosty skills on|off <agent-id> <slug>",
|
|
1848
1992
|
"ghosty skills get <agent-id> <slug> [--out DIR]",
|
|
1849
1993
|
"ghosty skills templates <agent-id>",
|
|
1850
1994
|
"ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
|
|
@@ -1901,15 +2045,16 @@ var HELPS = {
|
|
|
1901
2045
|
},
|
|
1902
2046
|
turns: {
|
|
1903
2047
|
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]"],
|
|
2048
|
+
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
|
|
1905
2049
|
flags: [
|
|
1906
2050
|
["--errors", "only failed turns (same as --status error)"],
|
|
1907
2051
|
["--status S", "running | done | stopped | error"],
|
|
1908
2052
|
["--since T", "30m, 1h, 2d or an ISO date"],
|
|
1909
|
-
["--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)"]
|
|
1910
2055
|
],
|
|
1911
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"],
|
|
1912
|
-
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)."],
|
|
1913
2058
|
learn: `${REF}#turnos`
|
|
1914
2059
|
},
|
|
1915
2060
|
files: {
|
|
@@ -1998,6 +2143,7 @@ HELPS.channels = {
|
|
|
1998
2143
|
"ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
|
|
1999
2144
|
"ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
|
|
2000
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]",
|
|
2001
2147
|
"ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
|
|
2002
2148
|
"ghosty channels wa pair <agent-id> --phone 5215512345678",
|
|
2003
2149
|
"ghosty channels wa status|pause <agent-id>",
|
|
@@ -2011,7 +2157,8 @@ HELPS.channels = {
|
|
|
2011
2157
|
["--role R", "groups: equipo (the team: writes to databases, admin tools) or cliente (like any customer)"],
|
|
2012
2158
|
["--on | --off", "groups set: the agent answers in that group, or stops"],
|
|
2013
2159
|
["--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"],
|
|
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)"],
|
|
2015
2162
|
["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
|
|
2016
2163
|
["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
|
|
2017
2164
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
|
|
@@ -2027,6 +2174,7 @@ HELPS.channels = {
|
|
|
2027
2174
|
"ghosty channels wa groups ls <id>",
|
|
2028
2175
|
'ghosty channels wa groups set <id> "Pia-cotizadora" --role equipo --on --dry-run',
|
|
2029
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",
|
|
2030
2178
|
"ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
|
|
2031
2179
|
],
|
|
2032
2180
|
notes: [
|
|
@@ -2034,6 +2182,7 @@ HELPS.channels = {
|
|
|
2034
2182
|
"Disabling keeps logging messages; nobody answers them.",
|
|
2035
2183
|
"Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
|
|
2036
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.",
|
|
2037
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."
|
|
2038
2187
|
],
|
|
2039
2188
|
learn: `${REF}#canales-de-whatsapp`
|
|
@@ -2073,7 +2222,7 @@ function helpText(cmd) {
|
|
|
2073
2222
|
}
|
|
2074
2223
|
|
|
2075
2224
|
// src/update.ts
|
|
2076
|
-
import { mkdir as mkdir4, readFile as
|
|
2225
|
+
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
|
|
2077
2226
|
import { homedir as homedir3 } from "node:os";
|
|
2078
2227
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2079
2228
|
var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
|
|
@@ -2089,7 +2238,7 @@ async function latestVersion() {
|
|
|
2089
2238
|
if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
|
|
2090
2239
|
let cache = {};
|
|
2091
2240
|
try {
|
|
2092
|
-
cache = JSON.parse(await
|
|
2241
|
+
cache = JSON.parse(await readFile9(FILE, "utf8"));
|
|
2093
2242
|
} catch {
|
|
2094
2243
|
}
|
|
2095
2244
|
if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
|
|
@@ -2122,13 +2271,18 @@ ${bold("Session")}
|
|
|
2122
2271
|
${bold("Agents")}
|
|
2123
2272
|
agents ls List your agents
|
|
2124
2273
|
agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
2125
|
-
|
|
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]
|
|
2126
2276
|
agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
2127
2277
|
[--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
|
|
2128
2278
|
[--channels wa=on,teams=off] turn channels on/off (wa = personal WhatsApp)
|
|
2129
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
|
|
2130
2282
|
agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
|
|
2131
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
|
|
2132
2286
|
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
2133
2287
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
2134
2288
|
agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
|
|
@@ -2137,12 +2291,14 @@ ${bold("Agents")}
|
|
|
2137
2291
|
|
|
2138
2292
|
${bold("Configure an agent")}
|
|
2139
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
|
|
2140
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)
|
|
2141
2296
|
skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
|
|
2142
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]
|
|
2143
2298
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
2144
2299
|
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
2145
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]
|
|
2146
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]
|
|
2147
2303
|
channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
|
|
2148
2304
|
channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
|
|
@@ -2157,7 +2313,7 @@ ${bold("Talk to an agent")}
|
|
|
2157
2313
|
conversations ls <id> List conversations
|
|
2158
2314
|
conversations show <id> <conversation> What was said [--limit 20] (0 = all)
|
|
2159
2315
|
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
2160
|
-
turns show <id> [turn]
|
|
2316
|
+
turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
|
|
2161
2317
|
|
|
2162
2318
|
${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
|
|
2163
2319
|
schedule ls <id> Pending turns and recent runs
|
|
@@ -2184,7 +2340,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
|
|
|
2184
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.")}
|
|
2185
2341
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
2186
2342
|
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"];
|
|
2343
|
+
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
|
|
2188
2344
|
function unknown(what, input, known, helpCmd) {
|
|
2189
2345
|
const s = suggest(input, known);
|
|
2190
2346
|
return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
|
|
@@ -2262,6 +2418,15 @@ async function main() {
|
|
|
2262
2418
|
since: { type: "string" },
|
|
2263
2419
|
limit: { type: "string" },
|
|
2264
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" },
|
|
2265
2430
|
voice: { type: "string" },
|
|
2266
2431
|
"voice-replies": { type: "string" },
|
|
2267
2432
|
"auto-stage": { type: "string" },
|
|
@@ -2293,7 +2458,7 @@ async function main() {
|
|
|
2293
2458
|
const tokens = splitTokens(pos, v);
|
|
2294
2459
|
globals.token = tokens.session;
|
|
2295
2460
|
globals.profile = v.profile;
|
|
2296
|
-
if (v.version) return out({ version: "0.
|
|
2461
|
+
if (v.version) return out({ version: "0.14.0" }, () => "0.14.0");
|
|
2297
2462
|
const [cmd, sub, a1, a2] = pos;
|
|
2298
2463
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
2299
2464
|
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
@@ -2322,7 +2487,8 @@ async function main() {
|
|
|
2322
2487
|
case "agents":
|
|
2323
2488
|
if (sub === "ls" || sub === "list" || !sub) return agentsLs();
|
|
2324
2489
|
if (sub === "create" || sub === "new") return agentsCreate(v);
|
|
2325
|
-
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);
|
|
2326
2492
|
if (sub === "set") return agentsSet(a1, v);
|
|
2327
2493
|
if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
|
|
2328
2494
|
if (sub === "doctor") return agentsDoctor(a1, v);
|
|
@@ -2378,19 +2544,19 @@ async function main() {
|
|
|
2378
2544
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
2379
2545
|
}
|
|
2380
2546
|
}
|
|
2381
|
-
var notice = updateNotice("0.
|
|
2547
|
+
var notice = updateNotice("0.14.0").catch(() => null);
|
|
2382
2548
|
main().then(
|
|
2383
2549
|
async () => {
|
|
2384
2550
|
const n = await notice;
|
|
2385
2551
|
if (n) info(dim(n));
|
|
2386
|
-
|
|
2552
|
+
return exitAfterFlush(EXIT.OK);
|
|
2387
2553
|
},
|
|
2388
2554
|
async (e) => {
|
|
2389
2555
|
if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
|
|
2390
2556
|
const n = await notice;
|
|
2391
|
-
fail(usage(e.message, n ? `${n}
|
|
2557
|
+
return fail(usage(e.message, n ? `${n}
|
|
2392
2558
|
This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
|
|
2393
2559
|
}
|
|
2394
|
-
fail(e);
|
|
2560
|
+
return fail(e);
|
|
2395
2561
|
}
|
|
2396
2562
|
);
|
package/package.json
CHANGED