@ghostystudio/cli 0.8.0 → 0.9.1
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 +28 -6
- package/dist/index.js +273 -89
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,29 +8,36 @@ npx -y @ghostystudio/cli login
|
|
|
8
8
|
npm i -g @ghostystudio/cli && ghosty login
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`ghosty login` opens your browser (OAuth2 + PKCE). If you already have a session on ghosty.studio, opening the link is all it takes. A coding agent can drive it for you: `ghosty login --json` prints `{"event":"login_url","url":…}` first so the agent can hand you the link.
|
|
11
|
+
`ghosty login` opens your browser (OAuth2 + PKCE). If you already have a session on ghosty.studio, opening the link is all it takes. A coding agent can drive it for you: `ghosty login --json` prints `{"event":"login_url","url":…}` first so the agent can hand you the link. Over SSH, CI or from another device, `ghosty login --device` gives you a link + code instead (automatic when there is no local browser).
|
|
12
12
|
|
|
13
13
|
## Commands
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
ghosty whoami
|
|
17
|
+
ghosty usage # plan and usage this week / month (also: ghosty me usage)
|
|
17
18
|
ghosty agents ls
|
|
19
|
+
ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…]
|
|
18
20
|
ghosty agents get <id> [--fields name,model,prompt]
|
|
19
|
-
ghosty agents set <id> [--name] [--model] [--prompt | --prompt-file FILE]
|
|
21
|
+
ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…]
|
|
22
|
+
ghosty agents rm <id> [--yes]
|
|
20
23
|
ghosty agents restart <id>
|
|
24
|
+
ghosty agents demo <id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N]
|
|
25
|
+
[--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate] # public /p/ link, no account needed
|
|
21
26
|
ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
|
|
22
27
|
ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
|
|
23
28
|
ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
|
|
24
29
|
|
|
25
30
|
ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
|
|
26
31
|
ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR]
|
|
27
|
-
ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
|
|
28
|
-
ghosty turns ls <id> [--errors] [--since 1h] [--limit N]
|
|
32
|
+
ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--from easybits:ID --tables a,b --append --dry-run]
|
|
33
|
+
ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
|
|
29
34
|
ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
|
|
30
35
|
ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
|
|
31
36
|
ghosty mcp get|set <id> [--file servers.json]
|
|
32
37
|
|
|
33
|
-
ghosty try <id> "message" [--session] [--reset]
|
|
38
|
+
ghosty try <id> "message" [--session] [--reset]
|
|
39
|
+
ghosty try <id> "message" --waba --session PHONE [--media FILE] [--history FILE] # real WhatsApp path, nothing sent
|
|
40
|
+
ghosty try <id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]
|
|
34
41
|
ghosty chat <id> ["message"] [--conversation ID]
|
|
35
42
|
ghosty conversations ls <id>
|
|
36
43
|
|
|
@@ -41,14 +48,29 @@ ghosty schedule rm <id> <schedule-id>
|
|
|
41
48
|
ghosty me files ls|upload|rm [--kind image|video|audio|document|other]
|
|
42
49
|
ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argument
|
|
43
50
|
ghosty docs
|
|
51
|
+
|
|
52
|
+
ghosty partner ls|enable|credentials|agent|tenants|audit|try # one agent for each of YOUR customers: ghosty partner --help
|
|
44
53
|
```
|
|
45
54
|
|
|
46
|
-
Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--help`, `--version`.
|
|
55
|
+
Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--profile <name>` / `GHOSTY_PROFILE` (a separate saved session, e.g. one per account), `--yes`, `--help`, `--version`. Env: `GHOSTY_URL`, `XDG_CONFIG_HOME`, `NO_COLOR`; partner commands sign with `GHOSTY_PARTNER_KEY` + `GHOSTY_PARTNER_SECRET` when set.
|
|
56
|
+
|
|
57
|
+
Aliases: `creds` = `credentials`, `db` = `dbs`, `dbs sql` = `dbs query`.
|
|
47
58
|
|
|
48
59
|
Every command has its own help with examples: `ghosty <command> --help` or `ghosty help <command>`.
|
|
49
60
|
|
|
50
61
|
Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
51
62
|
|
|
63
|
+
### Changes in 0.9.1
|
|
64
|
+
|
|
65
|
+
- `ghosty agents --help` describes every `agents demo` flag; `ghosty usage --help` has its own page.
|
|
66
|
+
- "Learn more" links point to the exact section of https://www.ghosty.studio/docs/cli/comandos; `partner --help` links to /docs/cli/partners.
|
|
67
|
+
|
|
68
|
+
### Changes in 0.9.0
|
|
69
|
+
|
|
70
|
+
- `try --waba`: a test turn through the **real** WhatsApp Business path (inherited context, price rule, board, voice decision). It prints what it would have sent (text, voice note, files) and sends nothing. `--media` for photos/audio/PDF and `--history` to seed what was said before. `--whatsapp` is now an alias.
|
|
71
|
+
- `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.
|
|
72
|
+
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
73
|
+
|
|
52
74
|
### Changes in 0.8.0
|
|
53
75
|
|
|
54
76
|
- Destructive commands (`agents rm`, `files rm`, `skills rm`, `credentials rm`, `board archive`, `dbs import` over existing rows, `channels whatsapp enable`) ask first. **Without a terminal they need `--yes`** or exit `2`.
|
package/dist/index.js
CHANGED
|
@@ -273,6 +273,40 @@ async function currentToken() {
|
|
|
273
273
|
if (cfg.expiresAt !== void 0 && cfg.expiresAt - Date.now() < 6e4) cfg = await refresh(cfg) ?? cfg;
|
|
274
274
|
return cfg.accessToken;
|
|
275
275
|
}
|
|
276
|
+
async function postLong(path, body, timeoutMs = 9e5) {
|
|
277
|
+
const base = await baseUrl();
|
|
278
|
+
const token = await currentToken();
|
|
279
|
+
const httpsRequest = base.startsWith("https:") ? (await import("node:https")).request : (await import("node:http")).request;
|
|
280
|
+
const data = JSON.stringify(body);
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const req = httpsRequest(`${base}${path}`, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
timeout: timeoutMs,
|
|
285
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.9.1"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
286
|
+
}, (res) => {
|
|
287
|
+
let buf = "";
|
|
288
|
+
res.on("data", (c) => buf += c);
|
|
289
|
+
res.on("end", () => {
|
|
290
|
+
let parsed;
|
|
291
|
+
try {
|
|
292
|
+
parsed = JSON.parse(buf);
|
|
293
|
+
} catch {
|
|
294
|
+
parsed = { error: buf.slice(0, 300) };
|
|
295
|
+
}
|
|
296
|
+
resolve({ status: res.statusCode ?? 0, retryAfter: retryAfterSeconds(res.headers["retry-after"]), body: parsed });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
req.on("timeout", () => req.destroy(new Error(`no answer in ${Math.round(timeoutMs / 6e4)} min`)));
|
|
300
|
+
req.on("error", (e) => resolve({ status: 0, retryAfter: null, body: { error: String(e) } }));
|
|
301
|
+
req.end(data);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function retryAfterSeconds(h, now = Date.now()) {
|
|
305
|
+
if (!h) return null;
|
|
306
|
+
if (/^\d+$/.test(h.trim())) return Number(h.trim());
|
|
307
|
+
const t = Date.parse(h);
|
|
308
|
+
return Number.isNaN(t) ? null : Math.max(0, Math.round((t - now) / 1e3));
|
|
309
|
+
}
|
|
276
310
|
async function api(path, opts = {}) {
|
|
277
311
|
const res = await request(path, opts);
|
|
278
312
|
const text = await res.text();
|
|
@@ -290,7 +324,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
290
324
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
291
325
|
headers: {
|
|
292
326
|
authorization: `Bearer ${token}`,
|
|
293
|
-
"user-agent": `ghosty-cli/${"0.
|
|
327
|
+
"user-agent": `ghosty-cli/${"0.9.1"}`,
|
|
294
328
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
295
329
|
...opts.headers
|
|
296
330
|
},
|
|
@@ -322,9 +356,9 @@ async function toError(res) {
|
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
// src/commands/agents.ts
|
|
325
|
-
import { mkdir as
|
|
359
|
+
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
326
360
|
import { homedir as homedir2 } from "node:os";
|
|
327
|
-
import { dirname as dirname2, join as
|
|
361
|
+
import { dirname as dirname2, join as join3, relative } from "node:path";
|
|
328
362
|
|
|
329
363
|
// src/prompt.ts
|
|
330
364
|
import { createInterface } from "node:readline/promises";
|
|
@@ -359,9 +393,109 @@ function suggest(input, known) {
|
|
|
359
393
|
return known.find((k) => k.startsWith(input) && input.length >= 2);
|
|
360
394
|
}
|
|
361
395
|
|
|
362
|
-
// src/commands/
|
|
396
|
+
// src/commands/replay.ts
|
|
397
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
398
|
+
import { basename, join as join2 } from "node:path";
|
|
363
399
|
var enc = encodeURIComponent;
|
|
364
|
-
var
|
|
400
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
401
|
+
var MIME = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", pdf: "application/pdf", ogg: "audio/ogg", opus: "audio/ogg", mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", mp4: "video/mp4" };
|
|
402
|
+
async function tryPost(id, body) {
|
|
403
|
+
for (let attempt = 0; ; attempt++) {
|
|
404
|
+
const r = await postLong(`/api/v2/agents/${enc(id)}/try`, body);
|
|
405
|
+
if ((r.status === 503 || r.status === 409 || r.status === 0) && attempt < 40) {
|
|
406
|
+
const wait = r.status === 503 ? r.retryAfter ?? 15 : Math.min(30, 2 ** Math.min(attempt, 4) + 1);
|
|
407
|
+
if (!globals.json) info(dim(` ${r.status === 503 ? "Ghosty is restarting" : r.status === 409 ? "previous turn still running" : "network error"} \u2014 retrying in ${wait}s`));
|
|
408
|
+
await sleep(wait * 1e3);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
return { status: r.status, body: r.body };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function uploadMedia(file) {
|
|
415
|
+
const bytes = await readFile2(file);
|
|
416
|
+
const ext = file.split(".").pop()?.toLowerCase() ?? "";
|
|
417
|
+
const r = await api(`/api/v2/me/files?nombre=${enc(basename(file))}`, {
|
|
418
|
+
method: "POST",
|
|
419
|
+
raw: new Uint8Array(bytes),
|
|
420
|
+
headers: { "content-type": "application/octet-stream" }
|
|
421
|
+
});
|
|
422
|
+
const mime = MIME[ext] ?? "application/octet-stream";
|
|
423
|
+
return { url: r.url, mime, filename: r.name, type: mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document" };
|
|
424
|
+
}
|
|
425
|
+
function sentText(sent) {
|
|
426
|
+
return sent.map((s) => s.kind === "text" ? `${lavender("\u2192")} ${s.text}` : s.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s.text}` : `${lavender("\u{1F4CE}")} ${s.name} ${dim(s.url)}`).join("\n\n");
|
|
427
|
+
}
|
|
428
|
+
async function tryWaba(id, text, v) {
|
|
429
|
+
const phone = v.session ?? "";
|
|
430
|
+
if (!/^\d{10,15}$/.test(phone)) throw usage("--waba needs --session <phone> (10-15 digits, a made-up number).", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
431
|
+
if ((v.media?.length ?? 0) > 1) throw usage("One --media per turn (WhatsApp sends one file per message).", "Send several turns, one file each.");
|
|
432
|
+
if (!text && !v.media?.length && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
433
|
+
let history;
|
|
434
|
+
if (v.history) {
|
|
435
|
+
const raw = JSON.parse(await readFile2(v.history, "utf8"));
|
|
436
|
+
history = Array.isArray(raw) ? raw : raw.messages;
|
|
437
|
+
if (!Array.isArray(history)) throw usage(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`);
|
|
438
|
+
if (!v.reset) info(dim("Note: history only counts on the first turn of a session; add --reset to start over."));
|
|
439
|
+
}
|
|
440
|
+
const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
|
|
441
|
+
if (process.stderr.isTTY && !globals.json && (text || media)) info(dim("Thinking\u2026 (the real WhatsApp path; nothing is sent)"));
|
|
442
|
+
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {} });
|
|
443
|
+
if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ?? "turn failed"}`), { code: 1 });
|
|
444
|
+
out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim("(the agent sent nothing)") : dim("Session reset.")) + (r.body.error ? `
|
|
445
|
+
${dim(`(warning: ${r.body.error})`)}` : ""));
|
|
446
|
+
}
|
|
447
|
+
async function tryReplay(id, v) {
|
|
448
|
+
if (!v.out) throw usage("--replay needs --out DIR (the report is written there after every step).", "Example: ghosty try <agent-id> --replay muestra.json --out ./replay");
|
|
449
|
+
const max = Number(v.max ?? 15);
|
|
450
|
+
const conc = Math.max(1, Math.min(30, Number(v.concurrency ?? 8)));
|
|
451
|
+
const input = JSON.parse(await readFile2(v.replay, "utf8"));
|
|
452
|
+
const convs = input.sample ?? [];
|
|
453
|
+
if (!convs.length) throw usage(`${v.replay}: no conversations in "sample".`, 'Format: {"sample": [{"id", "fakePhone", "messages": [{"role": "user"|"agent", "text"}]}]}');
|
|
454
|
+
await mkdir2(v.out, { recursive: true });
|
|
455
|
+
const file = join2(v.out, "replay.json");
|
|
456
|
+
const prev = v.resume ? JSON.parse(await readFile2(file, "utf8").catch(() => "null")) : null;
|
|
457
|
+
const report = prev ?? { agentId: id, startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, done: false, results: {} };
|
|
458
|
+
report.done = false;
|
|
459
|
+
let saving = Promise.resolve();
|
|
460
|
+
const save = () => saving = saving.then(() => writeFile2(file, JSON.stringify(report, null, 1)));
|
|
461
|
+
info(`${bold("Replay")} ${convs.length} conversation(s), up to ${max} customer turns each, ${conc} at a time \u2192 ${file}`);
|
|
462
|
+
const queue = [...convs];
|
|
463
|
+
const worker = async () => {
|
|
464
|
+
for (let c = queue.shift(); c; c = queue.shift()) {
|
|
465
|
+
const pairs = [];
|
|
466
|
+
for (const m of c.messages) {
|
|
467
|
+
if (m.role === "user") pairs.push({ user: m.text, original: "" });
|
|
468
|
+
else if (pairs.length) pairs[pairs.length - 1].original += (pairs[pairs.length - 1].original ? "\n\n" : "") + m.text;
|
|
469
|
+
}
|
|
470
|
+
const entry = report.results[c.id] ??= { tags: c.tags ?? [], steps: [] };
|
|
471
|
+
const todo = pairs.slice(0, max).slice(entry.steps.length);
|
|
472
|
+
if (!todo.length) continue;
|
|
473
|
+
if (!entry.steps.length) await tryPost(id, { as: "waba", session: c.fakePhone, reset: true });
|
|
474
|
+
for (const p of todo) {
|
|
475
|
+
const t0 = Date.now();
|
|
476
|
+
const r = await tryPost(id, { as: "waba", session: c.fakePhone, text: p.user });
|
|
477
|
+
entry.steps.push({ user: p.user, original: p.original, reply: r.body.text ?? "", sent: r.body.sent ?? [], error: r.body.error ?? null, ms: Date.now() - t0, status: r.status });
|
|
478
|
+
await save();
|
|
479
|
+
}
|
|
480
|
+
const bad = entry.steps.filter((s) => s.status !== 200 || !s.reply).length;
|
|
481
|
+
info(` ${bad ? red("!") : lavender("\u2713")} ${c.id}: ${entry.steps.length} turn(s)${bad ? `, ${bad} without an answer` : ""}`);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
await Promise.all(Array.from({ length: Math.min(conc, convs.length) }, worker));
|
|
485
|
+
report.done = true;
|
|
486
|
+
report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
487
|
+
await save();
|
|
488
|
+
const steps = Object.values(report.results).flatMap((r) => r.steps);
|
|
489
|
+
const failed = steps.filter((s) => s.status !== 200 || !s.reply).length;
|
|
490
|
+
const summary = { file, conversations: Object.keys(report.results).length, turns: steps.length, failed, medianMs: steps.map((s) => s.ms).sort((a, b) => a - b)[Math.floor(steps.length / 2)] ?? 0 };
|
|
491
|
+
out(summary, () => `${failed ? red("!") : lavender("\u2713")} ${summary.turns} turn(s) in ${summary.conversations} conversation(s), ${failed} without an answer, median ${Math.round(summary.medianMs / 1e3)}s
|
|
492
|
+
${dim(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since 2h`)}`);
|
|
493
|
+
if (failed) process.exitCode = 1;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/commands/agents.ts
|
|
497
|
+
var enc2 = encodeURIComponent;
|
|
498
|
+
var agentPath = (id, rest = "") => `/api/v2/agents/${enc2(id)}${rest}`;
|
|
365
499
|
var need = (v, what, example) => {
|
|
366
500
|
if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
|
|
367
501
|
return v;
|
|
@@ -394,7 +528,7 @@ async function agentsCreate(v) {
|
|
|
394
528
|
if (v.engine) body.engine = v.engine;
|
|
395
529
|
if (v.model) body.model = v.model;
|
|
396
530
|
if (v.prompt) body.prompt = v.prompt;
|
|
397
|
-
if (v["prompt-file"]) body.prompt = await
|
|
531
|
+
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
398
532
|
const env = parseEnv(v.env);
|
|
399
533
|
if (env) body.env = env;
|
|
400
534
|
const r = await api("/api/v2/me/agents", { method: "POST", body });
|
|
@@ -403,7 +537,7 @@ async function agentsCreate(v) {
|
|
|
403
537
|
}
|
|
404
538
|
async function agentsGet(id, fields) {
|
|
405
539
|
id = need(id, "agent id", "ghosty agents get <agent-id>");
|
|
406
|
-
const q = fields ? `?fields=${
|
|
540
|
+
const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
|
|
407
541
|
out(await api(agentPath(id, q)));
|
|
408
542
|
}
|
|
409
543
|
async function agentsSet(id, v) {
|
|
@@ -413,7 +547,7 @@ async function agentsSet(id, v) {
|
|
|
413
547
|
if (v.engine) body.engine = v.engine;
|
|
414
548
|
if (v.model) body.model = v.model;
|
|
415
549
|
if (v.prompt) body.prompt = v.prompt;
|
|
416
|
-
if (v["prompt-file"]) body.prompt = await
|
|
550
|
+
if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
|
|
417
551
|
const env = parseEnv(v.env);
|
|
418
552
|
if (env) body.env = env;
|
|
419
553
|
const connectors = {};
|
|
@@ -455,11 +589,13 @@ async function agentsRestart(id) {
|
|
|
455
589
|
}
|
|
456
590
|
async function tryTurn(id, text, v) {
|
|
457
591
|
id = need(id, "agent id", 'ghosty try <agent-id> "hello"');
|
|
592
|
+
if (v.replay) return tryReplay(id, v);
|
|
593
|
+
if (v.whatsapp && !v.waba) info(dim("Note: --whatsapp is now --waba (the real WhatsApp path)."));
|
|
594
|
+
if (v.waba || v.whatsapp || v.media?.length || v.history) return tryWaba(id, text, v);
|
|
458
595
|
if (!text && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hello"');
|
|
459
|
-
if (
|
|
460
|
-
} else info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
|
|
596
|
+
if (process.stderr.isTTY && text && !globals.json) info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
|
|
461
597
|
const r = await api(agentPath(id, "/try"), {
|
|
462
|
-
body: { text, session: v.session, reset: v.reset
|
|
598
|
+
body: { text, session: v.session, reset: v.reset }
|
|
463
599
|
});
|
|
464
600
|
out(r, () => (r.text ?? "") + (r.error ? `
|
|
465
601
|
${dim(`(warning: ${r.error})`)}` : ""));
|
|
@@ -481,14 +617,14 @@ ${table(r.files ?? [], cols)}`);
|
|
|
481
617
|
const res = await request(agentPath(id, `/files/${p}`));
|
|
482
618
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
483
619
|
if (v.out) {
|
|
484
|
-
await
|
|
620
|
+
await writeFile3(v.out, bytes);
|
|
485
621
|
out({ path: p, saved: v.out, bytes: bytes.length }, () => `${lavender("\u2713")} Saved ${bytes.length} bytes to ${v.out}`);
|
|
486
622
|
} else process.stdout.write(bytes);
|
|
487
623
|
return;
|
|
488
624
|
}
|
|
489
625
|
case "put": {
|
|
490
626
|
need(path, "path", ex);
|
|
491
|
-
const data = v.file ? await
|
|
627
|
+
const data = v.file ? await readFile3(v.file) : await readStdin();
|
|
492
628
|
const r = await api(agentPath(id, `/files/${p}`), { method: "PUT", raw: new Uint8Array(data), headers: { "content-type": "application/octet-stream" } });
|
|
493
629
|
out(r ?? { path: p, bytes: data.length }, () => `${lavender("\u2713")} Wrote ${p} (${data.length} bytes).`);
|
|
494
630
|
return;
|
|
@@ -512,11 +648,11 @@ async function readStdin() {
|
|
|
512
648
|
async function writeSkill(dir, sk) {
|
|
513
649
|
let bytes = 0;
|
|
514
650
|
for (const f of sk.files) {
|
|
515
|
-
const full =
|
|
651
|
+
const full = join3(dir, f.path);
|
|
516
652
|
if (relative(dir, full).startsWith("..")) continue;
|
|
517
|
-
await
|
|
653
|
+
await mkdir3(dirname2(full), { recursive: true });
|
|
518
654
|
const b = Buffer.from(f.contentBase64, "base64");
|
|
519
|
-
await
|
|
655
|
+
await writeFile3(full, b);
|
|
520
656
|
bytes += b.length;
|
|
521
657
|
}
|
|
522
658
|
return bytes;
|
|
@@ -526,15 +662,15 @@ async function readSkillDir(dir) {
|
|
|
526
662
|
let markdown;
|
|
527
663
|
const assets = [];
|
|
528
664
|
for (const e of files) {
|
|
529
|
-
const full =
|
|
665
|
+
const full = join3(e.parentPath, e.name);
|
|
530
666
|
const rel = relative(dir, full).split("\\").join("/");
|
|
531
|
-
if (rel === "SKILL.md") markdown = await
|
|
532
|
-
else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await
|
|
667
|
+
if (rel === "SKILL.md") markdown = await readFile3(full, "utf8");
|
|
668
|
+
else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await readFile3(full)).toString("base64") });
|
|
533
669
|
}
|
|
534
670
|
if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
|
|
535
671
|
return { markdown, assets };
|
|
536
672
|
}
|
|
537
|
-
var trashDir = (id, slug) =>
|
|
673
|
+
var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
538
674
|
async function skillsCmd(sub, id, slug, v) {
|
|
539
675
|
const ex = "ghosty skills ls <agent-id> | get <agent-id> <slug> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
|
|
540
676
|
id = need(id, "agent id", ex);
|
|
@@ -554,7 +690,7 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
|
|
|
554
690
|
}
|
|
555
691
|
case "get": {
|
|
556
692
|
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
557
|
-
const sk = await api(agentPath(id, `/skills/${
|
|
693
|
+
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
|
|
558
694
|
const dir = v.out ?? slug;
|
|
559
695
|
const bytes = await writeSkill(dir, sk);
|
|
560
696
|
out({ slug, saved: dir, files: sk.files.map((f) => f.path), bytes }, () => `${lavender("\u2713")} ${slug} \u2192 ${dir}/ ${dim(`(${sk.files.length} file(s), ${bytes} bytes)`)}`);
|
|
@@ -563,8 +699,8 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
|
|
|
563
699
|
case "add": {
|
|
564
700
|
slug = need(slug, "skill slug", ex);
|
|
565
701
|
if (v.file && v.dir) throw usage("Use --file or --dir, not both.");
|
|
566
|
-
const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await
|
|
567
|
-
const r = await api(agentPath(id, `/skills/${
|
|
702
|
+
const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await readFile3(v.file, "utf8") } : { fromCatalog: true };
|
|
703
|
+
const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PUT", body });
|
|
568
704
|
out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
|
|
569
705
|
return;
|
|
570
706
|
}
|
|
@@ -572,11 +708,11 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
|
|
|
572
708
|
case "remove": {
|
|
573
709
|
slug = need(slug, "skill slug", ex);
|
|
574
710
|
requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
|
|
575
|
-
const sk = await api(agentPath(id, `/skills/${
|
|
711
|
+
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`)).catch(() => null);
|
|
576
712
|
await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
|
|
577
713
|
const backup = sk ? trashDir(id, slug) : null;
|
|
578
714
|
if (sk && backup) await writeSkill(backup, sk);
|
|
579
|
-
const r = await api(agentPath(id, `/skills/${
|
|
715
|
+
const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
|
|
580
716
|
const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
|
|
581
717
|
out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
|
|
582
718
|
${dim(`Backup: ${backup}
|
|
@@ -593,7 +729,7 @@ async function mcpCmd(sub, id, v) {
|
|
|
593
729
|
if (sub === "get") return out(await api(agentPath(id, "/mcp")));
|
|
594
730
|
if (sub === "set") {
|
|
595
731
|
const file = need(v.file, "--file", ex);
|
|
596
|
-
const parsed = JSON.parse(await
|
|
732
|
+
const parsed = JSON.parse(await readFile3(file, "utf8"));
|
|
597
733
|
const body = { servers: Array.isArray(parsed) ? parsed : parsed.servers ?? [] };
|
|
598
734
|
const r = await api(agentPath(id, "/mcp"), { method: "PUT", body });
|
|
599
735
|
return out(r, () => `${lavender("\u2713")} MCP servers replaced \u2014 ${r?.reiniciado ? "the agent restarted with them." : "they load on the next turn."}`);
|
|
@@ -608,11 +744,12 @@ async function agentsDemo(id, v) {
|
|
|
608
744
|
if (v.on) body.enabled = true;
|
|
609
745
|
if (v.off) body.enabled = false;
|
|
610
746
|
if (v.turns) body.maxTurns = Number(v.turns);
|
|
611
|
-
if (v.
|
|
747
|
+
if (v.vence !== void 0) body.venceEl = v.vence === "no" ? null : new Date(v.vence).toISOString();
|
|
612
748
|
if (v["per-ip"]) body.perIpDay = Number(v["per-ip"]);
|
|
613
749
|
if (v.welcome !== void 0) body.welcome = v.welcome;
|
|
614
750
|
if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
|
|
615
751
|
if (v.starter !== void 0) body.starter = v.starter || null;
|
|
752
|
+
if (v.rotate) body.rotate = true;
|
|
616
753
|
if (v.sponsor !== void 0) {
|
|
617
754
|
if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
|
|
618
755
|
body.sponsor = v.sponsor === "on";
|
|
@@ -622,7 +759,7 @@ async function agentsDemo(id, v) {
|
|
|
622
759
|
d,
|
|
623
760
|
() => [
|
|
624
761
|
`${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
|
|
625
|
-
dim(`
|
|
762
|
+
dim(`expires: ${d.venceEl ? new Date(d.venceEl).toLocaleString("es-MX") : "never"} \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
|
|
626
763
|
d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
|
|
627
764
|
d.starter ? `starter: \u2728 ${d.starter}` : dim("starter: (none)"),
|
|
628
765
|
d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
|
|
@@ -635,8 +772,8 @@ async function agentsDemo(id, v) {
|
|
|
635
772
|
// src/commands/chat.ts
|
|
636
773
|
import { randomUUID } from "node:crypto";
|
|
637
774
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
638
|
-
import { readFile as
|
|
639
|
-
import { basename } from "node:path";
|
|
775
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
776
|
+
import { basename as basename2 } from "node:path";
|
|
640
777
|
|
|
641
778
|
// src/sse.ts
|
|
642
779
|
async function* readSse(body) {
|
|
@@ -671,8 +808,8 @@ function parseData(e) {
|
|
|
671
808
|
}
|
|
672
809
|
|
|
673
810
|
// src/commands/chat.ts
|
|
674
|
-
var
|
|
675
|
-
var convPath = (agent, rest = "") => `/api/v2/me/agents/${
|
|
811
|
+
var enc3 = encodeURIComponent;
|
|
812
|
+
var convPath = (agent, rest = "") => `/api/v2/me/agents/${enc3(agent)}/conversations${rest}`;
|
|
676
813
|
async function conversationsLs(agent) {
|
|
677
814
|
if (!agent) throw usage("Missing agent id.", "Example: ghosty conversations ls <agent-id>");
|
|
678
815
|
const r = await api(convPath(agent));
|
|
@@ -682,17 +819,17 @@ async function conversationsLs(agent) {
|
|
|
682
819
|
async function turn(agent, sid, text, first) {
|
|
683
820
|
const turnId = randomUUID();
|
|
684
821
|
const ctrl = new AbortController();
|
|
685
|
-
const res = await request(convPath(agent, `/${
|
|
822
|
+
const res = await request(convPath(agent, `/${enc3(sid)}/events`), { signal: ctrl.signal, headers: { accept: "text/event-stream" } });
|
|
686
823
|
let cancelled = false;
|
|
687
824
|
const onSigint = () => {
|
|
688
825
|
if (cancelled) process.exit(130);
|
|
689
826
|
cancelled = true;
|
|
690
827
|
info(dim("\n(cancelling\u2026 press Ctrl-C again to quit)"));
|
|
691
|
-
void api(convPath(agent, `/${
|
|
828
|
+
void api(convPath(agent, `/${enc3(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
|
|
692
829
|
});
|
|
693
830
|
};
|
|
694
831
|
process.on("SIGINT", onSigint);
|
|
695
|
-
const sent = api(convPath(agent, `/${
|
|
832
|
+
const sent = api(convPath(agent, `/${enc3(sid)}/messages`), { body: { content: text, turnId, primerTurno: first } });
|
|
696
833
|
let answer = "";
|
|
697
834
|
let usageData;
|
|
698
835
|
let ours = false;
|
|
@@ -771,7 +908,7 @@ async function meFiles(sub, arg, v) {
|
|
|
771
908
|
switch (sub) {
|
|
772
909
|
case "ls": {
|
|
773
910
|
const r = await api(
|
|
774
|
-
`/api/v2/me/files${v.kind ? `?kind=${
|
|
911
|
+
`/api/v2/me/files${v.kind ? `?kind=${enc3(v.kind)}` : ""}`
|
|
775
912
|
);
|
|
776
913
|
out(r, () => {
|
|
777
914
|
const rows = r.files.map((f) => ({ id: f.id, name: f.name, kind: f.kind, size: f.size, created: f.createdAt.slice(0, 10) }));
|
|
@@ -782,8 +919,8 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
|
|
|
782
919
|
}
|
|
783
920
|
case "upload": {
|
|
784
921
|
if (!arg) throw usage("Missing file.", ex);
|
|
785
|
-
const bytes = await
|
|
786
|
-
const r = await api(`/api/v2/me/files?nombre=${
|
|
922
|
+
const bytes = await readFile4(arg);
|
|
923
|
+
const r = await api(`/api/v2/me/files?nombre=${enc3(basename2(arg))}`, {
|
|
787
924
|
method: "POST",
|
|
788
925
|
raw: new Uint8Array(bytes),
|
|
789
926
|
headers: { "content-type": "application/octet-stream" }
|
|
@@ -793,7 +930,7 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
|
|
|
793
930
|
}
|
|
794
931
|
case "rm": {
|
|
795
932
|
if (!arg) throw usage("Missing file id.", ex);
|
|
796
|
-
out(await api(`/api/v2/me/files/${
|
|
933
|
+
out(await api(`/api/v2/me/files/${enc3(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} Deleted ${arg}`);
|
|
797
934
|
return;
|
|
798
935
|
}
|
|
799
936
|
default:
|
|
@@ -816,7 +953,7 @@ async function meUsage() {
|
|
|
816
953
|
}
|
|
817
954
|
|
|
818
955
|
// src/commands/schedule.ts
|
|
819
|
-
var
|
|
956
|
+
var enc4 = encodeURIComponent;
|
|
820
957
|
function minutesOf(s, flag) {
|
|
821
958
|
const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s.trim());
|
|
822
959
|
if (!m) throw usage(`Invalid ${flag}: ${s}.`, "Use minutes, hours or days: 30m, 12h, 1d");
|
|
@@ -832,7 +969,7 @@ function isoOf(s, flag) {
|
|
|
832
969
|
var every = (min) => min == null ? "once" : min % 1440 === 0 ? `${min / 1440}d` : min % 60 === 0 ? `${min / 60}h` : `${min}m`;
|
|
833
970
|
var short = (s, n = 60) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
|
|
834
971
|
async function agentSchedule(agent) {
|
|
835
|
-
return api(`/api/v2/me/agents/${
|
|
972
|
+
return api(`/api/v2/me/agents/${enc4(agent)}/schedule`);
|
|
836
973
|
}
|
|
837
974
|
async function scheduleCmd(sub, agent, arg, v) {
|
|
838
975
|
if (sub === "ls" || sub === "list") {
|
|
@@ -875,13 +1012,13 @@ async function scheduleCmd(sub, agent, arg, v) {
|
|
|
875
1012
|
}
|
|
876
1013
|
let r;
|
|
877
1014
|
try {
|
|
878
|
-
r = await api(convPath(agent, `/${
|
|
1015
|
+
r = await api(convPath(agent, `/${enc4(sid)}/schedule`), { body });
|
|
879
1016
|
} catch (e) {
|
|
880
|
-
if (created) await api(convPath(agent, `/${
|
|
1017
|
+
if (created) await api(convPath(agent, `/${enc4(sid)}`), { method: "DELETE" }).catch(() => {
|
|
881
1018
|
});
|
|
882
1019
|
throw e;
|
|
883
1020
|
}
|
|
884
|
-
if (v.title && created) await api(convPath(agent, `/${
|
|
1021
|
+
if (v.title && created) await api(convPath(agent, `/${enc4(sid)}`), { method: "PATCH", body: { title: v.title } });
|
|
885
1022
|
const p = r.programado;
|
|
886
1023
|
return out(
|
|
887
1024
|
{ scheduled: p },
|
|
@@ -893,7 +1030,7 @@ ${dim(`conversation ${p.sessionId}`)}`
|
|
|
893
1030
|
if (!agent || !arg) throw usage("Missing agent id or schedule id.", "Example: ghosty schedule rm <agent-id> <schedule-id> (ids: ghosty schedule ls <agent-id>)");
|
|
894
1031
|
const row = (await agentSchedule(agent)).pendientes.find((p) => p.id === arg);
|
|
895
1032
|
if (!row) throw usage(`No pending scheduled turn ${arg}.`, "See them with: ghosty schedule ls <agent-id>");
|
|
896
|
-
await api(convPath(agent, `/${
|
|
1033
|
+
await api(convPath(agent, `/${enc4(row.sessionId)}/schedule/${enc4(arg)}`), { method: "DELETE" });
|
|
897
1034
|
return out({ canceled: arg }, () => `${lavender("\u2713")} Canceled ${arg}`);
|
|
898
1035
|
}
|
|
899
1036
|
throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
|
|
@@ -901,7 +1038,7 @@ ${dim(`conversation ${p.sessionId}`)}`
|
|
|
901
1038
|
|
|
902
1039
|
// src/commands/partner.ts
|
|
903
1040
|
import { createHmac } from "node:crypto";
|
|
904
|
-
import { readFile as
|
|
1041
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
905
1042
|
var PARTNER_HELP = `${bold("Usage:")} ghosty partner <command> [options]
|
|
906
1043
|
|
|
907
1044
|
${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
|
|
@@ -932,8 +1069,8 @@ ${bold("Options")}
|
|
|
932
1069
|
--json Machine-readable output
|
|
933
1070
|
|
|
934
1071
|
${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
|
|
935
|
-
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
936
|
-
var
|
|
1072
|
+
${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`;
|
|
1073
|
+
var enc5 = encodeURIComponent;
|
|
937
1074
|
function partnerCreds() {
|
|
938
1075
|
const key = process.env.GHOSTY_PARTNER_KEY;
|
|
939
1076
|
const secret = process.env.GHOSTY_PARTNER_SECRET;
|
|
@@ -953,7 +1090,7 @@ async function call(path, v, init = {}) {
|
|
|
953
1090
|
"x-ghosty-ts": ts,
|
|
954
1091
|
"x-ghosty-sig": sig,
|
|
955
1092
|
"content-type": "application/json",
|
|
956
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1093
|
+
"user-agent": `ghosty-cli/${"0.9.1"}`
|
|
957
1094
|
},
|
|
958
1095
|
body: method === "GET" ? void 0 : raw
|
|
959
1096
|
});
|
|
@@ -1001,10 +1138,10 @@ async function credentialsCmd(sub, id, v) {
|
|
|
1001
1138
|
if (sub === "rotate") {
|
|
1002
1139
|
const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
|
|
1003
1140
|
if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage("--grace must be minutes \u2265 0.");
|
|
1004
|
-
return showSecret(await json(`/api/v2/partner/credentials/${
|
|
1141
|
+
return showSecret(await json(`/api/v2/partner/credentials/${enc5(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
|
|
1005
1142
|
}
|
|
1006
1143
|
if (sub === "revoke" || sub === "rm") {
|
|
1007
|
-
const r = await json(`/api/v2/partner/credentials/${
|
|
1144
|
+
const r = await json(`/api/v2/partner/credentials/${enc5(id)}`, v, { ...owner, method: "DELETE" });
|
|
1008
1145
|
return out(r, () => `${lavender("\u2713")} Revoked ${r.keyId}.`);
|
|
1009
1146
|
}
|
|
1010
1147
|
throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty partner credentials ls | create | rotate | revoke");
|
|
@@ -1023,7 +1160,7 @@ async function agentCmd(sub, v) {
|
|
|
1023
1160
|
}
|
|
1024
1161
|
if (sub === "apply" || sub === "set") {
|
|
1025
1162
|
if (!v.file) throw usage("Missing --file.", "Example: ghosty partner agent apply --file agent.json");
|
|
1026
|
-
const body = JSON.parse(await
|
|
1163
|
+
const body = JSON.parse(await readFile5(v.file, "utf8"));
|
|
1027
1164
|
const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
|
|
1028
1165
|
return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} \xB7 v${r.agent.version}`);
|
|
1029
1166
|
}
|
|
@@ -1035,7 +1172,7 @@ async function tenantsCmd(sub, id, v) {
|
|
|
1035
1172
|
return out(r, () => table(r.tenants.map((t) => ({ externalId: t.externalId, config: JSON.stringify(t.config ?? {}), updated: fmt(t.updatedAt) })), ["externalId", "config", "updated"]));
|
|
1036
1173
|
}
|
|
1037
1174
|
if (!id) throw usage("Missing externalId.", `Example: ghosty partner tenants ${sub} org_123`);
|
|
1038
|
-
const path = `/api/v2/partner/tenants/${
|
|
1175
|
+
const path = `/api/v2/partner/tenants/${enc5(id)}`;
|
|
1039
1176
|
if (sub === "get") return out(await json(path, v));
|
|
1040
1177
|
if (sub === "set" || sub === "apply") {
|
|
1041
1178
|
if (!v.config) throw usage("Missing --config.", `Example: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
|
|
@@ -1119,8 +1256,8 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
1119
1256
|
}
|
|
1120
1257
|
|
|
1121
1258
|
// src/commands/data.ts
|
|
1122
|
-
var
|
|
1123
|
-
var agentPath2 = (id, rest = "") => `/api/v2/agents/${
|
|
1259
|
+
var enc6 = encodeURIComponent;
|
|
1260
|
+
var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc6(id)}${rest}`;
|
|
1124
1261
|
var need2 = (v, what, example) => {
|
|
1125
1262
|
if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
|
|
1126
1263
|
return v;
|
|
@@ -1136,7 +1273,7 @@ async function dbsImport(id, db, v) {
|
|
|
1136
1273
|
const easybitsKey = process.env.EASYBITS_API_KEY;
|
|
1137
1274
|
if (!easybitsKey) throw usage("EASYBITS_API_KEY is not set.", "Export your EasyBits API key (https://www.easybits.cloud/dash/developer) and run again.");
|
|
1138
1275
|
const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {} };
|
|
1139
|
-
const path = agentPath2(id, `/dbs/${
|
|
1276
|
+
const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
|
|
1140
1277
|
const dry = await api(path, { body: { ...body, dryRun: true } });
|
|
1141
1278
|
if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
|
|
1142
1279
|
${planText(dry.plan)}
|
|
@@ -1160,7 +1297,7 @@ async function dbsCmd(sub, id, db, rest, v) {
|
|
|
1160
1297
|
}
|
|
1161
1298
|
case "tables": {
|
|
1162
1299
|
db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
|
|
1163
|
-
const r = await api(agentPath2(id, `/dbs/${
|
|
1300
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
|
|
1164
1301
|
const ext = new Set(r.externalWriteTables);
|
|
1165
1302
|
const rows = r.tables.map((t) => ({ table: t.name, rows: t.rows, "customer writes": ext.has(t.name) ? "yes" : "", columns: t.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
|
|
1166
1303
|
return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
|
|
@@ -1170,7 +1307,7 @@ ${table(rows, ["table", "rows", "customer writes", "columns"])}`);
|
|
|
1170
1307
|
case "sql": {
|
|
1171
1308
|
db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
|
|
1172
1309
|
const sql = need2(rest.join(" ").trim() || void 0, "SQL", 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
|
|
1173
|
-
const r = await api(agentPath2(id, `/dbs/${
|
|
1310
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
|
|
1174
1311
|
if (e instanceof CliError && !v.write && /lectura|SELECT|escribir/i.test(e.message)) throw new CliError(e.message, EXIT.API, "To write on purpose, add --write.");
|
|
1175
1312
|
throw e;
|
|
1176
1313
|
});
|
|
@@ -1184,7 +1321,7 @@ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
|
|
|
1184
1321
|
case "grant": {
|
|
1185
1322
|
db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
|
|
1186
1323
|
const body = v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {};
|
|
1187
|
-
const r = await api(agentPath2(id, `/dbs/${
|
|
1324
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "PUT", body });
|
|
1188
1325
|
return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
|
|
1189
1326
|
}
|
|
1190
1327
|
case "import": {
|
|
@@ -1193,7 +1330,7 @@ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
|
|
|
1193
1330
|
}
|
|
1194
1331
|
case "revoke": {
|
|
1195
1332
|
db = need2(db, "database (id or name)", "ghosty dbs revoke <agent-id> <db>");
|
|
1196
|
-
const r = await api(agentPath2(id, `/dbs/${
|
|
1333
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "DELETE" });
|
|
1197
1334
|
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
|
|
1198
1335
|
}
|
|
1199
1336
|
default:
|
|
@@ -1215,12 +1352,12 @@ async function agentDocsCmd(sub, id, doc, v) {
|
|
|
1215
1352
|
if (v.write) body.mode = "write";
|
|
1216
1353
|
if (v["no-deliver"]) body.deliverable = false;
|
|
1217
1354
|
else if (v.deliver) body.deliverable = true;
|
|
1218
|
-
const r = await api(agentPath2(id, `/documents/${
|
|
1355
|
+
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
|
|
1219
1356
|
return out(r, () => `${lavender("\u2713")} ${r.name}: ${r.mode === "write" ? "reads and replaces it" : "reads it"}${r.deliverable ? ", and can send it" : ", never sends it"}.`);
|
|
1220
1357
|
}
|
|
1221
1358
|
case "revoke": {
|
|
1222
1359
|
doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
|
|
1223
|
-
const r = await api(agentPath2(id, `/documents/${
|
|
1360
|
+
const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
|
|
1224
1361
|
return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
|
|
1225
1362
|
}
|
|
1226
1363
|
default:
|
|
@@ -1344,7 +1481,7 @@ async function credentialsCmd2(sub, provider, v) {
|
|
|
1344
1481
|
}
|
|
1345
1482
|
|
|
1346
1483
|
// src/commands/channels.ts
|
|
1347
|
-
var
|
|
1484
|
+
var enc7 = encodeURIComponent;
|
|
1348
1485
|
var digits = (s) => s.replace(/\D/g, "");
|
|
1349
1486
|
function find(nums, ref) {
|
|
1350
1487
|
const d = digits(ref);
|
|
@@ -1356,7 +1493,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
|
|
|
1356
1493
|
if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
|
|
1357
1494
|
if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
|
|
1358
1495
|
if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
|
|
1359
|
-
const path = `/api/v2/agents/${
|
|
1496
|
+
const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
|
|
1360
1497
|
const cur = await api(path);
|
|
1361
1498
|
if (!sub || sub === "ls" || sub === "list") {
|
|
1362
1499
|
const rows = cur.numbers.map((n2) => ({ number: n2.label, answers: n2.mine ? lavender("this agent") : n2.takenByOther ? "another agent" : dim("nobody (silent)"), id: n2.id }));
|
|
@@ -1400,6 +1537,7 @@ Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new mes
|
|
|
1400
1537
|
|
|
1401
1538
|
// src/help.ts
|
|
1402
1539
|
var DOCS = "https://www.ghosty.studio/docs/cli";
|
|
1540
|
+
var REF = `${DOCS}/comandos`;
|
|
1403
1541
|
var HELPS = {
|
|
1404
1542
|
agents: {
|
|
1405
1543
|
summary: "Create, inspect, change, check and delete agents.",
|
|
@@ -1411,7 +1549,7 @@ var HELPS = {
|
|
|
1411
1549
|
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1412
1550
|
"ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
|
|
1413
1551
|
"ghosty agents restart <agent-id>",
|
|
1414
|
-
|
|
1552
|
+
'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]',
|
|
1415
1553
|
"ghosty agents rm <agent-id> [--yes]"
|
|
1416
1554
|
],
|
|
1417
1555
|
flags: [
|
|
@@ -1422,17 +1560,28 @@ var HELPS = {
|
|
|
1422
1560
|
["--verbose", "doctor: also list the checks that passed"],
|
|
1423
1561
|
["--write", "docs grant: the agent may replace the document (default: read only)"],
|
|
1424
1562
|
["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
|
|
1425
|
-
["--yes", "rm: skip the confirmation (required without a terminal)"]
|
|
1563
|
+
["--yes", "rm: skip the confirmation (required without a terminal)"],
|
|
1564
|
+
["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
|
|
1565
|
+
["--on | --off", "demo: turn the public link on or off"],
|
|
1566
|
+
["--vence DATE|no", `demo: fixed expiry in your machine's time ("2026-10-15T20:00"); no = never`],
|
|
1567
|
+
["--turns N", "demo: messages per visitor (default 150)"],
|
|
1568
|
+
["--per-ip N", "demo: messages per IP per day (default 30)"],
|
|
1569
|
+
["--welcome TEXT", "demo: chat greeting (empty = default)"],
|
|
1570
|
+
["--starter TEXT", 'demo: highlighted first suggestion ("" removes it)'],
|
|
1571
|
+
['--chips "a|b|c"', "demo: suggestions, separated by |"],
|
|
1572
|
+
["--sponsor on|off", "demo: copies people keep run on YOUR engine key"],
|
|
1573
|
+
["--rotate", "demo: new code; the old link stops working"]
|
|
1426
1574
|
],
|
|
1427
1575
|
examples: [
|
|
1428
1576
|
"ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
|
|
1429
1577
|
"ghosty agents set <id> --prompt-file PROMPT.md",
|
|
1430
1578
|
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1431
1579
|
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1432
|
-
"ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
|
|
1580
|
+
"ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
|
|
1581
|
+
'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"'
|
|
1433
1582
|
],
|
|
1434
|
-
notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
|
|
1435
|
-
learn: `${
|
|
1583
|
+
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."],
|
|
1584
|
+
learn: `${REF}#agentes`
|
|
1436
1585
|
},
|
|
1437
1586
|
skills: {
|
|
1438
1587
|
summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
|
|
@@ -1455,7 +1604,7 @@ var HELPS = {
|
|
|
1455
1604
|
"ghosty skills rm <id> cotizacion # keeps a local copy first"
|
|
1456
1605
|
],
|
|
1457
1606
|
notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
|
|
1458
|
-
learn: `${
|
|
1607
|
+
learn: `${REF}#skills`
|
|
1459
1608
|
},
|
|
1460
1609
|
dbs: {
|
|
1461
1610
|
summary: "Databases an agent can use, and what it may write from customer channels.",
|
|
@@ -1484,7 +1633,7 @@ var HELPS = {
|
|
|
1484
1633
|
"ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
|
|
1485
1634
|
],
|
|
1486
1635
|
notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
|
|
1487
|
-
learn: `${
|
|
1636
|
+
learn: `${REF}#bases-de-datos`
|
|
1488
1637
|
},
|
|
1489
1638
|
turns: {
|
|
1490
1639
|
summary: "An agent's recent turns: what answered, what failed and why.",
|
|
@@ -1496,43 +1645,68 @@ var HELPS = {
|
|
|
1496
1645
|
["--limit N", "max rows (default 20, max 500)"]
|
|
1497
1646
|
],
|
|
1498
1647
|
examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
|
|
1499
|
-
learn: `${
|
|
1648
|
+
learn: `${REF}#turnos`
|
|
1500
1649
|
},
|
|
1501
1650
|
files: {
|
|
1502
1651
|
summary: "Files in the agent's machine.",
|
|
1503
1652
|
usage: ["ghosty files ls <agent-id> [dir]", "ghosty files get <agent-id> <path> [--out LOCAL]", "ghosty files put <agent-id> <path> [--file LOCAL | < stdin]", "ghosty files rm <agent-id> <path> [--yes]"],
|
|
1504
1653
|
examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
|
|
1505
|
-
learn: `${
|
|
1654
|
+
learn: `${REF}#archivos`
|
|
1506
1655
|
},
|
|
1507
1656
|
mcp: {
|
|
1508
1657
|
summary: "MCP servers of an agent. set REPLACES the whole list.",
|
|
1509
1658
|
usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
|
|
1510
1659
|
examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
|
|
1511
|
-
learn: `${
|
|
1660
|
+
learn: `${REF}#servidores-mcp`
|
|
1512
1661
|
},
|
|
1513
1662
|
try: {
|
|
1514
|
-
summary: "
|
|
1515
|
-
usage: [
|
|
1516
|
-
|
|
1517
|
-
|
|
1663
|
+
summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
|
|
1664
|
+
usage: [
|
|
1665
|
+
'ghosty try <agent-id> "message" [--session ID] [--reset]',
|
|
1666
|
+
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
|
|
1667
|
+
"ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
|
|
1668
|
+
],
|
|
1669
|
+
flags: [
|
|
1670
|
+
["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
|
|
1671
|
+
["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
|
|
1672
|
+
["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
|
|
1673
|
+
["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
|
|
1674
|
+
["--reset", "forget that test session (with --waba also its test thread)"],
|
|
1675
|
+
["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
|
|
1676
|
+
["--out DIR", "replay: where replay.json is written, after every step"],
|
|
1677
|
+
["--max N", "replay: customer turns per conversation (default 15)"],
|
|
1678
|
+
["--concurrency N", "replay: conversations at the same time (default 8, max 30)"],
|
|
1679
|
+
["--resume", "replay: continue from DIR/replay.json, skipping finished steps"]
|
|
1680
|
+
],
|
|
1681
|
+
examples: [
|
|
1682
|
+
'ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"',
|
|
1683
|
+
'ghosty try <id> "cot\xEDzame esto" --waba --session 5210000000009 --media lista.jpg',
|
|
1684
|
+
'ghosty try <id> "\xBFy el env\xEDo?" --waba --session 5210000000009 --history hilo.json --reset',
|
|
1685
|
+
"ghosty try <id> --replay muestra.json --out ./replay --max 5"
|
|
1686
|
+
],
|
|
1687
|
+
notes: [
|
|
1688
|
+
"Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
|
|
1689
|
+
"--whatsapp is now an alias of --waba."
|
|
1690
|
+
],
|
|
1691
|
+
learn: `${REF}#turnos-de-prueba`
|
|
1518
1692
|
},
|
|
1519
1693
|
chat: {
|
|
1520
1694
|
summary: "Stream a conversation; with no message it is interactive.",
|
|
1521
1695
|
usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
|
|
1522
1696
|
examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
|
|
1523
|
-
learn: `${
|
|
1697
|
+
learn: `${REF}#conversaciones`
|
|
1524
1698
|
},
|
|
1525
1699
|
schedule: {
|
|
1526
1700
|
summary: "Turns that run later, on the agent's own clock.",
|
|
1527
1701
|
usage: ['ghosty schedule add <agent-id> "what to do" --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]', "ghosty schedule ls <agent-id>", "ghosty schedule rm <agent-id> <schedule-id>"],
|
|
1528
1702
|
examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
|
|
1529
|
-
learn: `${
|
|
1703
|
+
learn: `${REF}#programar-turnos`
|
|
1530
1704
|
},
|
|
1531
1705
|
me: {
|
|
1532
1706
|
summary: "Your account: files and usage.",
|
|
1533
1707
|
usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
|
|
1534
1708
|
examples: ["ghosty me files ls --kind document"],
|
|
1535
|
-
learn: `${
|
|
1709
|
+
learn: `${REF}#tu-cuenta`
|
|
1536
1710
|
}
|
|
1537
1711
|
};
|
|
1538
1712
|
HELPS.credentials = {
|
|
@@ -1548,7 +1722,7 @@ HELPS.credentials = {
|
|
|
1548
1722
|
"op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
|
|
1549
1723
|
],
|
|
1550
1724
|
notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
|
|
1551
|
-
learn: `${
|
|
1725
|
+
learn: `${REF}#credenciales`
|
|
1552
1726
|
};
|
|
1553
1727
|
HELPS.creds = HELPS.credentials;
|
|
1554
1728
|
HELPS.channels = {
|
|
@@ -1560,7 +1734,7 @@ HELPS.channels = {
|
|
|
1560
1734
|
],
|
|
1561
1735
|
examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
|
|
1562
1736
|
notes: ["<number> is its id, its label or its last digits. One number, one agent: enabling it takes it from the other agent.", "Disabling keeps logging messages; nobody answers them."],
|
|
1563
|
-
learn: `${
|
|
1737
|
+
learn: `${REF}#canales-de-whatsapp`
|
|
1564
1738
|
};
|
|
1565
1739
|
HELPS.board = {
|
|
1566
1740
|
summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
|
|
@@ -1574,10 +1748,10 @@ HELPS.board = {
|
|
|
1574
1748
|
],
|
|
1575
1749
|
examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
1576
1750
|
notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
1577
|
-
learn: `${
|
|
1751
|
+
learn: `${REF}#tablero`
|
|
1578
1752
|
};
|
|
1579
1753
|
HELPS.conversations = HELPS.chat;
|
|
1580
|
-
HELPS.usage = HELPS.me;
|
|
1754
|
+
HELPS.usage = { ...HELPS.me, summary: "Your plan and how much you've used this week and month.", usage: ["ghosty usage", "ghosty me usage"], examples: ["ghosty usage", "ghosty usage --json"], learn: `${REF}#uso` };
|
|
1581
1755
|
function helpText(cmd) {
|
|
1582
1756
|
const h = HELPS[cmd];
|
|
1583
1757
|
if (!h) return void 0;
|
|
@@ -1618,7 +1792,7 @@ ${bold("Agents")}
|
|
|
1618
1792
|
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
1619
1793
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
1620
1794
|
agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
|
|
1621
|
-
[--slug NAME] [--on | --off] [--
|
|
1795
|
+
[--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
|
|
1622
1796
|
|
|
1623
1797
|
${bold("Configure an agent")}
|
|
1624
1798
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
@@ -1629,7 +1803,9 @@ ${bold("Configure an agent")}
|
|
|
1629
1803
|
board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
1630
1804
|
|
|
1631
1805
|
${bold("Talk to an agent")}
|
|
1632
|
-
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
1806
|
+
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
1807
|
+
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
|
|
1808
|
+
--replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
|
|
1633
1809
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
1634
1810
|
conversations ls <id> List conversations
|
|
1635
1811
|
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
@@ -1692,6 +1868,13 @@ async function main() {
|
|
|
1692
1868
|
session: { type: "string" },
|
|
1693
1869
|
reset: { type: "boolean" },
|
|
1694
1870
|
whatsapp: { type: "boolean" },
|
|
1871
|
+
waba: { type: "boolean" },
|
|
1872
|
+
media: { type: "string", multiple: true },
|
|
1873
|
+
history: { type: "string" },
|
|
1874
|
+
replay: { type: "string" },
|
|
1875
|
+
max: { type: "string" },
|
|
1876
|
+
concurrency: { type: "string" },
|
|
1877
|
+
resume: { type: "boolean" },
|
|
1695
1878
|
conversation: { type: "string" },
|
|
1696
1879
|
kind: { type: "string" },
|
|
1697
1880
|
at: { type: "string" },
|
|
@@ -1710,11 +1893,12 @@ async function main() {
|
|
|
1710
1893
|
on: { type: "boolean" },
|
|
1711
1894
|
off: { type: "boolean" },
|
|
1712
1895
|
turns: { type: "string" },
|
|
1713
|
-
|
|
1896
|
+
vence: { type: "string" },
|
|
1714
1897
|
"per-ip": { type: "string" },
|
|
1715
1898
|
welcome: { type: "string" },
|
|
1716
1899
|
chips: { type: "string" },
|
|
1717
1900
|
starter: { type: "string" },
|
|
1901
|
+
rotate: { type: "boolean" },
|
|
1718
1902
|
sponsor: { type: "string" },
|
|
1719
1903
|
yes: { type: "boolean", short: "y" },
|
|
1720
1904
|
dir: { type: "string" },
|
|
@@ -1745,7 +1929,7 @@ async function main() {
|
|
|
1745
1929
|
const tokens = splitTokens(pos, v);
|
|
1746
1930
|
globals.token = tokens.session;
|
|
1747
1931
|
globals.profile = v.profile;
|
|
1748
|
-
if (v.version) return out({ version: "0.
|
|
1932
|
+
if (v.version) return out({ version: "0.9.1" }, () => "0.9.1");
|
|
1749
1933
|
const [cmd, sub, a1, a2] = pos;
|
|
1750
1934
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1751
1935
|
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostystudio/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --define:__VERSION__=\\\"$npm_package_version\\\"",
|
|
18
18
|
"prepack": "npm run build",
|
|
19
|
-
"test": "for t in flags prompt; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
|
|
19
|
+
"test": "for t in flags prompt api; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"ghosty",
|