@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 +107 -0
- package/dist/schedules.js +55 -0
- package/package.json +1 -1
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
|
+
}
|