@wrangled/cli 0.3.47 → 0.3.49
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/guide.js +34 -0
- package/dist/main.js +206 -0
- package/dist/schedules.js +55 -0
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -17,6 +17,24 @@ export const funnelTestExample = {
|
|
|
17
17
|
split_method: "adzara_ads",
|
|
18
18
|
evidence: [{ kind: "customer_words", ref: "sales call 2026-09-30", note: "I didn't know what the webinar was about." }],
|
|
19
19
|
};
|
|
20
|
+
/** The dashboard the guide shows; the app's parser must accept it (src/lib/cli-consistency.test.ts). */
|
|
21
|
+
export const dashboardExample = {
|
|
22
|
+
title: "Daily ads and leads",
|
|
23
|
+
dateRange: { default: "30d", picker: true },
|
|
24
|
+
panels: [
|
|
25
|
+
{
|
|
26
|
+
id: "spend",
|
|
27
|
+
title: "Ad spend by day",
|
|
28
|
+
sql: "SELECT date, sum(spend) AS spend FROM daily_funnel WHERE date BETWEEN {{from}} AND {{to}} GROUP BY date ORDER BY date",
|
|
29
|
+
chart: "line",
|
|
30
|
+
x: "date",
|
|
31
|
+
y: ["spend"],
|
|
32
|
+
format: { spend: "money" },
|
|
33
|
+
width: 2,
|
|
34
|
+
},
|
|
35
|
+
{ id: "leads", title: "Leads", sql: "SELECT sum(leads) AS leads FROM daily_funnel WHERE date BETWEEN {{from}} AND {{to}}", chart: "number", y: ["leads"], format: { leads: "count" } },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
20
38
|
export const guide = `# ${brand.name} CLI
|
|
21
39
|
|
|
22
40
|
\`${cli}\` gives read-only SQL access to one client's marketing data warehouse (ad spend, orders, and other synced sources), runs paid generation and research tools for that client, and keeps the creative ledger. Each API key belongs to exactly one workspace, and every command works only in that workspace.
|
|
@@ -266,6 +284,20 @@ Each section's \`audience\` is \`staff\`, \`client\` or \`both\`; a person on st
|
|
|
266
284
|
|
|
267
285
|
The story hand writes it each week. Its playbook installs with \`${cli} skill install story\` as the \`${STORY_SKILL_NAME}\` skill and prints with \`${cli} docs story\`.
|
|
268
286
|
|
|
287
|
+
## Team dashboards
|
|
288
|
+
|
|
289
|
+
A team dashboard is a page of charts the team opens for one client, such as daily ad spend, leads and cost per lead with a date picker. You write it as a short spec; ${brand.name} saves it and reads every number live each time someone opens it, so nothing is copied out. \`${cli} dashboards create --file spec.json\` publishes one (or pipe the spec on stdin):
|
|
290
|
+
|
|
291
|
+
\`\`\`json
|
|
292
|
+
${JSON.stringify(dashboardExample)}
|
|
293
|
+
\`\`\`
|
|
294
|
+
|
|
295
|
+
Each panel is one SQL query and how to draw it: \`chart\` is \`line\`, \`bar\`, \`area\`, \`table\` or \`number\`. A line, bar or area panel names its \`x\` column and its \`y\` columns; a number panel one \`y\` column. \`format\` marks columns as \`money\`, \`count\` or \`percent\`, and \`width\` 2 takes the full row. \`dateRange.default\` is \`7d\`, \`30d\`, \`90d\`, \`this-month\` or \`last-month\`, and \`picker\` shows a date picker. Write \`{{from}}\` and \`{{to}}\` where the page's first and last day go; the server binds them as dates in the client's reporting time zone, and refuses any other \`{{...}}\`. A dashboard has at most 20 panels and 20 KB of SQL in each.
|
|
296
|
+
|
|
297
|
+
Before saving, the server runs every panel once over the default range as this workspace's reader. A panel that fails, or lacks a column it names, refuses the whole spec with the panel's id and, when it knows one, a hint at the right name. The answer has \`url\`, the link that opens the dashboard for this client: give it to the person who asked.
|
|
298
|
+
|
|
299
|
+
\`${cli} dashboards update <slug> --file spec.json\` publishes the next version of the whole spec, \`${cli} dashboards rollback <slug> --version <n>\` publishes an earlier version's spec again as the next version, and \`${cli} dashboards archive <slug>\` takes one down. \`${cli} dashboards list\` lists them, and \`${cli} dashboards show <slug> --data\` prints the spec, every version, and each panel's rows now (\`--from\` and \`--to\` pick the days), so you can check the numbers.
|
|
300
|
+
|
|
269
301
|
## The Facebook ads agent
|
|
270
302
|
|
|
271
303
|
The daily Facebook ads agent follows its own playbook: install it with \`${cli} skill install meta-ads\` (or \`--codex\` for an AGENTS.md in the current directory) and read it with \`${cli} docs meta-ads\`. It works in runs (\`${cli} agent start\`, \`${cli} agent finish\`), reads and suggests settled facts (\`${cli} facts\`), keeps a brand profile (\`${cli} brand\`), and makes proposals (\`${cli} proposals\`) that a person approves in the web app. Nothing it does is sent to Meta.
|
|
@@ -383,6 +415,8 @@ ${cli} voice lists where that voice comes from: the competitors
|
|
|
383
415
|
${cli} voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>] suggest a competitor, subreddit or Reddit search phrase for a person to approve
|
|
384
416
|
${cli} story [--json] | story list | story show <version> the weekly story, its versions, and the rows behind each sentence
|
|
385
417
|
${cli} story submit --file <story.json> save the next version of the weekly story
|
|
418
|
+
${cli} dashboards list | dashboards show <slug> [--data] [--from <date>] [--to <date>] the team dashboards, one's panels and versions, and its numbers now
|
|
419
|
+
${cli} dashboards create --file <spec.json> | dashboards update <slug> --file <spec.json> | dashboards rollback <slug> --version <n> | dashboards archive <slug>
|
|
386
420
|
${cli} worker list | worker runs <hand-id | skill> [--limit <n>] | worker run-now <hand-id | skill> this workspace's scheduled hands, their runs and costs, and Run now
|
|
387
421
|
${cli} skill install [${PLAYBOOK_CHOICES}] [--project] [--codex] install a Claude Code skill (or the playbook for Codex)
|
|
388
422
|
${cli} docs [${PLAYBOOK_CHOICES}] print this guide, or a hand's playbook
|
package/dist/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import { parseArgs } from "node:util";
|
|
|
7
7
|
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, entryTagFlags, streamSigned, } from "./api.js";
|
|
8
8
|
import { brand, envPrefix } from "./brand.js";
|
|
9
9
|
import { firstSymlink, inRunFolder, writeFileAtomic } from "./files.js";
|
|
10
|
+
import { scheduleCreateBody, scheduleIdFor, SCHEDULES_CREATE_USAGE } from "./schedules.js";
|
|
10
11
|
import { secretBody, secretProvider, SECRETS_SET_USAGE } from "./secrets.js";
|
|
11
12
|
import { guide, skill } from "./guide.js";
|
|
12
13
|
import { PULL_STATE_FILE, pullContent } from "./pull.js";
|
|
@@ -1805,6 +1806,83 @@ const commands = {
|
|
|
1805
1806
|
printStory(story);
|
|
1806
1807
|
},
|
|
1807
1808
|
},
|
|
1809
|
+
"dashboards list": {
|
|
1810
|
+
usage: "dashboards list the team dashboards, the most recently changed first, with the link that opens each",
|
|
1811
|
+
async run(args, options) {
|
|
1812
|
+
if (args.length > 0)
|
|
1813
|
+
throw new CliError("Usage: dashboards list", exitCodes.usage);
|
|
1814
|
+
const { dashboards } = await api(await resolveConnection(options), DASHBOARDS_PATH);
|
|
1815
|
+
if (options.format === "json")
|
|
1816
|
+
return printJson({ dashboards });
|
|
1817
|
+
if (dashboards.length === 0)
|
|
1818
|
+
return console.log("No dashboards yet.");
|
|
1819
|
+
console.log(renderTable(["slug", "title", "version", "updated", "url"], dashboards.map((d) => ({ ...d, updated: d.updatedAt }))));
|
|
1820
|
+
},
|
|
1821
|
+
},
|
|
1822
|
+
"dashboards show": {
|
|
1823
|
+
usage: "dashboards show <slug> [--data] [--from <date>] [--to <date>] one dashboard's panels and versions; --data reads every panel's rows now",
|
|
1824
|
+
async run(args, options) {
|
|
1825
|
+
if (args.length !== 1)
|
|
1826
|
+
throw new CliError("Usage: dashboards show <slug> [--data] [--from <date>] [--to <date>]", exitCodes.usage);
|
|
1827
|
+
if (!options.data && (options.from !== undefined || options.to !== undefined))
|
|
1828
|
+
throw new CliError("--from and --to go with --data.", exitCodes.usage);
|
|
1829
|
+
const connection = await resolveConnection(options);
|
|
1830
|
+
const { dashboard } = await api(connection, dashboardPath(args[0]));
|
|
1831
|
+
let data;
|
|
1832
|
+
if (options.data) {
|
|
1833
|
+
const range = new URLSearchParams(Object.entries({ from: options.from, to: options.to }).filter((e) => e[1] !== undefined));
|
|
1834
|
+
data = JSON.parse(await apiText(connection, `${dashboardPath(args[0])}/data${range.size > 0 ? `?${range}` : ""}`), keepRawNumbers);
|
|
1835
|
+
}
|
|
1836
|
+
if (options.format === "json")
|
|
1837
|
+
return printJson(data ? { dashboard, data } : { dashboard });
|
|
1838
|
+
printDashboard(dashboard, data);
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
"dashboards create": {
|
|
1842
|
+
usage: "dashboards create --file <spec.json> | < spec.json publish a new dashboard once every panel runs",
|
|
1843
|
+
async run(args, options) {
|
|
1844
|
+
if (args.length > 0)
|
|
1845
|
+
throw new CliError("Usage: dashboards create --file <spec.json> | < spec.json", exitCodes.usage);
|
|
1846
|
+
const { dashboard } = await api(await resolveConnection(options), DASHBOARDS_PATH, await readToolInput(options));
|
|
1847
|
+
if (options.format === "json")
|
|
1848
|
+
return printJson({ dashboard });
|
|
1849
|
+
console.log(`Published dashboard ${dashboard.slug}, version ${dashboard.version}.\n${dashboard.url}`);
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
"dashboards update": {
|
|
1853
|
+
usage: "dashboards update <slug> --file <spec.json> | < spec.json publish the dashboard's next version once every panel runs",
|
|
1854
|
+
async run(args, options) {
|
|
1855
|
+
if (args.length !== 1)
|
|
1856
|
+
throw new CliError("Usage: dashboards update <slug> --file <spec.json> | < spec.json", exitCodes.usage);
|
|
1857
|
+
const { dashboard } = await api(await resolveConnection(options), dashboardPath(args[0]), await readToolInput(options), "PUT");
|
|
1858
|
+
if (options.format === "json")
|
|
1859
|
+
return printJson({ dashboard });
|
|
1860
|
+
console.log(`Published dashboard ${dashboard.slug}, version ${dashboard.version}.\n${dashboard.url}`);
|
|
1861
|
+
},
|
|
1862
|
+
},
|
|
1863
|
+
"dashboards rollback": {
|
|
1864
|
+
usage: "dashboards rollback <slug> --version <n> publish an earlier version's spec again, as the next version",
|
|
1865
|
+
async run(args, options) {
|
|
1866
|
+
if (args.length !== 1 || options.version === undefined)
|
|
1867
|
+
throw new CliError("Usage: dashboards rollback <slug> --version <n>", exitCodes.usage);
|
|
1868
|
+
const version = wholeNumber(options.version, "--version");
|
|
1869
|
+
const { dashboard } = await api(await resolveConnection(options), `${dashboardPath(args[0])}/rollback`, { version });
|
|
1870
|
+
if (options.format === "json")
|
|
1871
|
+
return printJson({ dashboard });
|
|
1872
|
+
console.log(`Published version ${version}'s spec as dashboard ${dashboard.slug}, version ${dashboard.version}.\n${dashboard.url}`);
|
|
1873
|
+
},
|
|
1874
|
+
},
|
|
1875
|
+
"dashboards archive": {
|
|
1876
|
+
usage: "dashboards archive <slug> take a dashboard down; its versions are kept, and publishing at its slug brings it back",
|
|
1877
|
+
async run(args, options) {
|
|
1878
|
+
if (args.length !== 1)
|
|
1879
|
+
throw new CliError("Usage: dashboards archive <slug>", exitCodes.usage);
|
|
1880
|
+
const { archived } = await api(await resolveConnection(options), dashboardPath(args[0]), undefined, "DELETE");
|
|
1881
|
+
if (options.format === "json")
|
|
1882
|
+
return printJson({ archived });
|
|
1883
|
+
console.log(`Archived dashboard ${archived.slug}.`);
|
|
1884
|
+
},
|
|
1885
|
+
},
|
|
1808
1886
|
"story submit": {
|
|
1809
1887
|
usage: "story submit --file <story.json> | < story.json save the next version; the server runs each query and keeps its rows",
|
|
1810
1888
|
async run(args, options) {
|
|
@@ -2052,6 +2130,72 @@ const commands = {
|
|
|
2052
2130
|
console.log(`Stopped trying ${dropped.skill} version ${dropped.version} on ${dropped.workspace}. Runs stay on version ${dropped.versionInUse}.`);
|
|
2053
2131
|
},
|
|
2054
2132
|
},
|
|
2133
|
+
"schedules list": {
|
|
2134
|
+
usage: "schedules list (the client's scheduled hands, and who set each one up)",
|
|
2135
|
+
async run(args, options) {
|
|
2136
|
+
if (args.length > 0)
|
|
2137
|
+
throw new CliError("Usage: schedules list", exitCodes.usage);
|
|
2138
|
+
const { schedules } = await api(await resolveConnection(options), SCHEDULES_PATH);
|
|
2139
|
+
if (options.format === "json")
|
|
2140
|
+
return printJson({ schedules });
|
|
2141
|
+
if (schedules.length === 0)
|
|
2142
|
+
return console.log("No scheduled hands yet.");
|
|
2143
|
+
const rows = schedules.map((w) => ({
|
|
2144
|
+
id: w.id,
|
|
2145
|
+
skill: w.skill,
|
|
2146
|
+
schedule: `${w.schedule} ${w.timeZone}`,
|
|
2147
|
+
runner: w.runner,
|
|
2148
|
+
enabled: w.enabled ? "yes" : "no",
|
|
2149
|
+
"next run": w.nextRunAt,
|
|
2150
|
+
"set up by": w.createdBy ?? "operator",
|
|
2151
|
+
}));
|
|
2152
|
+
console.log(renderTable(["id", "skill", "schedule", "runner", "enabled", "next run", "set up by"], rows));
|
|
2153
|
+
},
|
|
2154
|
+
},
|
|
2155
|
+
"schedules create": {
|
|
2156
|
+
usage: `${SCHEDULES_CREATE_USAGE} (a key that can manage settings)`,
|
|
2157
|
+
async run(args, options) {
|
|
2158
|
+
if (args.length > 0)
|
|
2159
|
+
throw new CliError(`Usage: ${SCHEDULES_CREATE_USAGE}`, exitCodes.usage);
|
|
2160
|
+
const file = options.schedule.promptFile;
|
|
2161
|
+
const body = scheduleCreateBody(options.schedule, file === undefined ? null : await readFile(await inRunFolder(file), "utf8"));
|
|
2162
|
+
const { schedule, created } = await api(await resolveConnection(options), SCHEDULES_PATH, body);
|
|
2163
|
+
if (options.format === "json")
|
|
2164
|
+
return printJson({ schedule, created });
|
|
2165
|
+
console.log(`${created ? "Set up" : "Changed"} the scheduled hand ${schedule.skill} (${schedule.id}): ${schedule.schedule} ${schedule.timeZone}, next run ${schedule.nextRunAt}.`);
|
|
2166
|
+
},
|
|
2167
|
+
},
|
|
2168
|
+
...Object.fromEntries([
|
|
2169
|
+
["pause", false, "Paused", "It does not run on its schedule until resumed."],
|
|
2170
|
+
["resume", true, "Resumed", "Its next run is"],
|
|
2171
|
+
].map(([verb, enabled, done, then]) => [
|
|
2172
|
+
`schedules ${verb}`,
|
|
2173
|
+
{
|
|
2174
|
+
usage: `schedules ${verb} <id | skill | name> (a key that can manage settings)`,
|
|
2175
|
+
async run(args, options) {
|
|
2176
|
+
if (args.length !== 1)
|
|
2177
|
+
throw new CliError(`Usage: schedules ${verb} <id | skill | name>`, exitCodes.usage);
|
|
2178
|
+
const connection = await resolveConnection(options);
|
|
2179
|
+
const id = await scheduleId(connection, args[0]);
|
|
2180
|
+
const { schedule } = await api(connection, schedulePath(id), { enabled }, "PATCH");
|
|
2181
|
+
if (options.format === "json")
|
|
2182
|
+
return printJson({ schedule });
|
|
2183
|
+
console.log(`${done} ${schedule.skill}. ${enabled ? `${then} ${schedule.nextRunAt}.` : then}`);
|
|
2184
|
+
},
|
|
2185
|
+
},
|
|
2186
|
+
])),
|
|
2187
|
+
"schedules delete": {
|
|
2188
|
+
usage: "schedules delete <id | skill | name> (a key that can manage settings; its past runs are kept)",
|
|
2189
|
+
async run(args, options) {
|
|
2190
|
+
if (args.length !== 1)
|
|
2191
|
+
throw new CliError("Usage: schedules delete <id | skill | name>", exitCodes.usage);
|
|
2192
|
+
const connection = await resolveConnection(options);
|
|
2193
|
+
const result = await api(connection, schedulePath(await scheduleId(connection, args[0])), undefined, "DELETE");
|
|
2194
|
+
if (options.format === "json")
|
|
2195
|
+
return printJson(result);
|
|
2196
|
+
console.log(`Deleted the scheduled hand ${result.deleted.skill}. It no longer runs; its past runs are kept.`);
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2055
2199
|
"runner once": {
|
|
2056
2200
|
usage: `runner once [--run <id>] [--max-seconds <n>] (takes the runner token in ${RUNNER_TOKEN_ENV})`,
|
|
2057
2201
|
async run(args, options) {
|
|
@@ -2559,6 +2703,26 @@ function nameKeyLines(value) {
|
|
|
2559
2703
|
return ["", "Name patterns:", ...(patterns.length > 0 ? patterns : ["- none yet"]), "", "Open questions:", ...(questions.length > 0 ? questions : ["- none"])];
|
|
2560
2704
|
}
|
|
2561
2705
|
const STORIES_PATH = "/api/v1/stories";
|
|
2706
|
+
const DASHBOARDS_PATH = "/api/v1/dashboards";
|
|
2707
|
+
const dashboardPath = (slug) => `${DASHBOARDS_PATH}/${encodeURIComponent(slug)}`;
|
|
2708
|
+
function printDashboard(dashboard, data) {
|
|
2709
|
+
console.log(`${dashboard.title}: version ${dashboard.version}, updated ${dashboard.updatedAt}.\n${dashboard.url}`);
|
|
2710
|
+
const range = dashboard.spec.dateRange;
|
|
2711
|
+
if (range)
|
|
2712
|
+
console.log(`Default range ${range.default}${range.picker ? ", with a date picker" : ""}.`);
|
|
2713
|
+
console.log(`\n${renderTable(["panel", "title", "chart", "x", "y"], dashboard.spec.panels.map((p) => ({ panel: p.id, title: p.title, chart: p.chart, x: p.x, y: p.y?.join(", ") })))}`);
|
|
2714
|
+
console.log(`\nVersions:\n${renderTable(["version", "by", "at"], dashboard.versions.map((v) => ({ version: v.version, by: v.createdBy, at: v.createdAt })))}`);
|
|
2715
|
+
if (!data)
|
|
2716
|
+
return;
|
|
2717
|
+
console.log(`\nNumbers from ${data.range.from} to ${data.range.to}:`);
|
|
2718
|
+
for (const panel of data.panels) {
|
|
2719
|
+
console.log(`\n${panel.id}`);
|
|
2720
|
+
if ("error" in panel)
|
|
2721
|
+
console.log(`failed: ${panel.error.message}${panel.error.hint ? `\nHint: ${panel.error.hint}` : ""}`);
|
|
2722
|
+
else
|
|
2723
|
+
console.log(`${renderTable(panel.columns.map((c) => c.name), panel.rows)}${panel.truncated ? `\n(cut off at the ${panel.truncated.replace("_", " ")})` : ""}`);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2562
2726
|
function printStory(story) {
|
|
2563
2727
|
const corrects = story.previousId ? `, correcting ${story.previousId}` : "";
|
|
2564
2728
|
console.log(`Story version ${story.version}, up to ${story.periodEnd}, saved ${story.createdAt} by ${story.author}${corrects}.`);
|
|
@@ -2705,6 +2869,16 @@ Options:
|
|
|
2705
2869
|
--kind email|text|page with content tags and content autotag, only that kind (content autotag takes a comma list)
|
|
2706
2870
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
2707
2871
|
--workspace <slug> for worker promote and worker drop: the workspace whose hand they change
|
|
2872
|
+
--skill <name>[@<n>] schedules create: the library skill the hand runs (its latest version unless @n)
|
|
2873
|
+
--name <name> --prompt-file <path> schedules create: save the file (at most 64 KB) as this client's own skill
|
|
2874
|
+
sched-<client code>-<name> and run that instead; changed text adds a new version
|
|
2875
|
+
--cron "<5 fields>" schedules create: when it runs, like "0 9 * * 1" for Mondays at 9:00
|
|
2876
|
+
--time-zone <zone> schedules create: the time zone --cron is read in (default: the client's reporting time zone)
|
|
2877
|
+
--runner mac_mini|trigger schedules create: where it runs (trigger, the cloud, is refused until it is live)
|
|
2878
|
+
--harness claude|codex, --model <id>, --prompt <text>, --deadline-minutes <1-60> schedules create: the agent, model,
|
|
2879
|
+
run prompt and time limit of each run
|
|
2880
|
+
--ai-cap-cents, --tool-cap-cents <n> schedules create: what one run may spend on AI and on images and video, at most
|
|
2881
|
+
1000 and 2000 cents
|
|
2708
2882
|
-h, --help show this help
|
|
2709
2883
|
|
|
2710
2884
|
skill publish, skill list and skill install <name> use the skill library, which takes the operator token in
|
|
@@ -2825,6 +2999,17 @@ function parseOptions(argv) {
|
|
|
2825
2999
|
confidence: { type: "string" },
|
|
2826
3000
|
remove: { type: "boolean", default: false },
|
|
2827
3001
|
"dry-run": { type: "boolean", default: false },
|
|
3002
|
+
data: { type: "boolean", default: false },
|
|
3003
|
+
skill: { type: "string" },
|
|
3004
|
+
cron: { type: "string" },
|
|
3005
|
+
"time-zone": { type: "string" },
|
|
3006
|
+
runner: { type: "string" },
|
|
3007
|
+
model: { type: "string" },
|
|
3008
|
+
prompt: { type: "string" },
|
|
3009
|
+
"prompt-file": { type: "string" },
|
|
3010
|
+
"deadline-minutes": { type: "string" },
|
|
3011
|
+
"ai-cap-cents": { type: "string" },
|
|
3012
|
+
"tool-cap-cents": { type: "string" },
|
|
2828
3013
|
help: { type: "boolean", short: "h", default: false },
|
|
2829
3014
|
},
|
|
2830
3015
|
});
|
|
@@ -2937,6 +3122,21 @@ function parseOptions(argv) {
|
|
|
2937
3122
|
confidence: values.confidence,
|
|
2938
3123
|
remove: values.remove,
|
|
2939
3124
|
dryRun: values["dry-run"],
|
|
3125
|
+
data: values.data,
|
|
3126
|
+
schedule: {
|
|
3127
|
+
skill: values.skill,
|
|
3128
|
+
name: values.name,
|
|
3129
|
+
promptFile: values["prompt-file"],
|
|
3130
|
+
cron: values.cron,
|
|
3131
|
+
timeZone: values["time-zone"],
|
|
3132
|
+
runner: values.runner,
|
|
3133
|
+
harness: values.harness,
|
|
3134
|
+
model: values.model,
|
|
3135
|
+
prompt: values.prompt,
|
|
3136
|
+
deadlineMinutes: values["deadline-minutes"],
|
|
3137
|
+
aiCapCents: values["ai-cap-cents"],
|
|
3138
|
+
toolCapCents: values["tool-cap-cents"],
|
|
3139
|
+
},
|
|
2940
3140
|
},
|
|
2941
3141
|
};
|
|
2942
3142
|
}
|
|
@@ -2947,6 +3147,12 @@ async function changeCandidate(change, args, options) {
|
|
|
2947
3147
|
return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
|
|
2948
3148
|
}
|
|
2949
3149
|
const SECRETS_PATH = "/api/v1/secrets";
|
|
3150
|
+
const SCHEDULES_PATH = "/api/v1/schedules";
|
|
3151
|
+
const schedulePath = (id) => `${SCHEDULES_PATH}/${encodeURIComponent(id)}`;
|
|
3152
|
+
async function scheduleId(connection, ref) {
|
|
3153
|
+
const { schedules } = await api(connection, SCHEDULES_PATH);
|
|
3154
|
+
return scheduleIdFor(schedules, ref);
|
|
3155
|
+
}
|
|
2950
3156
|
function secretPath(provider) {
|
|
2951
3157
|
return `${SECRETS_PATH}/${encodeURIComponent(provider)}`;
|
|
2952
3158
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CliError, exitCodes } from "./api.js";
|
|
2
|
+
export const SCHEDULES_CREATE_USAGE = 'schedules create (--skill <name>[@<version>] | --name <name> --prompt-file <path>) --cron "<5 fields>" [--time-zone <zone>] ' +
|
|
3
|
+
"[--runner mac_mini|trigger] [--harness claude|codex] [--model <id>] [--prompt <text>] [--deadline-minutes <n>] [--ai-cap-cents <n>] [--tool-cap-cents <n>]";
|
|
4
|
+
const usage = (problem) => new CliError(`${problem}\nUsage: ${SCHEDULES_CREATE_USAGE}`, exitCodes.usage);
|
|
5
|
+
function whole(flag, raw) {
|
|
6
|
+
if (raw === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (!/^\d+$/.test(raw))
|
|
9
|
+
throw usage(`--${flag} must be a whole number; got '${raw}'.`);
|
|
10
|
+
return Number(raw);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What POST /api/v1/schedules takes for these flags. `promptFileText` is the text of --prompt-file, read by the caller.
|
|
14
|
+
* The server checks the values themselves; this only checks the flags fit together.
|
|
15
|
+
*/
|
|
16
|
+
export function scheduleCreateBody(flags, promptFileText) {
|
|
17
|
+
const fromFile = flags.name !== undefined || flags.promptFile !== undefined;
|
|
18
|
+
if ((flags.skill === undefined) === !fromFile)
|
|
19
|
+
throw usage("Give --skill, or --name with --prompt-file: one of the two.");
|
|
20
|
+
if (fromFile && (flags.name === undefined || flags.promptFile === undefined))
|
|
21
|
+
throw usage("--name and --prompt-file go together.");
|
|
22
|
+
if (flags.cron === undefined)
|
|
23
|
+
throw usage("--cron is required.");
|
|
24
|
+
const body = {
|
|
25
|
+
skill: flags.skill,
|
|
26
|
+
name: flags.name,
|
|
27
|
+
promptFile: fromFile ? (promptFileText ?? "") : undefined,
|
|
28
|
+
schedule: flags.cron,
|
|
29
|
+
timeZone: flags.timeZone,
|
|
30
|
+
runner: flags.runner,
|
|
31
|
+
harness: flags.harness,
|
|
32
|
+
model: flags.model,
|
|
33
|
+
prompt: flags.prompt,
|
|
34
|
+
deadlineMinutes: whole("deadline-minutes", flags.deadlineMinutes),
|
|
35
|
+
aiCapCents: whole("ai-cap-cents", flags.aiCapCents),
|
|
36
|
+
toolCapCents: whole("tool-cap-cents", flags.toolCapCents),
|
|
37
|
+
};
|
|
38
|
+
return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The id of the hand `ref` names: its id, its skill, or for a hand made from a prompt file (skill
|
|
42
|
+
* `sched-<workspace code>-<name>`), the name it was given, when exactly one hand has it.
|
|
43
|
+
*/
|
|
44
|
+
export function scheduleIdFor(schedules, ref) {
|
|
45
|
+
const exact = schedules.find((s) => s.id === ref.toLowerCase()) ?? schedules.find((s) => s.skill === ref);
|
|
46
|
+
if (exact)
|
|
47
|
+
return exact.id;
|
|
48
|
+
const named = schedules.filter((s) => /^sched-[a-z0-9]+-/.test(s.skill) && s.skill.replace(/^sched-[a-z0-9]+-/, "") === ref);
|
|
49
|
+
if (named.length === 1)
|
|
50
|
+
return named[0].id;
|
|
51
|
+
if (named.length > 1) {
|
|
52
|
+
throw new CliError(`More than one scheduled hand is named ${JSON.stringify(ref)} (${named.map((s) => s.skill).join(", ")}); give its id or full skill name.`, exitCodes.usage);
|
|
53
|
+
}
|
|
54
|
+
throw new CliError(`schedule_not_found: no scheduled hand with id, skill or name ${JSON.stringify(ref)} in this workspace`, exitCodes.error);
|
|
55
|
+
}
|