@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.
Files changed (3) hide show
  1. package/README.md +12 -2
  2. package/dist/index.js +146 -32
  3. 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((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.1"}`, "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.1"}`,
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);
@@ -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.9.1"}`
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);
@@ -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 (default ./<slug>)"],
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: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
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.9.1" }, () => "0.9.1");
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
- () => process.exit(EXIT.OK),
2016
- (e) => {
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
- 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})`));
2019
2133
  }
2020
2134
  fail(e);
2021
2135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.9.1",
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": {