@ghostystudio/cli 0.9.1 → 0.10.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 +12 -2
- package/dist/index.js +146 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ ghosty agents get <id> [--fields name,model,prompt]
|
|
|
21
21
|
ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…]
|
|
22
22
|
ghosty agents rm <id> [--yes]
|
|
23
23
|
ghosty agents restart <id>
|
|
24
|
+
ghosty agents box <id> [--check PATH]… [--contains PATH=TEXT]… [--recycle] # is its machine on the new image?
|
|
24
25
|
ghosty agents demo <id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N]
|
|
25
26
|
[--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate] # public /p/ link, no account needed
|
|
26
27
|
ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
|
|
@@ -28,9 +29,11 @@ ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
|
|
|
28
29
|
ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
|
|
29
30
|
|
|
30
31
|
ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
|
|
31
|
-
ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR]
|
|
32
|
+
ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR] # get without --out only lists
|
|
33
|
+
ghosty skills templates <id> # pdf-doc templates: its own and Ghosty's, and which folder wins
|
|
32
34
|
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
35
|
ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
|
|
36
|
+
ghosty turns show <id> [turn-id|last] # where a turn's time went: each tool with its seconds, output tokens
|
|
34
37
|
ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
|
|
35
38
|
ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
|
|
36
39
|
ghosty mcp get|set <id> [--file servers.json]
|
|
@@ -52,7 +55,7 @@ ghosty docs
|
|
|
52
55
|
ghosty partner ls|enable|credentials|agent|tenants|audit|try # one agent for each of YOUR customers: ghosty partner --help
|
|
53
56
|
```
|
|
54
57
|
|
|
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.
|
|
58
|
+
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`, `GHOSTY_NO_UPDATE_CHECK`; partner commands sign with `GHOSTY_PARTNER_KEY` + `GHOSTY_PARTNER_SECRET` when set.
|
|
56
59
|
|
|
57
60
|
Aliases: `creds` = `credentials`, `db` = `dbs`, `dbs sql` = `dbs query`.
|
|
58
61
|
|
|
@@ -71,6 +74,13 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
|
71
74
|
- `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
75
|
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
73
76
|
|
|
77
|
+
### Changes in 0.10.0
|
|
78
|
+
|
|
79
|
+
- Tells you when a new version is out (checks npm once a day, on stderr; `GHOSTY_NO_UPDATE_CHECK=1` or `CI` turns it off). An unknown option on an old version says to update.
|
|
80
|
+
- `skills get` without `--out` only lists the files; with `--out` it prints the absolute path. It no longer writes into the current folder silently.
|
|
81
|
+
- New: `turns show` (timeline of one turn), `skills templates` (pdf-doc templates and their source), `agents box` (check / recycle the machine serving an agent).
|
|
82
|
+
- `agents get` includes `env` (secret-looking values masked).
|
|
83
|
+
|
|
74
84
|
### Changes in 0.8.0
|
|
75
85
|
|
|
76
86
|
- 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
|
@@ -129,7 +129,7 @@ async function login() {
|
|
|
129
129
|
const verifier = b64url(randomBytes(32));
|
|
130
130
|
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
131
131
|
const state = b64url(randomBytes(16));
|
|
132
|
-
const { code, redirectUri } = await new Promise((
|
|
132
|
+
const { code, redirectUri } = await new Promise((resolve2, reject) => {
|
|
133
133
|
const server = createServer((req, res) => {
|
|
134
134
|
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
135
135
|
if (u.pathname !== "/cb") {
|
|
@@ -140,7 +140,7 @@ async function login() {
|
|
|
140
140
|
res.writeHead(ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" }).end(CLOSE_PAGE(ok, base));
|
|
141
141
|
clearTimeout(timer);
|
|
142
142
|
server.close();
|
|
143
|
-
if (ok)
|
|
143
|
+
if (ok) resolve2({ code: u.searchParams.get("code"), redirectUri: redirectUri2 });
|
|
144
144
|
else reject(new CliError(`Login was not completed (${u.searchParams.get("error") ?? "state mismatch"}).`, EXIT.AUTH, "Run: ghosty login"));
|
|
145
145
|
});
|
|
146
146
|
let redirectUri2 = "";
|
|
@@ -278,11 +278,11 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
278
278
|
const token = await currentToken();
|
|
279
279
|
const httpsRequest = base.startsWith("https:") ? (await import("node:https")).request : (await import("node:http")).request;
|
|
280
280
|
const data = JSON.stringify(body);
|
|
281
|
-
return new Promise((
|
|
281
|
+
return new Promise((resolve2) => {
|
|
282
282
|
const req = httpsRequest(`${base}${path}`, {
|
|
283
283
|
method: "POST",
|
|
284
284
|
timeout: timeoutMs,
|
|
285
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.
|
|
285
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.10.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
286
286
|
}, (res) => {
|
|
287
287
|
let buf = "";
|
|
288
288
|
res.on("data", (c) => buf += c);
|
|
@@ -293,11 +293,11 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
293
293
|
} catch {
|
|
294
294
|
parsed = { error: buf.slice(0, 300) };
|
|
295
295
|
}
|
|
296
|
-
|
|
296
|
+
resolve2({ status: res.statusCode ?? 0, retryAfter: retryAfterSeconds(res.headers["retry-after"]), body: parsed });
|
|
297
297
|
});
|
|
298
298
|
});
|
|
299
299
|
req.on("timeout", () => req.destroy(new Error(`no answer in ${Math.round(timeoutMs / 6e4)} min`)));
|
|
300
|
-
req.on("error", (e) =>
|
|
300
|
+
req.on("error", (e) => resolve2({ status: 0, retryAfter: null, body: { error: String(e) } }));
|
|
301
301
|
req.end(data);
|
|
302
302
|
});
|
|
303
303
|
}
|
|
@@ -324,7 +324,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
324
324
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
325
325
|
headers: {
|
|
326
326
|
authorization: `Bearer ${token}`,
|
|
327
|
-
"user-agent": `ghosty-cli/${"0.
|
|
327
|
+
"user-agent": `ghosty-cli/${"0.10.0"}`,
|
|
328
328
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
329
329
|
...opts.headers
|
|
330
330
|
},
|
|
@@ -358,7 +358,7 @@ async function toError(res) {
|
|
|
358
358
|
// src/commands/agents.ts
|
|
359
359
|
import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
360
360
|
import { homedir as homedir2 } from "node:os";
|
|
361
|
-
import { dirname as dirname2, join as join3, relative } from "node:path";
|
|
361
|
+
import { dirname as dirname2, join as join3, relative, resolve } from "node:path";
|
|
362
362
|
|
|
363
363
|
// src/prompt.ts
|
|
364
364
|
import { createInterface } from "node:readline/promises";
|
|
@@ -672,7 +672,7 @@ async function readSkillDir(dir) {
|
|
|
672
672
|
}
|
|
673
673
|
var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
674
674
|
async function skillsCmd(sub, id, slug, v) {
|
|
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>";
|
|
675
|
+
const ex = "ghosty skills ls <agent-id> | get <agent-id> <slug> [--out DIR] | templates <agent-id> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
|
|
676
676
|
id = need(id, "agent id", ex);
|
|
677
677
|
switch (sub ?? "ls") {
|
|
678
678
|
case "ls":
|
|
@@ -691,7 +691,13 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
|
|
|
691
691
|
case "get": {
|
|
692
692
|
slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
|
|
693
693
|
const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
|
|
694
|
-
|
|
694
|
+
if (!v.out) {
|
|
695
|
+
const files = sk.files.map((f) => ({ path: f.path, bytes: Buffer.from(f.contentBase64, "base64").length }));
|
|
696
|
+
out({ slug, files, saved: null }, () => `${table(files, ["path", "bytes"])}
|
|
697
|
+
${dim(`Download: ghosty skills get ${id} ${slug} --out DIR`)}`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const dir = resolve(v.out);
|
|
695
701
|
const bytes = await writeSkill(dir, sk);
|
|
696
702
|
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)`)}`);
|
|
697
703
|
return;
|
|
@@ -704,6 +710,13 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
|
|
|
704
710
|
out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
|
|
705
711
|
return;
|
|
706
712
|
}
|
|
713
|
+
case "templates": {
|
|
714
|
+
const r = await api(agentPath(id, "/templates"));
|
|
715
|
+
const rows = r.templates.map((t) => ({ name: t.name, source: t.source, used: t.active ? "yes" : "no (shadowed)", description: (t.description ?? "").replace(/\s+/g, " ").slice(0, 60) }));
|
|
716
|
+
out(r, () => `${table(rows, ["name", "source", "used", "description"])}
|
|
717
|
+
${dim(`pdf-doc reads from: ${r.active ?? "(not found)"}`)}`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
707
720
|
case "rm":
|
|
708
721
|
case "remove": {
|
|
709
722
|
slug = need(slug, "skill slug", ex);
|
|
@@ -768,6 +781,28 @@ async function agentsDemo(id, v) {
|
|
|
768
781
|
].join("\n")
|
|
769
782
|
);
|
|
770
783
|
}
|
|
784
|
+
async function agentsBox(id, v) {
|
|
785
|
+
id = need(id, "agent id", "ghosty agents box <agent-id> [--check /opt/claude-worker/skills/pdf-doc/templates/calendario.html] [--recycle]");
|
|
786
|
+
if (v.recycle) {
|
|
787
|
+
const r2 = await api(agentPath(id, "/box"), { method: "POST", body: { recycle: true } });
|
|
788
|
+
out(r2, () => `${lavender("\u2713")} Recycled. ${dim(r2?.note ?? "")}
|
|
789
|
+
${dim(`Verify after one turn: ghosty agents box ${id} --check <path>`)}`);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const q = new URLSearchParams();
|
|
793
|
+
for (const c of v.check ?? []) q.append("check", c);
|
|
794
|
+
for (const c of v.contains ?? []) q.append("contains", c);
|
|
795
|
+
const r = await api(agentPath(id, `/box?${q}`));
|
|
796
|
+
out(r, () => {
|
|
797
|
+
if (!r.boxes.length) return dim(r.note ?? "No live box.");
|
|
798
|
+
const lines = r.boxes.flatMap((b) => [
|
|
799
|
+
`${bold(b.sandboxId)} ${dim(b.lastMessageAt ? `last message ${b.lastMessageAt}` : r.kind)}${b.error ? ` ${red(b.error)}` : ""}`,
|
|
800
|
+
...b.checks.map((c) => ` ${c.ok ? lavender("\u2713") : red("\u2717")} ${c.path}${c.contains !== void 0 ? ` \u220B ${c.contains}` : ""}`)
|
|
801
|
+
]);
|
|
802
|
+
const verdict = !(v.check?.length || v.contains?.length) ? dim("Alive. Add --check PATH to confirm the new image.") : r.stale ? red(`${r.stale} box(es) on an OLD image: ghosty agents box ${id} --recycle`) : `${lavender("\u2713")} Serving with the expected image.`;
|
|
803
|
+
return [...lines, verdict].join("\n");
|
|
804
|
+
});
|
|
805
|
+
}
|
|
771
806
|
|
|
772
807
|
// src/commands/chat.ts
|
|
773
808
|
import { randomUUID } from "node:crypto";
|
|
@@ -1090,7 +1125,7 @@ async function call(path, v, init = {}) {
|
|
|
1090
1125
|
"x-ghosty-ts": ts,
|
|
1091
1126
|
"x-ghosty-sig": sig,
|
|
1092
1127
|
"content-type": "application/json",
|
|
1093
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1128
|
+
"user-agent": `ghosty-cli/${"0.10.0"}`
|
|
1094
1129
|
},
|
|
1095
1130
|
body: method === "GET" ? void 0 : raw
|
|
1096
1131
|
});
|
|
@@ -1364,8 +1399,9 @@ async function agentDocsCmd(sub, id, doc, v) {
|
|
|
1364
1399
|
throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
|
|
1365
1400
|
}
|
|
1366
1401
|
}
|
|
1367
|
-
async function turnsCmd(sub, id, v) {
|
|
1368
|
-
if (sub
|
|
1402
|
+
async function turnsCmd(sub, id, turnId, v) {
|
|
1403
|
+
if (sub === "show") return turnShow(id, turnId);
|
|
1404
|
+
if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h \xB7 ghosty turns show <agent-id> [turn-id|last]");
|
|
1369
1405
|
id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
|
|
1370
1406
|
const q = new URLSearchParams();
|
|
1371
1407
|
const status = v.errors ? "error" : v.status;
|
|
@@ -1375,6 +1411,7 @@ async function turnsCmd(sub, id, v) {
|
|
|
1375
1411
|
const r = await api(agentPath2(id, `/turns?${q}`));
|
|
1376
1412
|
const when = (s) => s.slice(0, 16).replace("T", " ");
|
|
1377
1413
|
const rows = r.turns.map((t) => ({
|
|
1414
|
+
id: t.id,
|
|
1378
1415
|
started: when(t.startedAt),
|
|
1379
1416
|
state: t.state === "error" ? red(t.state) : t.state,
|
|
1380
1417
|
secs: t.ms == null ? "" : Math.round(t.ms / 1e3),
|
|
@@ -1383,8 +1420,24 @@ async function turnsCmd(sub, id, v) {
|
|
|
1383
1420
|
error: (t.error ?? "").replace(/\s+/g, " ").slice(0, 90)
|
|
1384
1421
|
}));
|
|
1385
1422
|
const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
|
|
1386
|
-
out(r, () => rows.length ? `${table(rows, ["started", "state", "secs", "from", "session", "error"])}
|
|
1387
|
-
${dim(`${v.since ? `since ${v.since}: ` : ""}${tally}
|
|
1423
|
+
out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
|
|
1424
|
+
${dim(`${v.since ? `since ${v.since}: ` : ""}${tally} \xB7 where the time went: ghosty turns show ${id} <turn-id>`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
|
|
1425
|
+
}
|
|
1426
|
+
async function turnShow(id, turnId) {
|
|
1427
|
+
id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last]");
|
|
1428
|
+
const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
|
|
1429
|
+
out(r, () => {
|
|
1430
|
+
const t = r.turn;
|
|
1431
|
+
const head = `${bold(t.id)} ${t.state === "error" ? red(t.state) : t.state} ${t.secs ?? "?"} s ${dim(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`)}`;
|
|
1432
|
+
const line = (s) => {
|
|
1433
|
+
const at = `+${s.at.toFixed(1)}s`.padStart(8);
|
|
1434
|
+
if (s.kind === "tool") return `${at} ${bold(s.tool ?? "?")} ${dim(s.input ?? "")} ${s.secs === void 0 ? red("(no result)") : `${s.secs} s`}${s.error ? red(" error") : ""}`;
|
|
1435
|
+
const tok = s.outTokens ? dim(` [${s.outTokens} out]`) : "";
|
|
1436
|
+
return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
|
|
1437
|
+
};
|
|
1438
|
+
const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
|
|
1439
|
+
return [head, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
|
|
1440
|
+
});
|
|
1388
1441
|
}
|
|
1389
1442
|
async function agentsDoctor(id, v) {
|
|
1390
1443
|
id = need2(id, "agent id", "ghosty agents doctor <agent-id>");
|
|
@@ -1416,7 +1469,7 @@ async function hiddenPrompt(label) {
|
|
|
1416
1469
|
stdin.setRawMode(true);
|
|
1417
1470
|
stdin.resume();
|
|
1418
1471
|
stdin.setEncoding("utf8");
|
|
1419
|
-
return new Promise((
|
|
1472
|
+
return new Promise((resolve2, reject) => {
|
|
1420
1473
|
let value = "";
|
|
1421
1474
|
const onData = (chunk) => {
|
|
1422
1475
|
for (const ch of chunk) {
|
|
@@ -1425,7 +1478,7 @@ async function hiddenPrompt(label) {
|
|
|
1425
1478
|
stdin.pause();
|
|
1426
1479
|
stdin.off("data", onData);
|
|
1427
1480
|
process.stderr.write("\n");
|
|
1428
|
-
return
|
|
1481
|
+
return resolve2(value);
|
|
1429
1482
|
}
|
|
1430
1483
|
if (ch === "") {
|
|
1431
1484
|
stdin.setRawMode(false);
|
|
@@ -1549,6 +1602,7 @@ var HELPS = {
|
|
|
1549
1602
|
"ghosty agents doctor <agent-id> [--verbose]",
|
|
1550
1603
|
"ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
|
|
1551
1604
|
"ghosty agents restart <agent-id>",
|
|
1605
|
+
"ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
|
|
1552
1606
|
'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]',
|
|
1553
1607
|
"ghosty agents rm <agent-id> [--yes]"
|
|
1554
1608
|
],
|
|
@@ -1570,7 +1624,10 @@ var HELPS = {
|
|
|
1570
1624
|
["--starter TEXT", 'demo: highlighted first suggestion ("" removes it)'],
|
|
1571
1625
|
['--chips "a|b|c"', "demo: suggestions, separated by |"],
|
|
1572
1626
|
["--sponsor on|off", "demo: copies people keep run on YOUR engine key"],
|
|
1573
|
-
["--rotate", "demo: new code; the old link stops working"]
|
|
1627
|
+
["--rotate", "demo: new code; the old link stops working"],
|
|
1628
|
+
["--check PATH", "box: this file must exist in the machine (repeatable), e.g. a file the new image brings"],
|
|
1629
|
+
["--contains PATH=TEXT", "box: that file must contain TEXT (a change that adds no files)"],
|
|
1630
|
+
["--recycle", "box: replace its machines (not mid-turn); the new one starts on the next turn. ACP: use restart"]
|
|
1574
1631
|
],
|
|
1575
1632
|
examples: [
|
|
1576
1633
|
"ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
|
|
@@ -1578,7 +1635,8 @@ var HELPS = {
|
|
|
1578
1635
|
"ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
|
|
1579
1636
|
'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
|
|
1580
1637
|
"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"'
|
|
1638
|
+
'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
|
|
1639
|
+
"ghosty agents box <id> --check /opt/claude-worker/skills/pdf-doc/templates/calendario.html"
|
|
1582
1640
|
],
|
|
1583
1641
|
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
1642
|
learn: `${REF}#agentes`
|
|
@@ -1588,11 +1646,12 @@ var HELPS = {
|
|
|
1588
1646
|
usage: [
|
|
1589
1647
|
"ghosty skills ls <agent-id>",
|
|
1590
1648
|
"ghosty skills get <agent-id> <slug> [--out DIR]",
|
|
1649
|
+
"ghosty skills templates <agent-id>",
|
|
1591
1650
|
"ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
|
|
1592
1651
|
"ghosty skills rm <agent-id> <slug> [--yes]"
|
|
1593
1652
|
],
|
|
1594
1653
|
flags: [
|
|
1595
|
-
["--out DIR", "get: folder to write into
|
|
1654
|
+
["--out DIR", "get: folder to write into; without it, get only lists the files"],
|
|
1596
1655
|
["--file FILE", "add: upload one SKILL.md"],
|
|
1597
1656
|
["--dir FOLDER", "add: upload a whole skill folder (SKILL.md + assets); restores a backup"],
|
|
1598
1657
|
["--yes", "rm: skip the confirmation (required without a terminal)"]
|
|
@@ -1603,7 +1662,10 @@ var HELPS = {
|
|
|
1603
1662
|
"ghosty skills add <id> pdf-doc # from the community catalog",
|
|
1604
1663
|
"ghosty skills rm <id> cotizacion # keeps a local copy first"
|
|
1605
1664
|
],
|
|
1606
|
-
notes: [
|
|
1665
|
+
notes: [
|
|
1666
|
+
"rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it.",
|
|
1667
|
+
"templates reads a live machine (run one turn first). pdf-doc uses the FIRST folder with print.css: a full skill of your own shadows Ghosty's templates."
|
|
1668
|
+
],
|
|
1607
1669
|
learn: `${REF}#skills`
|
|
1608
1670
|
},
|
|
1609
1671
|
dbs: {
|
|
@@ -1636,15 +1698,16 @@ var HELPS = {
|
|
|
1636
1698
|
learn: `${REF}#bases-de-datos`
|
|
1637
1699
|
},
|
|
1638
1700
|
turns: {
|
|
1639
|
-
summary: "An agent's recent turns: what answered, what failed and why.",
|
|
1640
|
-
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]"],
|
|
1701
|
+
summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
|
|
1702
|
+
usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last]"],
|
|
1641
1703
|
flags: [
|
|
1642
1704
|
["--errors", "only failed turns (same as --status error)"],
|
|
1643
1705
|
["--status S", "running | done | stopped | error"],
|
|
1644
1706
|
["--since T", "30m, 1h, 2d or an ISO date"],
|
|
1645
1707
|
["--limit N", "max rows (default 20, max 500)"]
|
|
1646
1708
|
],
|
|
1647
|
-
examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
|
|
1709
|
+
examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100", "ghosty turns show <id> # last turn: each tool, its seconds, output tokens"],
|
|
1710
|
+
notes: ["show reads the worker's transcript: pool engines only (not ACP/goose)."],
|
|
1648
1711
|
learn: `${REF}#turnos`
|
|
1649
1712
|
},
|
|
1650
1713
|
files: {
|
|
@@ -1770,6 +1833,43 @@ function helpText(cmd) {
|
|
|
1770
1833
|
].join("\n");
|
|
1771
1834
|
}
|
|
1772
1835
|
|
|
1836
|
+
// src/update.ts
|
|
1837
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
1838
|
+
import { homedir as homedir3 } from "node:os";
|
|
1839
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1840
|
+
var FILE = join4(process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config"), "ghosty", "update.json");
|
|
1841
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
1842
|
+
var UPGRADE = "npm i -g @ghostystudio/cli@latest";
|
|
1843
|
+
function newer(a, b) {
|
|
1844
|
+
const pa = a.split(".").map(Number);
|
|
1845
|
+
const pb = b.split(".").map(Number);
|
|
1846
|
+
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
async function latestVersion() {
|
|
1850
|
+
if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
|
|
1851
|
+
let cache = {};
|
|
1852
|
+
try {
|
|
1853
|
+
cache = JSON.parse(await readFile6(FILE, "utf8"));
|
|
1854
|
+
} catch {
|
|
1855
|
+
}
|
|
1856
|
+
if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
|
|
1857
|
+
try {
|
|
1858
|
+
const r = await fetch("https://registry.npmjs.org/@ghostystudio/cli/latest", { signal: AbortSignal.timeout(1500) });
|
|
1859
|
+
const latest = (await r.json()).version;
|
|
1860
|
+
if (latest) cache = { latest, checkedAt: Date.now() };
|
|
1861
|
+
await mkdir4(dirname3(FILE), { recursive: true });
|
|
1862
|
+
await writeFile4(FILE, JSON.stringify(cache));
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return cache.latest ?? null;
|
|
1867
|
+
}
|
|
1868
|
+
async function updateNotice(current) {
|
|
1869
|
+
const latest = await latestVersion();
|
|
1870
|
+
return latest && newer(latest, current) ? `Update available: ${current} \u2192 ${latest}. Run: ${UPGRADE}` : null;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1773
1873
|
// src/index.ts
|
|
1774
1874
|
var HELP = `${bold("Usage:")} ghosty <command> [options]
|
|
1775
1875
|
|
|
@@ -1791,12 +1891,14 @@ ${bold("Agents")}
|
|
|
1791
1891
|
agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
|
|
1792
1892
|
agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
|
|
1793
1893
|
agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
|
|
1894
|
+
agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
|
|
1794
1895
|
agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
|
|
1795
1896
|
[--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]
|
|
1796
1897
|
|
|
1797
1898
|
${bold("Configure an agent")}
|
|
1798
1899
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
1799
|
-
skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; rm keeps a copy)
|
|
1900
|
+
skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; get lists, --out DIR saves; rm keeps a copy)
|
|
1901
|
+
skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
|
|
1800
1902
|
dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
|
|
1801
1903
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
1802
1904
|
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
@@ -1809,6 +1911,7 @@ ${bold("Talk to an agent")}
|
|
|
1809
1911
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
1810
1912
|
conversations ls <id> List conversations
|
|
1811
1913
|
turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
|
|
1914
|
+
turns show <id> [turn] Where a turn's time went: each tool with its seconds, output tokens (default: last)
|
|
1812
1915
|
|
|
1813
1916
|
${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
|
|
1814
1917
|
schedule ls <id> Pending turns and recent runs
|
|
@@ -1835,7 +1938,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
|
|
|
1835
1938
|
${dim("Note: `ghosty` may collide with another binary on your PATH (e.g. the Ghosty TUI). Check with `which -a ghosty`; run `npx @ghostystudio/cli` to be sure.")}
|
|
1836
1939
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
1837
1940
|
var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
|
|
1838
|
-
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo"];
|
|
1941
|
+
var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo", "box"];
|
|
1839
1942
|
function unknown(what, input, known, helpCmd) {
|
|
1840
1943
|
const s = suggest(input, known);
|
|
1841
1944
|
return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
|
|
@@ -1922,14 +2025,17 @@ async function main() {
|
|
|
1922
2025
|
tables: { type: "string" },
|
|
1923
2026
|
append: { type: "boolean" },
|
|
1924
2027
|
column: { type: "string" },
|
|
1925
|
-
before: { type: "string" }
|
|
2028
|
+
before: { type: "string" },
|
|
2029
|
+
recycle: { type: "boolean" },
|
|
2030
|
+
check: { type: "string", multiple: true },
|
|
2031
|
+
contains: { type: "string", multiple: true }
|
|
1926
2032
|
}
|
|
1927
2033
|
});
|
|
1928
2034
|
globals.json = Boolean(v.json);
|
|
1929
2035
|
const tokens = splitTokens(pos, v);
|
|
1930
2036
|
globals.token = tokens.session;
|
|
1931
2037
|
globals.profile = v.profile;
|
|
1932
|
-
if (v.version) return out({ version: "0.
|
|
2038
|
+
if (v.version) return out({ version: "0.10.0" }, () => "0.10.0");
|
|
1933
2039
|
const [cmd, sub, a1, a2] = pos;
|
|
1934
2040
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
1935
2041
|
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
@@ -1965,6 +2071,7 @@ async function main() {
|
|
|
1965
2071
|
if (sub === "docs" || sub === "documents") return agentDocsCmd(a1, a2, pos[4], v);
|
|
1966
2072
|
if (sub === "restart") return agentsRestart(a1);
|
|
1967
2073
|
if (sub === "demo") return agentsDemo(a1, v);
|
|
2074
|
+
if (sub === "box") return agentsBox(a1, v);
|
|
1968
2075
|
throw unknown(`agents ${sub}`, sub, AGENTS_SUBS, "ghosty agents --help");
|
|
1969
2076
|
case "files":
|
|
1970
2077
|
return filesCmd(sub, a1, a2, v);
|
|
@@ -1976,7 +2083,7 @@ async function main() {
|
|
|
1976
2083
|
case "db":
|
|
1977
2084
|
return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
|
|
1978
2085
|
case "turns":
|
|
1979
|
-
return turnsCmd(sub, a1, v);
|
|
2086
|
+
return turnsCmd(sub, a1, a2, v);
|
|
1980
2087
|
case "board":
|
|
1981
2088
|
return boardCmd(sub, a1, v);
|
|
1982
2089
|
case "channels":
|
|
@@ -2011,11 +2118,18 @@ async function main() {
|
|
|
2011
2118
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
2012
2119
|
}
|
|
2013
2120
|
}
|
|
2121
|
+
var notice = updateNotice("0.10.0").catch(() => null);
|
|
2014
2122
|
main().then(
|
|
2015
|
-
() =>
|
|
2016
|
-
|
|
2123
|
+
async () => {
|
|
2124
|
+
const n = await notice;
|
|
2125
|
+
if (n) info(dim(n));
|
|
2126
|
+
process.exit(EXIT.OK);
|
|
2127
|
+
},
|
|
2128
|
+
async (e) => {
|
|
2017
2129
|
if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
|
|
2018
|
-
|
|
2130
|
+
const n = await notice;
|
|
2131
|
+
fail(usage(e.message, n ? `${n}
|
|
2132
|
+
This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
|
|
2019
2133
|
}
|
|
2020
2134
|
fail(e);
|
|
2021
2135
|
}
|
package/package.json
CHANGED