@ghostystudio/cli 0.9.0 → 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.
Files changed (3) hide show
  1. package/README.md +30 -6
  2. package/dist/index.js +179 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,24 +8,32 @@ 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 box <id> [--check PATH]… [--contains PATH=TEXT]… [--recycle] # is its machine on the new image?
25
+ ghosty agents demo <id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N]
26
+ [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate] # public /p/ link, no account needed
21
27
  ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
22
28
  ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
23
29
  ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
24
30
 
25
31
  ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
26
- 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 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
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]
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
29
37
  ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
30
38
  ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
31
39
  ghosty mcp get|set <id> [--file servers.json]
@@ -43,20 +51,36 @@ ghosty schedule rm <id> <schedule-id>
43
51
  ghosty me files ls|upload|rm [--kind image|video|audio|document|other]
44
52
  ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argument
45
53
  ghosty docs
54
+
55
+ ghosty partner ls|enable|credentials|agent|tenants|audit|try # one agent for each of YOUR customers: ghosty partner --help
46
56
  ```
47
57
 
48
- Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--help`, `--version`.
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.
59
+
60
+ Aliases: `creds` = `credentials`, `db` = `dbs`, `dbs sql` = `dbs query`.
49
61
 
50
62
  Every command has its own help with examples: `ghosty <command> --help` or `ghosty help <command>`.
51
63
 
52
64
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
53
65
 
66
+ ### Changes in 0.9.1
67
+
68
+ - `ghosty agents --help` describes every `agents demo` flag; `ghosty usage --help` has its own page.
69
+ - "Learn more" links point to the exact section of https://www.ghosty.studio/docs/cli/comandos; `partner --help` links to /docs/cli/partners.
70
+
54
71
  ### Changes in 0.9.0
55
72
 
56
73
  - `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
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.
58
75
  - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
59
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
+
60
84
  ### Changes in 0.8.0
61
85
 
62
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((resolve, reject) => {
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) resolve({ code: u.searchParams.get("code"), redirectUri: redirectUri2 });
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((resolve) => {
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.9.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
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
- resolve({ status: res.statusCode ?? 0, retryAfter: retryAfterSeconds(res.headers["retry-after"]), body: parsed });
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) => resolve({ status: 0, retryAfter: null, body: { error: String(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.9.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
- const dir = v.out ?? slug;
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);
@@ -744,7 +757,7 @@ async function agentsDemo(id, v) {
744
757
  if (v.on) body.enabled = true;
745
758
  if (v.off) body.enabled = false;
746
759
  if (v.turns) body.maxTurns = Number(v.turns);
747
- if (v.hours) body.trialHours = Number(v.hours);
760
+ if (v.vence !== void 0) body.venceEl = v.vence === "no" ? null : new Date(v.vence).toISOString();
748
761
  if (v["per-ip"]) body.perIpDay = Number(v["per-ip"]);
749
762
  if (v.welcome !== void 0) body.welcome = v.welcome;
750
763
  if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
@@ -759,7 +772,7 @@ async function agentsDemo(id, v) {
759
772
  d,
760
773
  () => [
761
774
  `${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
762
- dim(`trial: ${d.trialHours} h from the first message \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
775
+ 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`),
763
776
  d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
764
777
  d.starter ? `starter: \u2728 ${d.starter}` : dim("starter: (none)"),
765
778
  d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
@@ -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";
@@ -1069,7 +1104,7 @@ ${bold("Options")}
1069
1104
  --json Machine-readable output
1070
1105
 
1071
1106
  ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
1072
- ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
1107
+ ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`;
1073
1108
  var enc5 = encodeURIComponent;
1074
1109
  function partnerCreds() {
1075
1110
  const key = process.env.GHOSTY_PARTNER_KEY;
@@ -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.9.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 && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h");
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}`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
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((resolve, reject) => {
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 resolve(value);
1481
+ return resolve2(value);
1429
1482
  }
1430
1483
  if (ch === "") {
1431
1484
  stdin.setRawMode(false);
@@ -1537,6 +1590,7 @@ Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new mes
1537
1590
 
1538
1591
  // src/help.ts
1539
1592
  var DOCS = "https://www.ghosty.studio/docs/cli";
1593
+ var REF = `${DOCS}/comandos`;
1540
1594
  var HELPS = {
1541
1595
  agents: {
1542
1596
  summary: "Create, inspect, change, check and delete agents.",
@@ -1548,7 +1602,8 @@ var HELPS = {
1548
1602
  "ghosty agents doctor <agent-id> [--verbose]",
1549
1603
  "ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
1550
1604
  "ghosty agents restart <agent-id>",
1551
- "ghosty agents demo <agent-id> [--slug NAME] [--on | --off] \u2026",
1605
+ "ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
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]',
1552
1607
  "ghosty agents rm <agent-id> [--yes]"
1553
1608
  ],
1554
1609
  flags: [
@@ -1559,28 +1614,44 @@ var HELPS = {
1559
1614
  ["--verbose", "doctor: also list the checks that passed"],
1560
1615
  ["--write", "docs grant: the agent may replace the document (default: read only)"],
1561
1616
  ["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
1562
- ["--yes", "rm: skip the confirmation (required without a terminal)"]
1617
+ ["--yes", "rm: skip the confirmation (required without a terminal)"],
1618
+ ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
1619
+ ["--on | --off", "demo: turn the public link on or off"],
1620
+ ["--vence DATE|no", `demo: fixed expiry in your machine's time ("2026-10-15T20:00"); no = never`],
1621
+ ["--turns N", "demo: messages per visitor (default 150)"],
1622
+ ["--per-ip N", "demo: messages per IP per day (default 30)"],
1623
+ ["--welcome TEXT", "demo: chat greeting (empty = default)"],
1624
+ ["--starter TEXT", 'demo: highlighted first suggestion ("" removes it)'],
1625
+ ['--chips "a|b|c"', "demo: suggestions, separated by |"],
1626
+ ["--sponsor on|off", "demo: copies people keep run on YOUR engine key"],
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"]
1563
1631
  ],
1564
1632
  examples: [
1565
1633
  "ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
1566
1634
  "ghosty agents set <id> --prompt-file PROMPT.md",
1567
1635
  "ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
1568
1636
  'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1569
- "ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
1637
+ "ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
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"
1570
1640
  ],
1571
- notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
1572
- learn: `${DOCS}#agents`
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."],
1642
+ learn: `${REF}#agentes`
1573
1643
  },
1574
1644
  skills: {
1575
1645
  summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
1576
1646
  usage: [
1577
1647
  "ghosty skills ls <agent-id>",
1578
1648
  "ghosty skills get <agent-id> <slug> [--out DIR]",
1649
+ "ghosty skills templates <agent-id>",
1579
1650
  "ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
1580
1651
  "ghosty skills rm <agent-id> <slug> [--yes]"
1581
1652
  ],
1582
1653
  flags: [
1583
- ["--out DIR", "get: folder to write into (default ./<slug>)"],
1654
+ ["--out DIR", "get: folder to write into; without it, get only lists the files"],
1584
1655
  ["--file FILE", "add: upload one SKILL.md"],
1585
1656
  ["--dir FOLDER", "add: upload a whole skill folder (SKILL.md + assets); restores a backup"],
1586
1657
  ["--yes", "rm: skip the confirmation (required without a terminal)"]
@@ -1591,8 +1662,11 @@ var HELPS = {
1591
1662
  "ghosty skills add <id> pdf-doc # from the community catalog",
1592
1663
  "ghosty skills rm <id> cotizacion # keeps a local copy first"
1593
1664
  ],
1594
- notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
1595
- learn: `${DOCS}#skills`
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
+ ],
1669
+ learn: `${REF}#skills`
1596
1670
  },
1597
1671
  dbs: {
1598
1672
  summary: "Databases an agent can use, and what it may write from customer channels.",
@@ -1621,31 +1695,32 @@ var HELPS = {
1621
1695
  "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
1622
1696
  ],
1623
1697
  notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
1624
- learn: `${DOCS}#dbs`
1698
+ learn: `${REF}#bases-de-datos`
1625
1699
  },
1626
1700
  turns: {
1627
- summary: "An agent's recent turns: what answered, what failed and why.",
1628
- 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]"],
1629
1703
  flags: [
1630
1704
  ["--errors", "only failed turns (same as --status error)"],
1631
1705
  ["--status S", "running | done | stopped | error"],
1632
1706
  ["--since T", "30m, 1h, 2d or an ISO date"],
1633
1707
  ["--limit N", "max rows (default 20, max 500)"]
1634
1708
  ],
1635
- examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
1636
- learn: `${DOCS}#turns`
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)."],
1711
+ learn: `${REF}#turnos`
1637
1712
  },
1638
1713
  files: {
1639
1714
  summary: "Files in the agent's machine.",
1640
1715
  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]"],
1641
1716
  examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
1642
- learn: `${DOCS}#files`
1717
+ learn: `${REF}#archivos`
1643
1718
  },
1644
1719
  mcp: {
1645
1720
  summary: "MCP servers of an agent. set REPLACES the whole list.",
1646
1721
  usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
1647
1722
  examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
1648
- learn: `${DOCS}#mcp`
1723
+ learn: `${REF}#servidores-mcp`
1649
1724
  },
1650
1725
  try: {
1651
1726
  summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
@@ -1676,25 +1751,25 @@ var HELPS = {
1676
1751
  "Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
1677
1752
  "--whatsapp is now an alias of --waba."
1678
1753
  ],
1679
- learn: `${DOCS}#try`
1754
+ learn: `${REF}#turnos-de-prueba`
1680
1755
  },
1681
1756
  chat: {
1682
1757
  summary: "Stream a conversation; with no message it is interactive.",
1683
1758
  usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
1684
1759
  examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
1685
- learn: `${DOCS}#chat`
1760
+ learn: `${REF}#conversaciones`
1686
1761
  },
1687
1762
  schedule: {
1688
1763
  summary: "Turns that run later, on the agent's own clock.",
1689
1764
  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>"],
1690
1765
  examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
1691
- learn: `${DOCS}#schedule`
1766
+ learn: `${REF}#programar-turnos`
1692
1767
  },
1693
1768
  me: {
1694
1769
  summary: "Your account: files and usage.",
1695
1770
  usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
1696
1771
  examples: ["ghosty me files ls --kind document"],
1697
- learn: `${DOCS}#me`
1772
+ learn: `${REF}#tu-cuenta`
1698
1773
  }
1699
1774
  };
1700
1775
  HELPS.credentials = {
@@ -1710,7 +1785,7 @@ HELPS.credentials = {
1710
1785
  "op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
1711
1786
  ],
1712
1787
  notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
1713
- learn: `${DOCS}#credentials`
1788
+ learn: `${REF}#credenciales`
1714
1789
  };
1715
1790
  HELPS.creds = HELPS.credentials;
1716
1791
  HELPS.channels = {
@@ -1722,7 +1797,7 @@ HELPS.channels = {
1722
1797
  ],
1723
1798
  examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
1724
1799
  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."],
1725
- learn: `${DOCS}#channels`
1800
+ learn: `${REF}#canales-de-whatsapp`
1726
1801
  };
1727
1802
  HELPS.board = {
1728
1803
  summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
@@ -1736,10 +1811,10 @@ HELPS.board = {
1736
1811
  ],
1737
1812
  examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
1738
1813
  notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
1739
- learn: `${DOCS}#board`
1814
+ learn: `${REF}#tablero`
1740
1815
  };
1741
1816
  HELPS.conversations = HELPS.chat;
1742
- HELPS.usage = HELPS.me;
1817
+ 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` };
1743
1818
  function helpText(cmd) {
1744
1819
  const h = HELPS[cmd];
1745
1820
  if (!h) return void 0;
@@ -1758,6 +1833,43 @@ function helpText(cmd) {
1758
1833
  ].join("\n");
1759
1834
  }
1760
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
+
1761
1873
  // src/index.ts
1762
1874
  var HELP = `${bold("Usage:")} ghosty <command> [options]
1763
1875
 
@@ -1779,12 +1891,14 @@ ${bold("Agents")}
1779
1891
  agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
1780
1892
  agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
1781
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]
1782
1895
  agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
1783
- [--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
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]
1784
1897
 
1785
1898
  ${bold("Configure an agent")}
1786
1899
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
1787
- 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
1788
1902
  dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
1789
1903
  mcp get|set <id> MCP servers [--file servers.json]
1790
1904
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
@@ -1797,6 +1911,7 @@ ${bold("Talk to an agent")}
1797
1911
  chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
1798
1912
  conversations ls <id> List conversations
1799
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)
1800
1915
 
1801
1916
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
1802
1917
  schedule ls <id> Pending turns and recent runs
@@ -1823,7 +1938,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
1823
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.")}
1824
1939
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
1825
1940
  var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
1826
- 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"];
1827
1942
  function unknown(what, input, known, helpCmd) {
1828
1943
  const s = suggest(input, known);
1829
1944
  return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
@@ -1881,7 +1996,7 @@ async function main() {
1881
1996
  on: { type: "boolean" },
1882
1997
  off: { type: "boolean" },
1883
1998
  turns: { type: "string" },
1884
- hours: { type: "string" },
1999
+ vence: { type: "string" },
1885
2000
  "per-ip": { type: "string" },
1886
2001
  welcome: { type: "string" },
1887
2002
  chips: { type: "string" },
@@ -1910,14 +2025,17 @@ async function main() {
1910
2025
  tables: { type: "string" },
1911
2026
  append: { type: "boolean" },
1912
2027
  column: { type: "string" },
1913
- before: { type: "string" }
2028
+ before: { type: "string" },
2029
+ recycle: { type: "boolean" },
2030
+ check: { type: "string", multiple: true },
2031
+ contains: { type: "string", multiple: true }
1914
2032
  }
1915
2033
  });
1916
2034
  globals.json = Boolean(v.json);
1917
2035
  const tokens = splitTokens(pos, v);
1918
2036
  globals.token = tokens.session;
1919
2037
  globals.profile = v.profile;
1920
- if (v.version) return out({ version: "0.9.0" }, () => "0.9.0");
2038
+ if (v.version) return out({ version: "0.10.0" }, () => "0.10.0");
1921
2039
  const [cmd, sub, a1, a2] = pos;
1922
2040
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
1923
2041
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
@@ -1953,6 +2071,7 @@ async function main() {
1953
2071
  if (sub === "docs" || sub === "documents") return agentDocsCmd(a1, a2, pos[4], v);
1954
2072
  if (sub === "restart") return agentsRestart(a1);
1955
2073
  if (sub === "demo") return agentsDemo(a1, v);
2074
+ if (sub === "box") return agentsBox(a1, v);
1956
2075
  throw unknown(`agents ${sub}`, sub, AGENTS_SUBS, "ghosty agents --help");
1957
2076
  case "files":
1958
2077
  return filesCmd(sub, a1, a2, v);
@@ -1964,7 +2083,7 @@ async function main() {
1964
2083
  case "db":
1965
2084
  return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
1966
2085
  case "turns":
1967
- return turnsCmd(sub, a1, v);
2086
+ return turnsCmd(sub, a1, a2, v);
1968
2087
  case "board":
1969
2088
  return boardCmd(sub, a1, v);
1970
2089
  case "channels":
@@ -1999,11 +2118,18 @@ async function main() {
1999
2118
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
2000
2119
  }
2001
2120
  }
2121
+ var notice = updateNotice("0.10.0").catch(() => null);
2002
2122
  main().then(
2003
- () => process.exit(EXIT.OK),
2004
- (e) => {
2123
+ async () => {
2124
+ const n = await notice;
2125
+ if (n) info(dim(n));
2126
+ process.exit(EXIT.OK);
2127
+ },
2128
+ async (e) => {
2005
2129
  if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
2006
- fail(usage(e.message, "Run: ghosty --help"));
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})`));
2007
2133
  }
2008
2134
  fail(e);
2009
2135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.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": {