patchcord 0.5.106 → 0.5.108
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/bin/patchcord.mjs +151 -109
- package/package.json +1 -1
package/bin/patchcord.mjs
CHANGED
|
@@ -67,22 +67,52 @@ function detectFolder(dir) {
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
70
|
-
console.log(`patchcord — agent messaging for AI coding agents
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
patchcord
|
|
78
|
-
patchcord
|
|
79
|
-
patchcord
|
|
80
|
-
patchcord
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
patchcord
|
|
84
|
-
patchcord --
|
|
85
|
-
patchcord
|
|
70
|
+
console.log(`patchcord — cross-agent messaging + team orchestration for AI coding agents
|
|
71
|
+
|
|
72
|
+
Two layers: THIS AGENT (a per-project identity, by bearer token) and your ACCOUNT
|
|
73
|
+
(user-level; provisions agents, runs teams + schedules). Account/team/schedule
|
|
74
|
+
commands authenticate the CLI in your browser automatically on first use.
|
|
75
|
+
|
|
76
|
+
SETUP (per agent / project)
|
|
77
|
+
patchcord Set up this project's agent (browser)
|
|
78
|
+
patchcord --token <t> [--server <url>] Headless / self-hosted setup
|
|
79
|
+
patchcord --full Same + full statusline
|
|
80
|
+
patchcord --rename <name> [--agent-type <t>] Rename this agent
|
|
81
|
+
|
|
82
|
+
THIS AGENT — identity + messaging
|
|
83
|
+
patchcord whoami Who am I — this agent's identity + project
|
|
84
|
+
patchcord whoami --propose "<text>" Set your self-description (two-shot confirm)
|
|
85
|
+
patchcord agents [name] List agents in your namespace (or show one)
|
|
86
|
+
patchcord subscribe [interval] Start the realtime listener (Kimi: bg poll)
|
|
87
|
+
patchcord upload <file> [--mime <t>] [--as <name>] Share a file as an attachment
|
|
88
|
+
|
|
89
|
+
ACCOUNT — log in to the CLI (commands below log in on demand)
|
|
90
|
+
patchcord login Authenticate the CLI (your account)
|
|
91
|
+
|
|
92
|
+
TEAMLEAD — set up the team-lead agent (the shepherd) in this folder
|
|
93
|
+
patchcord teamlead [--namespace <ns>] [--tool <harness>]
|
|
94
|
+
Provision the teamlead here; launch it
|
|
95
|
+
to adopt this project or build a new team
|
|
96
|
+
|
|
97
|
+
TEAM — the teamlead's tools (run from the project root)
|
|
98
|
+
patchcord provision <agent> --tool <X> --role <Y> --namespace <ns> [--dir <sub/>]
|
|
99
|
+
Create a worker agent (identity + config)
|
|
100
|
+
patchcord provision revoke <agent> --namespace <ns>
|
|
101
|
+
patchcord team list [--namespace <ns>] Provisioned agents (server view, deduped)
|
|
102
|
+
patchcord team status Reconcile folder ↔ identity ↔ tmux ↔ token
|
|
103
|
+
patchcord team launch Launch each worker in mux
|
|
104
|
+
|
|
105
|
+
SCHEDULES — timed / recurring messages (user-level)
|
|
106
|
+
patchcord schedule create <name> --namespace <ns> --to <agent> --content "..." \\
|
|
107
|
+
(--at <ISO> | --cron "<expr>" | --every <sec>) \\
|
|
108
|
+
[--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]
|
|
109
|
+
patchcord schedule list [--namespace <ns>]
|
|
110
|
+
patchcord schedule cancel|test|pause|resume <id>
|
|
111
|
+
|
|
112
|
+
MISC
|
|
113
|
+
patchcord update Update to the latest version
|
|
114
|
+
patchcord --version Show installed version
|
|
115
|
+
patchcord --help Show this help
|
|
86
116
|
|
|
87
117
|
First install: npx patchcord@latest`);
|
|
88
118
|
process.exit(0);
|
|
@@ -793,20 +823,21 @@ if (cmd === "subscribe") {
|
|
|
793
823
|
// works the same as the space-form (--token foo). The internal flag parsing below
|
|
794
824
|
// supports both. Non-flag commands (channel, init, skill, help, plugin-path) have
|
|
795
825
|
// their own branches above and below.
|
|
796
|
-
// ──
|
|
797
|
-
//
|
|
798
|
-
// token
|
|
799
|
-
// (legacy: master.json) or $
|
|
800
|
-
|
|
826
|
+
// ── CLI account auth + teamlead/team/provisioning/schedule ─────────────────
|
|
827
|
+
// These commands act on the user's ACCOUNT, so they require CLI login. The
|
|
828
|
+
// account token (user-level, tied to NO agent) lives at ~/.patchcord/auth.json
|
|
829
|
+
// (legacy: main.json / master.json) or $PATCHCORD_TOKEN (legacy: *_MAIN/MASTER).
|
|
830
|
+
// `patchcord login` authenticates; everything else logs in on demand.
|
|
831
|
+
if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "team" || cmd === "schedule") {
|
|
801
832
|
const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
|
|
802
|
-
const
|
|
803
|
-
const
|
|
833
|
+
const AUTH_CONFIG = join(HOME, ".patchcord", "auth.json");
|
|
834
|
+
const LEGACY_CONFIGS = [join(HOME, ".patchcord", "main.json"), join(HOME, ".patchcord", "master.json")];
|
|
804
835
|
const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
|
|
805
836
|
|
|
806
|
-
const
|
|
807
|
-
const envTok = process.env.PATCHCORD_MAIN_TOKEN || process.env.PATCHCORD_MASTER_TOKEN;
|
|
837
|
+
const readAuth = () => {
|
|
838
|
+
const envTok = process.env.PATCHCORD_TOKEN || process.env.PATCHCORD_MAIN_TOKEN || process.env.PATCHCORD_MASTER_TOKEN;
|
|
808
839
|
if (envTok) return { token: envTok, baseUrl: DEFAULT_API };
|
|
809
|
-
for (const p of [
|
|
840
|
+
for (const p of [AUTH_CONFIG, ...LEGACY_CONFIGS]) {
|
|
810
841
|
try {
|
|
811
842
|
const m = JSON.parse(readFileSync(p, "utf-8"));
|
|
812
843
|
if (m && m.token) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
|
|
@@ -821,11 +852,44 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
|
|
|
821
852
|
if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
|
|
822
853
|
return def;
|
|
823
854
|
};
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
855
|
+
// Browser login → saves + returns the account token. Authenticates the CLI;
|
|
856
|
+
// not related to any agent.
|
|
857
|
+
const doLogin = async () => {
|
|
858
|
+
const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/main/session" -H "Content-Type: application/json" -d '{}'`);
|
|
859
|
+
let sessionId = "";
|
|
860
|
+
try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
|
|
861
|
+
if (!sessionId) { console.error("Could not start login session."); process.exit(1); }
|
|
862
|
+
const url = `https://patchcord.dev/master?session=${sessionId}`;
|
|
863
|
+
console.log(`\n Log in to patchcord in your browser:\n ${M.cyan}${url}${M.rst}\n`);
|
|
864
|
+
console.log(` ${M.dim}Waiting...${M.rst}`);
|
|
865
|
+
const http = await import("http");
|
|
866
|
+
const https = await import("https");
|
|
867
|
+
const token = await new Promise((resolve) => {
|
|
868
|
+
const lib = DEFAULT_API.startsWith("https") ? https : http;
|
|
869
|
+
const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
|
|
870
|
+
let buf = "";
|
|
871
|
+
res.on("data", (c) => {
|
|
872
|
+
buf += c.toString();
|
|
873
|
+
for (const line of buf.split("\n")) {
|
|
874
|
+
if (line.startsWith("data: ")) {
|
|
875
|
+
try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
res.on("end", () => resolve(null));
|
|
880
|
+
res.on("error", () => resolve(null));
|
|
881
|
+
});
|
|
882
|
+
req.on("error", () => resolve(null));
|
|
883
|
+
setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
|
|
884
|
+
});
|
|
885
|
+
if (!token) { console.error("\n Login timed out or failed."); process.exit(1); }
|
|
886
|
+
mkdirSync(dirname(AUTH_CONFIG), { recursive: true });
|
|
887
|
+
writeFileSync(AUTH_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
|
|
888
|
+
console.log(` ${M.green}✓${M.rst} Logged in ${M.dim}(${AUTH_CONFIG})${M.rst}\n`);
|
|
889
|
+
return { token, baseUrl: DEFAULT_API };
|
|
828
890
|
};
|
|
891
|
+
// Auth on demand: any command needing the account logs in if not already.
|
|
892
|
+
const requireAuth = async () => readAuth() || await doLogin();
|
|
829
893
|
const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
|
|
830
894
|
mkdirSync(dir, { recursive: true });
|
|
831
895
|
const hdr = { Authorization: `Bearer ${token}`, "X-Patchcord-Machine": hostname };
|
|
@@ -865,49 +929,15 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
|
|
|
865
929
|
});
|
|
866
930
|
};
|
|
867
931
|
|
|
868
|
-
if (cmd === "
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
let sessionId = "";
|
|
874
|
-
try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
|
|
875
|
-
if (!sessionId) { console.error("Could not start main-auth session."); process.exit(1); }
|
|
876
|
-
const url = `https://patchcord.dev/master?session=${sessionId}`;
|
|
877
|
-
console.log(`\n Authorize the main agent in your browser:\n ${M.cyan}${url}${M.rst}\n`);
|
|
878
|
-
console.log(` ${M.dim}Waiting for authorization...${M.rst}`);
|
|
879
|
-
const http = await import("http");
|
|
880
|
-
const https = await import("https");
|
|
881
|
-
const token = await new Promise((resolve) => {
|
|
882
|
-
const lib = DEFAULT_API.startsWith("https") ? https : http;
|
|
883
|
-
const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
|
|
884
|
-
let buf = "";
|
|
885
|
-
res.on("data", (c) => {
|
|
886
|
-
buf += c.toString();
|
|
887
|
-
for (const line of buf.split("\n")) {
|
|
888
|
-
if (line.startsWith("data: ")) {
|
|
889
|
-
try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
|
-
res.on("end", () => resolve(null));
|
|
894
|
-
res.on("error", () => resolve(null));
|
|
895
|
-
});
|
|
896
|
-
req.on("error", () => resolve(null));
|
|
897
|
-
setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
|
|
898
|
-
});
|
|
899
|
-
if (!token) { console.error("\n Authorization timed out or failed."); process.exit(1); }
|
|
900
|
-
mkdirSync(dirname(MAIN_CONFIG), { recursive: true });
|
|
901
|
-
writeFileSync(MAIN_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
|
|
902
|
-
console.log(`\n ${M.green}✓${M.rst} Main token saved: ${M.dim}${MAIN_CONFIG}${M.rst}`);
|
|
903
|
-
process.exit(0);
|
|
904
|
-
}
|
|
905
|
-
console.error("Usage: patchcord main connect");
|
|
906
|
-
process.exit(1);
|
|
932
|
+
if (cmd === "login") {
|
|
933
|
+
// Explicit CLI login. Other commands log in on demand via requireAuth(),
|
|
934
|
+
// so this is only needed to authenticate ahead of time or switch accounts.
|
|
935
|
+
await doLogin();
|
|
936
|
+
process.exit(0);
|
|
907
937
|
}
|
|
908
938
|
|
|
909
939
|
if (cmd === "provision") {
|
|
910
|
-
const m =
|
|
940
|
+
const m = await requireAuth();
|
|
911
941
|
const arg = process.argv[3];
|
|
912
942
|
if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
|
|
913
943
|
if (arg === "revoke") {
|
|
@@ -947,43 +977,55 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
|
|
|
947
977
|
process.exit(0);
|
|
948
978
|
}
|
|
949
979
|
|
|
980
|
+
if (cmd === "teamlead") {
|
|
981
|
+
// Set up the TEAMLEAD agent in THIS folder. The teamlead is a distinct kind
|
|
982
|
+
// of agent — it shepherds a team: provisions, connects, and manages the
|
|
983
|
+
// other agents. It is NOT a normal agent: it gets its own identity, its own
|
|
984
|
+
// onboarding instruction, and on launch either ADOPTS this existing project
|
|
985
|
+
// or CREATES a new team by interviewing the user.
|
|
986
|
+
const m = await requireAuth();
|
|
987
|
+
const root = process.cwd();
|
|
988
|
+
const ns = (flagVal("namespace", basename(root)) || "team").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
989
|
+
const tool = flagVal("tool", "claude_code");
|
|
990
|
+
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
991
|
+
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "teamlead", tool, role: "teamlead", label: "teamlead:self" });
|
|
992
|
+
if (status !== "200" || !json?.token) { console.error(`could not set up teamlead (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
993
|
+
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
994
|
+
writeWorkerConfig(tool, root, base, json.token, hostname);
|
|
995
|
+
mkdirSync(join(root, ".patchcord"), { recursive: true });
|
|
996
|
+
const tj = join(root, ".patchcord", "team.json");
|
|
997
|
+
let manifest = { project: basename(root), namespace: ns, teamlead: { agent: "teamlead", tool }, agents: [] };
|
|
998
|
+
try { if (existsSync(tj)) manifest = { ...JSON.parse(readFileSync(tj, "utf-8")), namespace: ns, teamlead: { agent: "teamlead", tool } }; } catch {}
|
|
999
|
+
writeFileSync(tj, JSON.stringify(manifest, null, 2) + "\n");
|
|
1000
|
+
// The teamlead's onboarding instruction (read on launch).
|
|
1001
|
+
writeFileSync(join(root, "TEAMLEAD.md"), `# You are the TEAMLEAD (${ns}:teamlead)
|
|
1002
|
+
|
|
1003
|
+
You shepherd a team of patchcord agents in this folder. You are NOT a worker —
|
|
1004
|
+
you design the team, provision its agents, launch them, and manage them.
|
|
1005
|
+
|
|
1006
|
+
## On start, pick ONE
|
|
1007
|
+
1. **Adopt this project.** If a team already exists here (\`.patchcord/team.json\`
|
|
1008
|
+
lists workers, or agents are already set up), take it into management:
|
|
1009
|
+
\`patchcord team status\` to reconcile folder ↔ identity ↔ tmux ↔ token, then run it.
|
|
1010
|
+
2. **Create a new team.** Otherwise interview the user — what are we building,
|
|
1011
|
+
which roles/harnesses, how many — confirm the plan, then build it.
|
|
1012
|
+
|
|
1013
|
+
## Build / run
|
|
1014
|
+
- Provision each worker: \`patchcord provision <agent> --tool <claude_code|codex|opencode|kimi> --role <role> --namespace ${ns} --dir <agent>/\`
|
|
1015
|
+
- Launch: \`patchcord team launch\` (mux), verify with \`patchcord team status\`.
|
|
1016
|
+
- Coordinate over patchcord (inbox / send_message); manage at the meta level.
|
|
1017
|
+
- Your own identity here is ${ns}:teamlead — \`patchcord whoami\` confirms it.
|
|
1018
|
+
`);
|
|
1019
|
+
console.log(`\n ${M.green}✓${M.rst} Teamlead ready: ${M.green}${ns}:teamlead${M.rst} [${tool}] in ${root}`);
|
|
1020
|
+
console.log(` ${M.dim}Launch it here — it adopts this project or creates a new team, asking you:${M.rst}`);
|
|
1021
|
+
console.log(` mux new ${tool === "claude_code" ? "claude" : (tool === "kimi-code" ? "kimi" : tool)} --dir .`);
|
|
1022
|
+
process.exit(0);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
950
1025
|
if (cmd === "team") {
|
|
951
1026
|
const sub = process.argv[3];
|
|
952
|
-
if (sub === "init") {
|
|
953
|
-
const project = process.argv[4];
|
|
954
|
-
if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns] [--tool <main-harness>]"); process.exit(1); }
|
|
955
|
-
const ns = flagVal("namespace", project.replace(/[^a-z0-9-]/gi, "-").toLowerCase());
|
|
956
|
-
const mainTool = flagVal("tool", "claude_code");
|
|
957
|
-
const root = join(process.cwd(), project);
|
|
958
|
-
mkdirSync(join(root, ".patchcord"), { recursive: true });
|
|
959
|
-
const manifest = { project, namespace: ns, pattern: "architect-workers-reviewer", main: null, agents: [] };
|
|
960
|
-
// Provision the main's OWN messaging identity IN the team namespace and
|
|
961
|
-
// write its config at the project root, so the main — running here —
|
|
962
|
-
// resolves to ns:main and SEES every worker it provisions into ns.
|
|
963
|
-
// Without this the main has no identity and whoami grabs a stale global.
|
|
964
|
-
const m = readMain();
|
|
965
|
-
if (m) {
|
|
966
|
-
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
967
|
-
const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "main", tool: mainTool, role: "main", label: "main:self" });
|
|
968
|
-
if (status === "200" && json?.token) {
|
|
969
|
-
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
970
|
-
writeWorkerConfig(mainTool, root, base, json.token, hostname);
|
|
971
|
-
manifest.main = { agent: "main", tool: mainTool };
|
|
972
|
-
console.log(`✓ main identity: ${M.green}${ns}:main${M.rst} [${mainTool}] — config written at ${root}`);
|
|
973
|
-
} else {
|
|
974
|
-
console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
|
|
975
|
-
}
|
|
976
|
-
} else {
|
|
977
|
-
console.log(`⚠ no main token — run 'patchcord main connect' first, then re-run team init`);
|
|
978
|
-
}
|
|
979
|
-
writeFileSync(join(root, ".patchcord", "team.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
980
|
-
writeFileSync(join(root, "AGENTS.md"), `# ${project} — Patchcord team\n\nNamespace: ${ns}\nMain identity: ${ns}:main (you, when running patchcord from this folder)\nProvision workers: patchcord provision <agent> --tool X --role Y --namespace ${ns} --dir <agent>/\n`);
|
|
981
|
-
console.log(`✓ team scaffolded: ${root} (namespace ${ns})`);
|
|
982
|
-
console.log(` ${M.dim}cd ${project} — your identity there is ${ns}:main; you'll see every worker you provision into ${ns}.${M.rst}`);
|
|
983
|
-
process.exit(0);
|
|
984
|
-
}
|
|
985
1027
|
if (sub === "list") {
|
|
986
|
-
const m =
|
|
1028
|
+
const m = await requireAuth();
|
|
987
1029
|
const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
|
|
988
1030
|
if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
|
|
989
1031
|
// Dedup by namespace:agent — the server returns one row per token, so a
|
|
@@ -1051,10 +1093,10 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
|
|
|
1051
1093
|
panes.push({ session, window, path: real(path), cmd, claimed: false });
|
|
1052
1094
|
}
|
|
1053
1095
|
|
|
1054
|
-
// Build the roster:
|
|
1096
|
+
// Build the roster: teamlead first, then workers.
|
|
1055
1097
|
const roster = [];
|
|
1056
|
-
const
|
|
1057
|
-
if (
|
|
1098
|
+
const leadEntry = manifest.teamlead || manifest.main || manifest.master; // pre-rename names
|
|
1099
|
+
if (leadEntry) roster.push({ ...leadEntry, role: "teamlead", dir: ".", _lead: true });
|
|
1058
1100
|
for (const a of (manifest.agents || [])) roster.push(a);
|
|
1059
1101
|
|
|
1060
1102
|
const rows = [];
|
|
@@ -1135,14 +1177,14 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
|
|
|
1135
1177
|
}
|
|
1136
1178
|
process.exit(0);
|
|
1137
1179
|
}
|
|
1138
|
-
console.error("Usage: patchcord team <
|
|
1180
|
+
console.error("Usage: patchcord team <list|launch|status>");
|
|
1139
1181
|
process.exit(1);
|
|
1140
1182
|
}
|
|
1141
1183
|
|
|
1142
1184
|
if (cmd === "schedule") {
|
|
1143
1185
|
// User-level scheduled / recurring messages (server Plan 041). Authed by
|
|
1144
1186
|
// the main token — the user can manage schedules in any namespace they own.
|
|
1145
|
-
const m =
|
|
1187
|
+
const m = await requireAuth();
|
|
1146
1188
|
const sub = process.argv[3];
|
|
1147
1189
|
const BASE = `${m.baseUrl}/api/dashboard/scheduled`;
|
|
1148
1190
|
const when = (s) => s.cron_expr ? `cron ${s.cron_expr}` : s.interval_sec ? `every ${s.interval_sec}s` : s.fire_at ? `once @ ${s.fire_at}` : s.schedule_kind;
|