@ghostystudio/cli 0.5.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.
- package/dist/index.js +337 -14
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
82
|
-
await
|
|
83
|
-
await
|
|
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(
|
|
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.
|
|
293
|
+
"user-agent": `ghosty-cli/${"0.7.0"}`,
|
|
279
294
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
280
295
|
...opts.headers
|
|
281
296
|
},
|
|
@@ -364,7 +379,11 @@ async function agentsSet(id, v) {
|
|
|
364
379
|
if (v["prompt-file"]) body.prompt = await readFile2(v["prompt-file"], "utf8");
|
|
365
380
|
const env = parseEnv(v.env);
|
|
366
381
|
if (env) body.env = env;
|
|
367
|
-
|
|
382
|
+
const connectors = {};
|
|
383
|
+
for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
|
|
384
|
+
for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
|
|
385
|
+
if (Object.keys(connectors).length) body.connectors = connectors;
|
|
386
|
+
if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --connect or --disconnect.");
|
|
368
387
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
369
388
|
out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
|
|
370
389
|
}
|
|
@@ -435,9 +454,18 @@ async function skillsCmd(sub, id, slug, v) {
|
|
|
435
454
|
const ex = "ghosty skills ls <agent-id> | add <agent-id> <slug> [--file SKILL.md] | rm <agent-id> <slug>";
|
|
436
455
|
id = need(id, "agent id", ex);
|
|
437
456
|
switch (sub) {
|
|
438
|
-
case "ls":
|
|
439
|
-
|
|
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)`)}`);
|
|
440
467
|
return;
|
|
468
|
+
}
|
|
441
469
|
case "add": {
|
|
442
470
|
slug = need(slug, "skill slug", ex);
|
|
443
471
|
const body = v.file ? { markdown: await readFile2(v.file, "utf8") } : { fromCatalog: true };
|
|
@@ -466,6 +494,33 @@ async function mcpCmd(sub, id, v) {
|
|
|
466
494
|
}
|
|
467
495
|
throw usage(`Unknown mcp subcommand: ${sub ?? "(none)"}.`, ex);
|
|
468
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
|
+
}
|
|
469
524
|
|
|
470
525
|
// src/commands/chat.ts
|
|
471
526
|
import { randomUUID } from "node:crypto";
|
|
@@ -635,6 +690,20 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
|
|
|
635
690
|
throw usage(`Unknown files subcommand: ${sub ?? "(none)"}.`, ex);
|
|
636
691
|
}
|
|
637
692
|
}
|
|
693
|
+
async function meUsage() {
|
|
694
|
+
const r = await api("/api/v2/me/usage");
|
|
695
|
+
const pct = (p) => `${Math.round(p * 100)}%`;
|
|
696
|
+
out(
|
|
697
|
+
r,
|
|
698
|
+
() => [
|
|
699
|
+
`Plan ${r.plan.name}${r.exhausted ? " \u2014 out of usage for now" : ""}`,
|
|
700
|
+
` This week ${pct(r.week.pct).padStart(4)} used ${dim(`resets ${r.week.resetsAt.slice(0, 10)}`)}`,
|
|
701
|
+
...r.month.pct !== null ? [` This month ${pct(r.month.pct).padStart(4)} used ${dim(`resets ${r.month.resetsAt.slice(0, 10)}`)}`] : [],
|
|
702
|
+
` Top-ups ${r.creditMxn > 0 ? "available" : "none"}`,
|
|
703
|
+
dim("Change plan or top up: https://www.ghosty.studio/c/uso")
|
|
704
|
+
].join("\n")
|
|
705
|
+
);
|
|
706
|
+
}
|
|
638
707
|
|
|
639
708
|
// src/commands/schedule.ts
|
|
640
709
|
var enc3 = encodeURIComponent;
|
|
@@ -720,6 +789,225 @@ ${dim(`conversation ${p.sessionId}`)}`
|
|
|
720
789
|
throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
|
|
721
790
|
}
|
|
722
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
|
+
|
|
723
1011
|
// src/index.ts
|
|
724
1012
|
var HELP = `${bold("Usage:")} ghosty <command> [options]
|
|
725
1013
|
|
|
@@ -728,14 +1016,18 @@ ${bold("Session")}
|
|
|
728
1016
|
--device Sign in from another device with a link + code (auto over SSH/CI)
|
|
729
1017
|
logout Sign out and revoke this device's session
|
|
730
1018
|
whoami Show who you are signed in as
|
|
1019
|
+
usage Your plan and how much you've used this week and month
|
|
731
1020
|
|
|
732
1021
|
${bold("Agents")}
|
|
733
1022
|
agents ls List your agents
|
|
734
1023
|
agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
735
1024
|
agents get <id> Show an agent's config [--fields name,model,prompt]
|
|
736
1025
|
agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
|
|
1026
|
+
[--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
|
|
737
1027
|
agents rm <id> Delete an agent (owner only)
|
|
738
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]
|
|
739
1031
|
|
|
740
1032
|
${bold("Configure an agent")}
|
|
741
1033
|
files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
|
|
@@ -752,6 +1044,9 @@ ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies
|
|
|
752
1044
|
schedule add <id> "what to do" Schedule a turn --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]
|
|
753
1045
|
schedule rm <id> <schedule-id> Cancel a pending turn
|
|
754
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
|
+
|
|
755
1050
|
${bold("Your account")}
|
|
756
1051
|
me files ls|upload|rm Files in your account [--kind image|video|audio|document|other]
|
|
757
1052
|
docs [topic] Open the docs
|
|
@@ -759,10 +1054,12 @@ ${bold("Your account")}
|
|
|
759
1054
|
${bold("Global options")}
|
|
760
1055
|
--json Machine-readable output (stdout is JSON only)
|
|
761
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
|
|
762
1058
|
-h, --help Show help
|
|
763
1059
|
-v, --version Show version
|
|
764
1060
|
|
|
765
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.")}
|
|
766
1063
|
${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
|
|
767
1064
|
function showHelp() {
|
|
768
1065
|
if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
|
|
@@ -782,6 +1079,8 @@ async function main() {
|
|
|
782
1079
|
name: { type: "string" },
|
|
783
1080
|
model: { type: "string" },
|
|
784
1081
|
engine: { type: "string" },
|
|
1082
|
+
connect: { type: "string" },
|
|
1083
|
+
disconnect: { type: "string" },
|
|
785
1084
|
env: { type: "string" },
|
|
786
1085
|
prompt: { type: "string" },
|
|
787
1086
|
"prompt-file": { type: "string" },
|
|
@@ -796,13 +1095,31 @@ async function main() {
|
|
|
796
1095
|
in: { type: "string" },
|
|
797
1096
|
every: { type: "string" },
|
|
798
1097
|
until: { type: "string" },
|
|
799
|
-
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" }
|
|
800
1114
|
}
|
|
801
1115
|
});
|
|
802
1116
|
globals.json = Boolean(v.json);
|
|
803
|
-
|
|
804
|
-
|
|
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");
|
|
805
1121
|
const [cmd, sub, a1, a2] = pos;
|
|
1122
|
+
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
806
1123
|
if (!cmd || v.help || cmd === "help") return showHelp();
|
|
807
1124
|
switch (cmd) {
|
|
808
1125
|
case "login": {
|
|
@@ -830,7 +1147,8 @@ async function main() {
|
|
|
830
1147
|
if (sub === "set") return agentsSet(a1, v);
|
|
831
1148
|
if (sub === "rm" || sub === "delete") return agentsRm(a1);
|
|
832
1149
|
if (sub === "restart") return agentsRestart(a1);
|
|
833
|
-
|
|
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");
|
|
834
1152
|
case "files":
|
|
835
1153
|
return filesCmd(sub, a1, a2, v);
|
|
836
1154
|
case "skills":
|
|
@@ -846,8 +1164,13 @@ async function main() {
|
|
|
846
1164
|
throw usage(`Unknown conversations subcommand: ${sub ?? "(none)"}.`, "Try: ghosty conversations ls <agent-id>");
|
|
847
1165
|
case "schedule":
|
|
848
1166
|
return scheduleCmd(sub, a1, a2, v);
|
|
1167
|
+
case "partner":
|
|
1168
|
+
return partnerCmd(sub, a1, a2, { ...v, token: tokens.turn });
|
|
1169
|
+
case "usage":
|
|
1170
|
+
return meUsage();
|
|
849
1171
|
case "me":
|
|
850
1172
|
if (sub === "files") return meFiles(a1, a2, v);
|
|
1173
|
+
if (sub === "usage") return meUsage();
|
|
851
1174
|
throw usage(`Unknown me subcommand: ${sub ?? "(none)"}.`, "Try: ghosty me files ls");
|
|
852
1175
|
case "docs": {
|
|
853
1176
|
const url = `${await baseUrl()}/docs${sub ? `/${encodeURIComponent(sub)}` : "/cli"}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostystudio/cli",
|
|
3
|
-
"version": "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",
|