@wrangled/cli 0.3.47 → 0.3.48

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/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";
@@ -2052,6 +2053,72 @@ const commands = {
2052
2053
  console.log(`Stopped trying ${dropped.skill} version ${dropped.version} on ${dropped.workspace}. Runs stay on version ${dropped.versionInUse}.`);
2053
2054
  },
2054
2055
  },
2056
+ "schedules list": {
2057
+ usage: "schedules list (the client's scheduled hands, and who set each one up)",
2058
+ async run(args, options) {
2059
+ if (args.length > 0)
2060
+ throw new CliError("Usage: schedules list", exitCodes.usage);
2061
+ const { schedules } = await api(await resolveConnection(options), SCHEDULES_PATH);
2062
+ if (options.format === "json")
2063
+ return printJson({ schedules });
2064
+ if (schedules.length === 0)
2065
+ return console.log("No scheduled hands yet.");
2066
+ const rows = schedules.map((w) => ({
2067
+ id: w.id,
2068
+ skill: w.skill,
2069
+ schedule: `${w.schedule} ${w.timeZone}`,
2070
+ runner: w.runner,
2071
+ enabled: w.enabled ? "yes" : "no",
2072
+ "next run": w.nextRunAt,
2073
+ "set up by": w.createdBy ?? "operator",
2074
+ }));
2075
+ console.log(renderTable(["id", "skill", "schedule", "runner", "enabled", "next run", "set up by"], rows));
2076
+ },
2077
+ },
2078
+ "schedules create": {
2079
+ usage: `${SCHEDULES_CREATE_USAGE} (a key that can manage settings)`,
2080
+ async run(args, options) {
2081
+ if (args.length > 0)
2082
+ throw new CliError(`Usage: ${SCHEDULES_CREATE_USAGE}`, exitCodes.usage);
2083
+ const file = options.schedule.promptFile;
2084
+ const body = scheduleCreateBody(options.schedule, file === undefined ? null : await readFile(await inRunFolder(file), "utf8"));
2085
+ const { schedule, created } = await api(await resolveConnection(options), SCHEDULES_PATH, body);
2086
+ if (options.format === "json")
2087
+ return printJson({ schedule, created });
2088
+ console.log(`${created ? "Set up" : "Changed"} the scheduled hand ${schedule.skill} (${schedule.id}): ${schedule.schedule} ${schedule.timeZone}, next run ${schedule.nextRunAt}.`);
2089
+ },
2090
+ },
2091
+ ...Object.fromEntries([
2092
+ ["pause", false, "Paused", "It does not run on its schedule until resumed."],
2093
+ ["resume", true, "Resumed", "Its next run is"],
2094
+ ].map(([verb, enabled, done, then]) => [
2095
+ `schedules ${verb}`,
2096
+ {
2097
+ usage: `schedules ${verb} <id | skill | name> (a key that can manage settings)`,
2098
+ async run(args, options) {
2099
+ if (args.length !== 1)
2100
+ throw new CliError(`Usage: schedules ${verb} <id | skill | name>`, exitCodes.usage);
2101
+ const connection = await resolveConnection(options);
2102
+ const id = await scheduleId(connection, args[0]);
2103
+ const { schedule } = await api(connection, schedulePath(id), { enabled }, "PATCH");
2104
+ if (options.format === "json")
2105
+ return printJson({ schedule });
2106
+ console.log(`${done} ${schedule.skill}. ${enabled ? `${then} ${schedule.nextRunAt}.` : then}`);
2107
+ },
2108
+ },
2109
+ ])),
2110
+ "schedules delete": {
2111
+ usage: "schedules delete <id | skill | name> (a key that can manage settings; its past runs are kept)",
2112
+ async run(args, options) {
2113
+ if (args.length !== 1)
2114
+ throw new CliError("Usage: schedules delete <id | skill | name>", exitCodes.usage);
2115
+ const connection = await resolveConnection(options);
2116
+ const result = await api(connection, schedulePath(await scheduleId(connection, args[0])), undefined, "DELETE");
2117
+ if (options.format === "json")
2118
+ return printJson(result);
2119
+ console.log(`Deleted the scheduled hand ${result.deleted.skill}. It no longer runs; its past runs are kept.`);
2120
+ },
2121
+ },
2055
2122
  "runner once": {
2056
2123
  usage: `runner once [--run <id>] [--max-seconds <n>] (takes the runner token in ${RUNNER_TOKEN_ENV})`,
2057
2124
  async run(args, options) {
@@ -2705,6 +2772,16 @@ Options:
2705
2772
  --kind email|text|page with content tags and content autotag, only that kind (content autotag takes a comma list)
2706
2773
  --max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
2707
2774
  --workspace <slug> for worker promote and worker drop: the workspace whose hand they change
2775
+ --skill <name>[@<n>] schedules create: the library skill the hand runs (its latest version unless @n)
2776
+ --name <name> --prompt-file <path> schedules create: save the file (at most 64 KB) as this client's own skill
2777
+ sched-<client code>-<name> and run that instead; changed text adds a new version
2778
+ --cron "<5 fields>" schedules create: when it runs, like "0 9 * * 1" for Mondays at 9:00
2779
+ --time-zone <zone> schedules create: the time zone --cron is read in (default: the client's reporting time zone)
2780
+ --runner mac_mini|trigger schedules create: where it runs (trigger, the cloud, is refused until it is live)
2781
+ --harness claude|codex, --model <id>, --prompt <text>, --deadline-minutes <1-60> schedules create: the agent, model,
2782
+ run prompt and time limit of each run
2783
+ --ai-cap-cents, --tool-cap-cents <n> schedules create: what one run may spend on AI and on images and video, at most
2784
+ 1000 and 2000 cents
2708
2785
  -h, --help show this help
2709
2786
 
2710
2787
  skill publish, skill list and skill install <name> use the skill library, which takes the operator token in
@@ -2825,6 +2902,16 @@ function parseOptions(argv) {
2825
2902
  confidence: { type: "string" },
2826
2903
  remove: { type: "boolean", default: false },
2827
2904
  "dry-run": { type: "boolean", default: false },
2905
+ skill: { type: "string" },
2906
+ cron: { type: "string" },
2907
+ "time-zone": { type: "string" },
2908
+ runner: { type: "string" },
2909
+ model: { type: "string" },
2910
+ prompt: { type: "string" },
2911
+ "prompt-file": { type: "string" },
2912
+ "deadline-minutes": { type: "string" },
2913
+ "ai-cap-cents": { type: "string" },
2914
+ "tool-cap-cents": { type: "string" },
2828
2915
  help: { type: "boolean", short: "h", default: false },
2829
2916
  },
2830
2917
  });
@@ -2937,6 +3024,20 @@ function parseOptions(argv) {
2937
3024
  confidence: values.confidence,
2938
3025
  remove: values.remove,
2939
3026
  dryRun: values["dry-run"],
3027
+ schedule: {
3028
+ skill: values.skill,
3029
+ name: values.name,
3030
+ promptFile: values["prompt-file"],
3031
+ cron: values.cron,
3032
+ timeZone: values["time-zone"],
3033
+ runner: values.runner,
3034
+ harness: values.harness,
3035
+ model: values.model,
3036
+ prompt: values.prompt,
3037
+ deadlineMinutes: values["deadline-minutes"],
3038
+ aiCapCents: values["ai-cap-cents"],
3039
+ toolCapCents: values["tool-cap-cents"],
3040
+ },
2940
3041
  },
2941
3042
  };
2942
3043
  }
@@ -2947,6 +3048,12 @@ async function changeCandidate(change, args, options) {
2947
3048
  return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
2948
3049
  }
2949
3050
  const SECRETS_PATH = "/api/v1/secrets";
3051
+ const SCHEDULES_PATH = "/api/v1/schedules";
3052
+ const schedulePath = (id) => `${SCHEDULES_PATH}/${encodeURIComponent(id)}`;
3053
+ async function scheduleId(connection, ref) {
3054
+ const { schedules } = await api(connection, SCHEDULES_PATH);
3055
+ return scheduleIdFor(schedules, ref);
3056
+ }
2950
3057
  function secretPath(provider) {
2951
3058
  return `${SECRETS_PATH}/${encodeURIComponent(provider)}`;
2952
3059
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.47",
3
+ "version": "0.3.48",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {