@ghostystudio/cli 0.8.0 → 0.9.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 +9 -1
- package/dist/index.js +239 -67
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,9 @@ ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
|
|
|
30
30
|
ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
|
|
31
31
|
ghosty mcp get|set <id> [--file servers.json]
|
|
32
32
|
|
|
33
|
-
ghosty try <id> "message" [--session] [--reset]
|
|
33
|
+
ghosty try <id> "message" [--session] [--reset]
|
|
34
|
+
ghosty try <id> "message" --waba --session PHONE [--media FILE] [--history FILE] # real WhatsApp path, nothing sent
|
|
35
|
+
ghosty try <id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]
|
|
34
36
|
ghosty chat <id> ["message"] [--conversation ID]
|
|
35
37
|
ghosty conversations ls <id>
|
|
36
38
|
|
|
@@ -49,6 +51,12 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
|
|
|
49
51
|
|
|
50
52
|
Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
51
53
|
|
|
54
|
+
### Changes in 0.9.0
|
|
55
|
+
|
|
56
|
+
- `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.
|
|
57
|
+
- `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.
|
|
58
|
+
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
59
|
+
|
|
52
60
|
### Changes in 0.8.0
|
|
53
61
|
|
|
54
62
|
- 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.0"}`, "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.0"}`,
|
|
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."}`);
|
|
@@ -613,6 +749,7 @@ async function agentsDemo(id, v) {
|
|
|
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";
|
|
@@ -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)")}
|
|
@@ -933,7 +1070,7 @@ ${bold("Options")}
|
|
|
933
1070
|
|
|
934
1071
|
${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
|
|
935
1072
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
936
|
-
var
|
|
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.0"}`
|
|
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 }));
|
|
@@ -1511,9 +1648,34 @@ var HELPS = {
|
|
|
1511
1648
|
learn: `${DOCS}#mcp`
|
|
1512
1649
|
},
|
|
1513
1650
|
try: {
|
|
1514
|
-
summary: "
|
|
1515
|
-
usage: [
|
|
1516
|
-
|
|
1651
|
+
summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
|
|
1652
|
+
usage: [
|
|
1653
|
+
'ghosty try <agent-id> "message" [--session ID] [--reset]',
|
|
1654
|
+
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
|
|
1655
|
+
"ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
|
|
1656
|
+
],
|
|
1657
|
+
flags: [
|
|
1658
|
+
["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
|
|
1659
|
+
["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
|
|
1660
|
+
["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
|
|
1661
|
+
["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
|
|
1662
|
+
["--reset", "forget that test session (with --waba also its test thread)"],
|
|
1663
|
+
["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
|
|
1664
|
+
["--out DIR", "replay: where replay.json is written, after every step"],
|
|
1665
|
+
["--max N", "replay: customer turns per conversation (default 15)"],
|
|
1666
|
+
["--concurrency N", "replay: conversations at the same time (default 8, max 30)"],
|
|
1667
|
+
["--resume", "replay: continue from DIR/replay.json, skipping finished steps"]
|
|
1668
|
+
],
|
|
1669
|
+
examples: [
|
|
1670
|
+
'ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"',
|
|
1671
|
+
'ghosty try <id> "cot\xEDzame esto" --waba --session 5210000000009 --media lista.jpg',
|
|
1672
|
+
'ghosty try <id> "\xBFy el env\xEDo?" --waba --session 5210000000009 --history hilo.json --reset',
|
|
1673
|
+
"ghosty try <id> --replay muestra.json --out ./replay --max 5"
|
|
1674
|
+
],
|
|
1675
|
+
notes: [
|
|
1676
|
+
"Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
|
|
1677
|
+
"--whatsapp is now an alias of --waba."
|
|
1678
|
+
],
|
|
1517
1679
|
learn: `${DOCS}#try`
|
|
1518
1680
|
},
|
|
1519
1681
|
chat: {
|
|
@@ -1618,7 +1780,7 @@ ${bold("Agents")}
|
|
|
1618
1780
|
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
1619
1781
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
1620
1782
|
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] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off]
|
|
1783
|
+
[--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
|
|
1622
1784
|
|
|
1623
1785
|
${bold("Configure an agent")}
|
|
1624
1786
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
@@ -1629,7 +1791,9 @@ ${bold("Configure an agent")}
|
|
|
1629
1791
|
board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
1630
1792
|
|
|
1631
1793
|
${bold("Talk to an agent")}
|
|
1632
|
-
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
1794
|
+
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
1795
|
+
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
|
|
1796
|
+
--replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
|
|
1633
1797
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
1634
1798
|
conversations ls <id> List conversations
|
|
1635
1799
|
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
@@ -1692,6 +1856,13 @@ async function main() {
|
|
|
1692
1856
|
session: { type: "string" },
|
|
1693
1857
|
reset: { type: "boolean" },
|
|
1694
1858
|
whatsapp: { type: "boolean" },
|
|
1859
|
+
waba: { type: "boolean" },
|
|
1860
|
+
media: { type: "string", multiple: true },
|
|
1861
|
+
history: { type: "string" },
|
|
1862
|
+
replay: { type: "string" },
|
|
1863
|
+
max: { type: "string" },
|
|
1864
|
+
concurrency: { type: "string" },
|
|
1865
|
+
resume: { type: "boolean" },
|
|
1695
1866
|
conversation: { type: "string" },
|
|
1696
1867
|
kind: { type: "string" },
|
|
1697
1868
|
at: { type: "string" },
|
|
@@ -1715,6 +1886,7 @@ async function main() {
|
|
|
1715
1886
|
welcome: { type: "string" },
|
|
1716
1887
|
chips: { type: "string" },
|
|
1717
1888
|
starter: { type: "string" },
|
|
1889
|
+
rotate: { type: "boolean" },
|
|
1718
1890
|
sponsor: { type: "string" },
|
|
1719
1891
|
yes: { type: "boolean", short: "y" },
|
|
1720
1892
|
dir: { type: "string" },
|
|
@@ -1745,7 +1917,7 @@ async function main() {
|
|
|
1745
1917
|
const tokens = splitTokens(pos, v);
|
|
1746
1918
|
globals.token = tokens.session;
|
|
1747
1919
|
globals.profile = v.profile;
|
|
1748
|
-
if (v.version) return out({ version: "0.
|
|
1920
|
+
if (v.version) return out({ version: "0.9.0" }, () => "0.9.0");
|
|
1749
1921
|
const [cmd, sub, a1, a2] = pos;
|
|
1750
1922
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1751
1923
|
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.0",
|
|
4
4
|
"description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --define:__VERSION__=\\\"$npm_package_version\\\"",
|
|
18
18
|
"prepack": "npm run build",
|
|
19
|
-
"test": "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",
|