@ghostystudio/cli 0.6.0 → 0.7.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 (2) hide show
  1. package/dist/index.js +311 -13
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/flags.ts
4
+ function splitTokens(pos, v) {
5
+ const [cmd, sub] = pos;
6
+ const isTry = cmd === "partner" && sub === "try";
7
+ if (isTry) return { session: void 0, turn: v["turn-token"] ?? v.token };
8
+ return { session: v.token, turn: v["turn-token"] };
9
+ }
10
+
3
11
  // src/index.ts
4
12
  import { parseArgs } from "node:util";
5
13
 
@@ -69,20 +77,27 @@ async function openUrl(url) {
69
77
  var CLIENT_ID = "ghosty-cli";
70
78
  var SCOPES = "profile agents:read agents:write files";
71
79
  var LOGIN_TIMEOUT_MS = 5 * 6e4;
72
- var CONFIG_PATH = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "ghosty", "config.json");
80
+ function configPath() {
81
+ const profile = globals.profile || process.env.GHOSTY_PROFILE || "";
82
+ if (profile && !/^[A-Za-z0-9_-]{1,40}$/.test(profile)) {
83
+ throw new CliError(`Invalid profile: ${profile}.`, EXIT.USAGE, "Use letters, digits, - or _");
84
+ }
85
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "ghosty", profile ? `config.${profile}.json` : "config.json");
86
+ }
73
87
  async function readConfig() {
74
88
  try {
75
- return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
89
+ return JSON.parse(await readFile(configPath(), "utf8"));
76
90
  } catch {
77
91
  return {};
78
92
  }
79
93
  }
80
94
  async function writeConfig(c) {
81
- await mkdir(dirname(CONFIG_PATH), { recursive: true });
82
- await writeFile(CONFIG_PATH, JSON.stringify(c, null, 2), { mode: 384 });
83
- await chmod(CONFIG_PATH, 384);
95
+ const path = configPath();
96
+ await mkdir(dirname(path), { recursive: true });
97
+ await writeFile(path, JSON.stringify(c, null, 2), { mode: 384 });
98
+ await chmod(path, 384);
84
99
  }
85
- var clearConfig = () => rm(CONFIG_PATH, { force: true });
100
+ var clearConfig = () => rm(configPath(), { force: true });
86
101
  async function baseUrl() {
87
102
  const url = process.env.GHOSTY_URL || (await readConfig()).baseUrl || "https://www.ghosty.studio";
88
103
  return url.replace(/\/+$/, "");
@@ -275,7 +290,7 @@ async function request(path, opts = {}, retried = false) {
275
290
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
276
291
  headers: {
277
292
  authorization: `Bearer ${token}`,
278
- "user-agent": `ghosty-cli/${"0.6.0"}`,
293
+ "user-agent": `ghosty-cli/${"0.7.0"}`,
279
294
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
280
295
  ...opts.headers
281
296
  },
@@ -439,9 +454,18 @@ async function skillsCmd(sub, id, slug, v) {
439
454
  const ex = "ghosty skills ls <agent-id> | add <agent-id> <slug> [--file SKILL.md] | rm <agent-id> <slug>";
440
455
  id = need(id, "agent id", ex);
441
456
  switch (sub) {
442
- case "ls":
443
- out(await api(agentPath(id, "/skills")));
457
+ case "ls": {
458
+ const r = await api(agentPath(id, "/skills"));
459
+ const short2 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
460
+ const rows = [
461
+ ...r.skills.map((s) => ({ slug: s.slug, source: "own", on: "on", description: short2(s.description) })),
462
+ ...(r.house ?? []).map((s) => ({ slug: s.slug, source: "ghosty", on: s.enabled ? "on" : "off", description: short2(s.description) }))
463
+ ];
464
+ const more = r.community.filter((c) => !c.installed).length;
465
+ out(r, () => `${table(rows, ["slug", "source", "on", "description"])}
466
+ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them)`)}`);
444
467
  return;
468
+ }
445
469
  case "add": {
446
470
  slug = need(slug, "skill slug", ex);
447
471
  const body = v.file ? { markdown: await readFile2(v.file, "utf8") } : { fromCatalog: true };
@@ -470,6 +494,33 @@ async function mcpCmd(sub, id, v) {
470
494
  }
471
495
  throw usage(`Unknown mcp subcommand: ${sub ?? "(none)"}.`, ex);
472
496
  }
497
+ async function agentsDemo(id, v) {
498
+ id = need(id, "agent id", "ghosty agents demo <agent-id> --slug guardias --on");
499
+ if (v.on && v.off) throw usage("Use --on or --off, not both.");
500
+ const body = {};
501
+ if (v.slug) body.slug = v.slug;
502
+ if (v.on) body.enabled = true;
503
+ if (v.off) body.enabled = false;
504
+ if (v.turns) body.maxTurns = Number(v.turns);
505
+ if (v["per-ip"]) body.perIpDay = Number(v["per-ip"]);
506
+ if (v.welcome !== void 0) body.welcome = v.welcome;
507
+ if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
508
+ if (v.sponsor !== void 0) {
509
+ if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
510
+ body.sponsor = v.sponsor === "on";
511
+ }
512
+ const d = Object.keys(body).length ? await api(agentPath(id, "/demo"), { method: "PUT", body }) : await api(agentPath(id, "/demo"));
513
+ out(
514
+ d,
515
+ () => [
516
+ `${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
517
+ dim(`limits: ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
518
+ d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
519
+ d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
520
+ `tried ${d.stats.pruebas} \xB7 messages ${d.stats.mensajes} \xB7 kept ${d.stats.clones} (${d.stats.patrocinados} on your key)`
521
+ ].join("\n")
522
+ );
523
+ }
473
524
 
474
525
  // src/commands/chat.ts
475
526
  import { randomUUID } from "node:crypto";
@@ -738,6 +789,225 @@ ${dim(`conversation ${p.sessionId}`)}`
738
789
  throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
739
790
  }
740
791
 
792
+ // src/commands/partner.ts
793
+ import { createHmac } from "node:crypto";
794
+ import { readFile as readFile4 } from "node:fs/promises";
795
+ var PARTNER_HELP = `${bold("Usage:")} ghosty partner <command> [options]
796
+
797
+ ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
798
+ partner ls Your partner spaces
799
+ partner enable <workspace> Turn one of your spaces into a partner space
800
+ partner credentials ls Server credentials (no secrets)
801
+ partner credentials create [--label L] Issue one \u2014 the secret is shown ONCE
802
+ partner credentials rotate <keyId> [--grace MIN] New secret, same keyId (old one valid MIN, default 60)
803
+ partner credentials revoke <keyId> Revoke it (idempotent)
804
+ partner audit Who did what, when
805
+
806
+ ${bold("Shared agent (solo-MCP)")}
807
+ partner agent get The agent template and its version
808
+ partner agent apply --file agent.json Create or update it (idempotent)
809
+
810
+ ${bold("Tenants")} ${dim("(your customers, by externalId \u2014 config only, no box)")}
811
+ partner tenants ls
812
+ partner tenants get <externalId>
813
+ partner tenants set <externalId> --config '{"name":"\u2026","timezone":"America/Mexico_City"}'
814
+ partner tenants rm <externalId> Deletes its config AND its memory
815
+
816
+ ${bold("Try it")}
817
+ partner try <externalId> "message" One turn [--group END_USER_ID] [--turn-token SHORT_LIVED_TOKEN]
818
+ (the turn token goes to YOUR MCP as \${turn.token}; --token also works here)
819
+
820
+ ${bold("Options")}
821
+ --workspace SLUG Which partner space (only if you own more than one)
822
+ --json Machine-readable output
823
+
824
+ ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
825
+ ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
826
+ var enc4 = encodeURIComponent;
827
+ function partnerCreds() {
828
+ const key = process.env.GHOSTY_PARTNER_KEY;
829
+ const secret = process.env.GHOSTY_PARTNER_SECRET;
830
+ return key && secret ? { key, secret } : null;
831
+ }
832
+ async function call(path, v, init = {}) {
833
+ const method = init.method ?? (init.body !== void 0 ? "POST" : "GET");
834
+ const raw = init.body !== void 0 ? JSON.stringify(init.body) : "";
835
+ const creds = init.ownerOnly ? null : partnerCreds();
836
+ if (creds) {
837
+ const ts = String(Math.floor(Date.now() / 1e3));
838
+ const sig = createHmac("sha256", creds.secret).update(`${ts}.${creds.key}.${raw}`).digest("hex");
839
+ const res = await fetch(`${await baseUrl()}${path}`, {
840
+ method,
841
+ headers: {
842
+ "x-ghosty-key": creds.key,
843
+ "x-ghosty-ts": ts,
844
+ "x-ghosty-sig": sig,
845
+ "content-type": "application/json",
846
+ "user-agent": `ghosty-cli/${"0.7.0"}`
847
+ },
848
+ body: method === "GET" ? void 0 : raw
849
+ });
850
+ if (!res.ok) throw await toError(res);
851
+ return res;
852
+ }
853
+ return request(path, {
854
+ method,
855
+ ...init.body !== void 0 ? { body: init.body } : {},
856
+ headers: v.workspace ? { "x-ghosty-workspace": v.workspace } : {}
857
+ });
858
+ }
859
+ async function json(path, v, init = {}) {
860
+ const res = await call(path, v, init);
861
+ const t = await res.text();
862
+ return t ? JSON.parse(t) : void 0;
863
+ }
864
+ var fmt = (d) => d ? d.replace("T", " ").slice(0, 16) : "\u2014";
865
+ function showSecret(r) {
866
+ out(
867
+ r,
868
+ () => [
869
+ `${lavender("\u2713")} Save these now \u2014 the secret is not shown again:`,
870
+ ` GHOSTY_PARTNER_KEY=${r.keyId}`,
871
+ ` GHOSTY_PARTNER_SECRET=${r.secret}`
872
+ ].join("\n")
873
+ );
874
+ }
875
+ async function credentialsCmd(sub, id, v) {
876
+ const owner = { ownerOnly: true };
877
+ if (!sub || sub === "ls" || sub === "list") {
878
+ const r = await json("/api/v2/partner/credentials", v, owner);
879
+ return out(
880
+ r,
881
+ () => table(
882
+ r.credentials.map((c) => ({ keyId: c.keyId, label: c.label, status: c.status, lastUsed: fmt(c.lastUsedAt), created: fmt(c.createdAt) })),
883
+ ["keyId", "label", "status", "lastUsed", "created"]
884
+ )
885
+ );
886
+ }
887
+ if (sub === "create" || sub === "new") {
888
+ return showSecret(await json("/api/v2/partner/credentials", v, { ...owner, method: "POST", body: { label: v.label } }));
889
+ }
890
+ if (!id) throw usage("Missing keyId.", `Example: ghosty partner credentials ${sub} gpk_\u2026`);
891
+ if (sub === "rotate") {
892
+ const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
893
+ if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage("--grace must be minutes \u2265 0.");
894
+ return showSecret(await json(`/api/v2/partner/credentials/${enc4(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
895
+ }
896
+ if (sub === "revoke" || sub === "rm") {
897
+ const r = await json(`/api/v2/partner/credentials/${enc4(id)}`, v, { ...owner, method: "DELETE" });
898
+ return out(r, () => `${lavender("\u2713")} Revoked ${r.keyId}.`);
899
+ }
900
+ throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty partner credentials ls | create | rotate | revoke");
901
+ }
902
+ async function agentCmd(sub, v) {
903
+ if (!sub || sub === "get") {
904
+ const r = await json("/api/v2/partner/agent", v);
905
+ return out(r, () => {
906
+ const a = r.agent;
907
+ return [
908
+ `${bold(a.name)} ${dim(`v${a.version} \xB7 solo-MCP \xB7 ${a.engine}/${a.model}`)}`,
909
+ `MCP: ${a.mcp.map((m) => `${m.name} (${m.url})`).join(", ") || "none"}`,
910
+ dim(a.prompt.slice(0, 300))
911
+ ].join("\n");
912
+ });
913
+ }
914
+ if (sub === "apply" || sub === "set") {
915
+ if (!v.file) throw usage("Missing --file.", "Example: ghosty partner agent apply --file agent.json");
916
+ const body = JSON.parse(await readFile4(v.file, "utf8"));
917
+ const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
918
+ return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} \xB7 v${r.agent.version}`);
919
+ }
920
+ throw usage(`Unknown agent subcommand: ${sub}.`, "Try: ghosty partner agent get | apply --file agent.json");
921
+ }
922
+ async function tenantsCmd(sub, id, v) {
923
+ if (!sub || sub === "ls" || sub === "list") {
924
+ const r = await json("/api/v2/partner/tenants", v);
925
+ return out(r, () => table(r.tenants.map((t) => ({ externalId: t.externalId, config: JSON.stringify(t.config ?? {}), updated: fmt(t.updatedAt) })), ["externalId", "config", "updated"]));
926
+ }
927
+ if (!id) throw usage("Missing externalId.", `Example: ghosty partner tenants ${sub} org_123`);
928
+ const path = `/api/v2/partner/tenants/${enc4(id)}`;
929
+ if (sub === "get") return out(await json(path, v));
930
+ if (sub === "set" || sub === "apply") {
931
+ if (!v.config) throw usage("Missing --config.", `Example: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
932
+ let config;
933
+ try {
934
+ config = JSON.parse(v.config);
935
+ } catch {
936
+ throw usage("--config must be JSON.");
937
+ }
938
+ const r = await json(path, v, { method: "PUT", body: { config } });
939
+ return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} ${id}`);
940
+ }
941
+ if (sub === "rm" || sub === "delete") {
942
+ const r = await json(path, v, { method: "DELETE" });
943
+ return out(r, () => r.deleted ? `${lavender("\u2713")} Deleted ${id} (${r.conversations} conversations)` : dim(`${id} did not exist.`));
944
+ }
945
+ throw usage(`Unknown tenants subcommand: ${sub}.`, "Try: ghosty partner tenants ls | get | set | rm");
946
+ }
947
+ async function tryCmd(externalId, text, v) {
948
+ if (!externalId || !text) throw usage("Missing externalId or message.", 'Example: ghosty partner try org_123 "\xBFqu\xE9 citas tengo hoy?"');
949
+ const res = await call("/api/v2/partner/turns", v, {
950
+ method: "POST",
951
+ body: { tenant: { externalId }, groupId: v.group ?? "cli", text, ...v.token ? { token: v.token } : {} }
952
+ });
953
+ const dec = new TextDecoder();
954
+ let buf = "";
955
+ let full = "";
956
+ for await (const part of res.body) {
957
+ buf += dec.decode(part, { stream: true });
958
+ let i;
959
+ while ((i = buf.indexOf("\n\n")) >= 0) {
960
+ const block = buf.slice(0, i);
961
+ buf = buf.slice(i + 2);
962
+ const line = block.split("\n").find((l) => l.startsWith("data:"));
963
+ if (!line) continue;
964
+ const ev = JSON.parse(line.slice(5));
965
+ if (globals.json) {
966
+ process.stdout.write(JSON.stringify(ev) + "\n");
967
+ continue;
968
+ }
969
+ if (ev.type === "chunk" && ev.value) {
970
+ full += ev.value;
971
+ process.stdout.write(ev.value);
972
+ } else if (ev.type === "tool" && ev.phase === "start") info(dim(` \u21B3 ${ev.name}`));
973
+ else if (ev.type === "error") info(`Error: ${ev.message}`);
974
+ }
975
+ }
976
+ if (!globals.json && full) process.stdout.write("\n");
977
+ }
978
+ async function partnerCmd(sub, a1, a2, v) {
979
+ switch (sub) {
980
+ case void 0:
981
+ case "help":
982
+ return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
983
+ case "ls":
984
+ case "list": {
985
+ const r = await json("/api/v2/partner", v, { ownerOnly: true });
986
+ return out(r, () => table(r.partners.map((p) => ({ workspace: p.workspace, credentials: p.credentials, agent: p.agents ? "yes" : "no", status: p.disabledAt ? "off" : "on" })), ["workspace", "credentials", "agent", "status"]));
987
+ }
988
+ case "enable": {
989
+ if (!a1) throw usage("Missing workspace.", "Example: ghosty partner enable denik");
990
+ const r = await json("/api/v2/partner", v, { ownerOnly: true, method: "POST", body: { workspace: a1 } });
991
+ return out(r, () => `${lavender("\u2713")} ${r.partner.workspace} is a partner space.`);
992
+ }
993
+ case "credentials":
994
+ case "creds":
995
+ return credentialsCmd(a1, a2, v);
996
+ case "agent":
997
+ return agentCmd(a1, v);
998
+ case "tenants":
999
+ return tenantsCmd(a1, a2, v);
1000
+ case "audit": {
1001
+ const r = await json("/api/v2/partner/audit", v, { ownerOnly: true });
1002
+ return out(r, () => table(r.events.map((e) => ({ when: fmt(e.ts), action: e.action, externalId: e.externalId, actor: e.actor })), ["when", "action", "externalId", "actor"]));
1003
+ }
1004
+ case "try":
1005
+ return tryCmd(a1, a2, v);
1006
+ default:
1007
+ throw usage(`Unknown partner subcommand: ${sub}.`, "Run: ghosty partner --help");
1008
+ }
1009
+ }
1010
+
741
1011
  // src/index.ts
742
1012
  var HELP = `${bold("Usage:")} ghosty <command> [options]
743
1013
 
@@ -756,6 +1026,8 @@ ${bold("Agents")}
756
1026
  [--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
757
1027
  agents rm <id> Delete an agent (owner only)
758
1028
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
1029
+ agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
1030
+ [--slug NAME] [--on | --off] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--sponsor on|off]
759
1031
 
760
1032
  ${bold("Configure an agent")}
761
1033
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
@@ -772,6 +1044,9 @@ ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies
772
1044
  schedule add <id> "what to do" Schedule a turn --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]
773
1045
  schedule rm <id> <schedule-id> Cancel a pending turn
774
1046
 
1047
+ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see: ghosty partner --help)")}
1048
+ partner ls | enable | credentials | agent | tenants | audit | try
1049
+
775
1050
  ${bold("Your account")}
776
1051
  me files ls|upload|rm Files in your account [--kind image|video|audio|document|other]
777
1052
  docs [topic] Open the docs
@@ -779,10 +1054,12 @@ ${bold("Your account")}
779
1054
  ${bold("Global options")}
780
1055
  --json Machine-readable output (stdout is JSON only)
781
1056
  --token <token> Use this token (or env GHOSTY_TOKEN). A gat_\u2026 agent token reaches only that agent.
1057
+ --profile <name> Use a separate saved session (or env GHOSTY_PROFILE), e.g. one per account
782
1058
  -h, --help Show help
783
1059
  -v, --version Show version
784
1060
 
785
1061
  ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signed in
1062
+ ${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.")}
786
1063
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
787
1064
  function showHelp() {
788
1065
  if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
@@ -818,13 +1095,31 @@ async function main() {
818
1095
  in: { type: "string" },
819
1096
  every: { type: "string" },
820
1097
  until: { type: "string" },
821
- title: { type: "string" }
1098
+ title: { type: "string" },
1099
+ profile: { type: "string" },
1100
+ workspace: { type: "string" },
1101
+ label: { type: "string" },
1102
+ grace: { type: "string" },
1103
+ group: { type: "string" },
1104
+ config: { type: "string" },
1105
+ "turn-token": { type: "string" },
1106
+ slug: { type: "string" },
1107
+ on: { type: "boolean" },
1108
+ off: { type: "boolean" },
1109
+ turns: { type: "string" },
1110
+ "per-ip": { type: "string" },
1111
+ welcome: { type: "string" },
1112
+ chips: { type: "string" },
1113
+ sponsor: { type: "string" }
822
1114
  }
823
1115
  });
824
1116
  globals.json = Boolean(v.json);
825
- globals.token = v.token;
826
- if (v.version) return out({ version: "0.6.0" }, () => "0.6.0");
1117
+ const tokens = splitTokens(pos, v);
1118
+ globals.token = tokens.session;
1119
+ globals.profile = v.profile;
1120
+ if (v.version) return out({ version: "0.7.0" }, () => "0.7.0");
827
1121
  const [cmd, sub, a1, a2] = pos;
1122
+ if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
828
1123
  if (!cmd || v.help || cmd === "help") return showHelp();
829
1124
  switch (cmd) {
830
1125
  case "login": {
@@ -852,7 +1147,8 @@ async function main() {
852
1147
  if (sub === "set") return agentsSet(a1, v);
853
1148
  if (sub === "rm" || sub === "delete") return agentsRm(a1);
854
1149
  if (sub === "restart") return agentsRestart(a1);
855
- throw usage(`Unknown agents subcommand: ${sub}.`, "Try: ghosty agents ls | create | get | set | rm | restart");
1150
+ if (sub === "demo") return agentsDemo(a1, v);
1151
+ throw usage(`Unknown agents subcommand: ${sub}.`, "Try: ghosty agents ls | create | get | set | rm | restart | demo");
856
1152
  case "files":
857
1153
  return filesCmd(sub, a1, a2, v);
858
1154
  case "skills":
@@ -868,6 +1164,8 @@ async function main() {
868
1164
  throw usage(`Unknown conversations subcommand: ${sub ?? "(none)"}.`, "Try: ghosty conversations ls <agent-id>");
869
1165
  case "schedule":
870
1166
  return scheduleCmd(sub, a1, a2, v);
1167
+ case "partner":
1168
+ return partnerCmd(sub, a1, a2, { ...v, token: tokens.turn });
871
1169
  case "usage":
872
1170
  return meUsage();
873
1171
  case "me":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.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": {
@@ -15,7 +15,8 @@
15
15
  },
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
- "prepack": "npm run build"
18
+ "prepack": "npm run build",
19
+ "test": "esbuild src/flags.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-test.mjs --log-level=error && node --test /tmp/ghosty-cli-test.mjs"
19
20
  },
20
21
  "keywords": [
21
22
  "ghosty",