@ghostystudio/cli 0.10.0 → 0.10.2

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 +1 -1
  2. package/dist/index.js +52 -20
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -31,7 +31,7 @@ ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage o
31
31
  ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
32
32
  ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR] # get without --out only lists
33
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]
34
+ ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--external-rows t:col+col] [--from easybits:ID --tables a,b --append --dry-run]
35
35
  ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
36
36
  ghosty turns show <id> [turn-id|last] # where a turn's time went: each tool with its seconds, output tokens
37
37
  ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
package/dist/index.js CHANGED
@@ -282,7 +282,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
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.10.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
285
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.10.2"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
286
286
  }, (res) => {
287
287
  let buf = "";
288
288
  res.on("data", (c) => buf += c);
@@ -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.10.0"}`,
327
+ "user-agent": `ghosty-cli/${"0.10.2"}`,
328
328
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
329
329
  ...opts.headers
330
330
  },
@@ -712,9 +712,9 @@ ${dim(`Download: ghosty skills get ${id} ${slug} --out DIR`)}`);
712
712
  }
713
713
  case "templates": {
714
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)"}`)}`);
715
+ const rows = r.templates.map((t) => ({ name: t.name, source: t.source, description: (t.description ?? "").replace(/\s+/g, " ").slice(0, 60) }));
716
+ out(r, () => `${table(rows, ["name", "source", "description"])}
717
+ ${dim("Its own: put <name>.html in <skill>/pdf-templates/ of any of its skills (it wins over a house one with the same name).")}`);
718
718
  return;
719
719
  }
720
720
  case "rm":
@@ -782,7 +782,7 @@ async function agentsDemo(id, v) {
782
782
  );
783
783
  }
784
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]");
785
+ id = need(id, "agent id", "ghosty agents box <agent-id> [--check PATH] [--contains PATH=TEXT] [--recycle]");
786
786
  if (v.recycle) {
787
787
  const r2 = await api(agentPath(id, "/box"), { method: "POST", body: { recycle: true } });
788
788
  out(r2, () => `${lavender("\u2713")} Recycled. ${dim(r2?.note ?? "")}
@@ -1125,7 +1125,7 @@ async function call(path, v, init = {}) {
1125
1125
  "x-ghosty-ts": ts,
1126
1126
  "x-ghosty-sig": sig,
1127
1127
  "content-type": "application/json",
1128
- "user-agent": `ghosty-cli/${"0.10.0"}`
1128
+ "user-agent": `ghosty-cli/${"0.10.2"}`
1129
1129
  },
1130
1130
  body: method === "GET" ? void 0 : raw
1131
1131
  });
@@ -1290,6 +1290,24 @@ async function partnerCmd(sub, a1, a2, v) {
1290
1290
  }
1291
1291
  }
1292
1292
 
1293
+ // src/rows.ts
1294
+ var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
1295
+ function parseRowScope(raw) {
1296
+ const s = raw.trim();
1297
+ if (!s) return null;
1298
+ const out2 = {};
1299
+ for (const part of s.split(",").map((x) => x.trim()).filter(Boolean)) {
1300
+ const [table2, cols = ""] = part.split(":").map((x) => x.trim());
1301
+ const list = cols.split("+").map((x) => x.trim()).filter(Boolean);
1302
+ if (!IDENT.test(table2 ?? "") || !list.length || !list.every((c) => IDENT.test(c))) {
1303
+ throw new Error(`\xAB${part}\xBB no es tabla:columna(+columna); ej. clientes:telefono+tel_secundario`);
1304
+ }
1305
+ out2[table2] = list;
1306
+ }
1307
+ return out2;
1308
+ }
1309
+ var describeRowScope = (scope) => Object.entries(scope ?? {}).map(([t, c]) => `${t} (${c.join(", ")})`).join("; ");
1310
+
1293
1311
  // src/commands/data.ts
1294
1312
  var enc6 = encodeURIComponent;
1295
1313
  var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc6(id)}${rest}`;
@@ -1327,16 +1345,17 @@ async function dbsCmd(sub, id, db, rest, v) {
1327
1345
  case "ls":
1328
1346
  case "list": {
1329
1347
  const r = await api(agentPath2(id, "/dbs"));
1330
- const rows = r.dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", id: d.id }));
1331
- return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "id"]) : dim("This account has no databases yet: create one at /app/dbs."));
1348
+ const rows = r.dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", "own rows": d.granted ? Object.keys(d.externalRowScope ?? {}).join(", ") : "", id: d.id }));
1349
+ return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "own rows", "id"]) : dim("This account has no databases yet: create one at /app/dbs."));
1332
1350
  }
1333
1351
  case "tables": {
1334
1352
  db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
1335
1353
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
1336
1354
  const ext = new Set(r.externalWriteTables);
1337
- const rows = r.tables.map((t) => ({ table: t.name, rows: t.rows, "customer writes": ext.has(t.name) ? "yes" : "", columns: t.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
1355
+ const own = r.externalRowScope ?? {};
1356
+ const rows = r.tables.map((t) => ({ table: t.name, rows: t.rows, "customer writes": ext.has(t.name) ? "yes" : "", "customer sees": own[t.name] ? `own rows (${own[t.name].join(", ")})` : "", columns: t.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
1338
1357
  return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
1339
- ${table(rows, ["table", "rows", "customer writes", "columns"])}`);
1358
+ ${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}`);
1340
1359
  }
1341
1360
  case "query":
1342
1361
  case "sql": {
@@ -1354,10 +1373,20 @@ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
1354
1373
  });
1355
1374
  }
1356
1375
  case "grant": {
1357
- db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
1358
- const body = v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {};
1376
+ db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col]");
1377
+ let rowScope;
1378
+ try {
1379
+ rowScope = v["external-rows"] !== void 0 ? parseRowScope(v["external-rows"]) : void 0;
1380
+ } catch (e) {
1381
+ throw usage(e.message, `ghosty dbs grant ${id} ${db} --external-rows clientes:telefono+tel_secundario`);
1382
+ }
1383
+ const body = {
1384
+ ...v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {},
1385
+ ...rowScope !== void 0 ? { externalRowScope: rowScope } : {}
1386
+ };
1359
1387
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "PUT", body });
1360
- return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
1388
+ const own = describeRowScope(r.externalRowScope);
1389
+ return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.${own ? ` Each customer sees only their own rows of: ${own}.` : ""}`)}`);
1361
1390
  }
1362
1391
  case "import": {
1363
1392
  db = need2(db, "database (id or name)", "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
@@ -1636,7 +1665,7 @@ var HELPS = {
1636
1665
  'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1637
1666
  "ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
1638
1667
  '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"
1668
+ "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
1640
1669
  ],
1641
1670
  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
1671
  learn: `${REF}#agentes`
@@ -1664,7 +1693,7 @@ var HELPS = {
1664
1693
  ],
1665
1694
  notes: [
1666
1695
  "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."
1696
+ "templates reads a live machine (run one turn first). An agent's own template is <skill>/pdf-templates/<name>.html in any of its skills; it wins over a house one with the same name."
1668
1697
  ],
1669
1698
  learn: `${REF}#skills`
1670
1699
  },
@@ -1675,13 +1704,14 @@ var HELPS = {
1675
1704
  "ghosty dbs tables <agent-id> <db>",
1676
1705
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
1677
1706
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--dry-run] [--yes]",
1678
- "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]",
1707
+ "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col,\u2026]",
1679
1708
  "ghosty dbs revoke <agent-id> <db>"
1680
1709
  ],
1681
1710
  flags: [
1682
1711
  ["--arg V", "query: value for a ? placeholder (repeatable)"],
1683
1712
  ["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
1684
1713
  ["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
1714
+ ["--external-rows t:col+col", 'grant: from WhatsApp/Messenger each customer sees only THEIR rows of t, matched by phone column(s) ("" = no filter)'],
1685
1715
  ["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
1686
1716
  ["--tables a,b", "import: only these tables (default: all)"],
1687
1717
  ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
@@ -1692,6 +1722,7 @@ var HELPS = {
1692
1722
  "ghosty dbs tables <id> totequim",
1693
1723
  'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
1694
1724
  "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
1725
+ "ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
1695
1726
  "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
1696
1727
  ],
1697
1728
  notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
@@ -1899,7 +1930,7 @@ ${bold("Configure an agent")}
1899
1930
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
1900
1931
  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
1932
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
1902
- dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
1933
+ dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run]
1903
1934
  mcp get|set <id> MCP servers [--file servers.json]
1904
1935
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
1905
1936
  board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
@@ -2008,6 +2039,7 @@ async function main() {
2008
2039
  arg: { type: "string", multiple: true },
2009
2040
  write: { type: "boolean" },
2010
2041
  "external-write": { type: "string" },
2042
+ "external-rows": { type: "string" },
2011
2043
  "no-deliver": { type: "boolean" },
2012
2044
  deliver: { type: "boolean" },
2013
2045
  errors: { type: "boolean" },
@@ -2035,7 +2067,7 @@ async function main() {
2035
2067
  const tokens = splitTokens(pos, v);
2036
2068
  globals.token = tokens.session;
2037
2069
  globals.profile = v.profile;
2038
- if (v.version) return out({ version: "0.10.0" }, () => "0.10.0");
2070
+ if (v.version) return out({ version: "0.10.2" }, () => "0.10.2");
2039
2071
  const [cmd, sub, a1, a2] = pos;
2040
2072
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
2041
2073
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
@@ -2118,7 +2150,7 @@ async function main() {
2118
2150
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
2119
2151
  }
2120
2152
  }
2121
- var notice = updateNotice("0.10.0").catch(() => null);
2153
+ var notice = updateNotice("0.10.2").catch(() => null);
2122
2154
  main().then(
2123
2155
  async () => {
2124
2156
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --define:__VERSION__=\\\"$npm_package_version\\\"",
18
18
  "prepack": "npm run build",
19
- "test": "for t in flags prompt api; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
19
+ "test": "for t in flags prompt api rows; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
20
20
  },
21
21
  "keywords": [
22
22
  "ghosty",