@hyperdrive.bot/paseo-cli 0.3.34 → 0.3.36

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/cli.js CHANGED
@@ -6,6 +6,7 @@ import { createLoopCommand } from "./commands/loop/index.js";
6
6
  import { createPermitCommand } from "./commands/permit/index.js";
7
7
  import { createProviderCommand } from "./commands/provider/index.js";
8
8
  import { createScheduleCommand } from "./commands/schedule/index.js";
9
+ import { createFleetCommand } from "./commands/fleet/index.js";
9
10
  import { createSpeechCommand } from "./commands/speech/index.js";
10
11
  import { createTerminalCommand } from "./commands/terminal/index.js";
11
12
  import { createWorktreeCommand } from "./commands/worktree/index.js";
@@ -102,6 +103,8 @@ export function createCli() {
102
103
  program.addCommand(createLoopCommand());
103
104
  // Schedule commands
104
105
  program.addCommand(createScheduleCommand());
106
+ // Fleet (Loops) commands
107
+ program.addCommand(createFleetCommand());
105
108
  // Permission commands
106
109
  program.addCommand(createPermitCommand());
107
110
  // Provider commands
@@ -0,0 +1,39 @@
1
+ import type { AskFleetDecisionInput } from "./types.js";
2
+ export interface AskRow {
3
+ decision: string;
4
+ status: string;
5
+ chosen: string;
6
+ resolvedBy: string;
7
+ }
8
+ export interface ParsedAsk {
9
+ ask: AskFleetDecisionInput;
10
+ wait: boolean;
11
+ pollMs: number;
12
+ }
13
+ /**
14
+ * Parse a duration like `45m`, `2h`, `90s` into milliseconds. Loops declare windows in
15
+ * human units; the protocol stores milliseconds.
16
+ */
17
+ export declare function parseDuration(input: string): number;
18
+ /**
19
+ * Parse one `--option` flag: `id:Label:effectKind:reversibility[:compensating action]`.
20
+ *
21
+ * The reversibility is REQUIRED rather than defaulted, because it is what bounds the
22
+ * posture: defaulting it would let an author quietly ship an irreversible act under an
23
+ * advisory posture, which is the one combination that must never reach a human.
24
+ */
25
+ export declare function parseOption(raw: string): AskFleetDecisionInput["options"][number];
26
+ export declare function parseAskOptions(name: string, options: {
27
+ kind?: string;
28
+ question?: string;
29
+ posture?: string;
30
+ option?: string[];
31
+ onExpiry?: string;
32
+ expiresIn?: string;
33
+ itemKey?: string;
34
+ runId?: string;
35
+ evidence?: string;
36
+ wait?: boolean;
37
+ pollMs?: string;
38
+ }): ParsedAsk;
39
+ //# sourceMappingURL=ask-options.d.ts.map
@@ -0,0 +1,88 @@
1
+ const KINDS = new Set(["gate-approval", "action-approval", "outcome-review"]);
2
+ const POSTURES = new Set(["blocking", "deferring", "advisory"]);
3
+ const EFFECT_KINDS = new Set(["act", "rehearse", "defer", "suppress"]);
4
+ const REVERSIBILITIES = new Set(["reversible", "compensable", "irreversible"]);
5
+ /**
6
+ * Parse a duration like `45m`, `2h`, `90s` into milliseconds. Loops declare windows in
7
+ * human units; the protocol stores milliseconds.
8
+ */
9
+ export function parseDuration(input) {
10
+ const match = /^(\d+)(ms|s|m|h|d)$/.exec(input.trim());
11
+ if (!match) {
12
+ throw new Error(`Invalid duration "${input}". Use forms like 90s, 45m, 2h, 1d.`);
13
+ }
14
+ const factors = {
15
+ ms: 1,
16
+ s: 1000,
17
+ m: 60000,
18
+ h: 3600000,
19
+ d: 86400000,
20
+ };
21
+ return Number(match[1]) * factors[match[2]];
22
+ }
23
+ /**
24
+ * Parse one `--option` flag: `id:Label:effectKind:reversibility[:compensating action]`.
25
+ *
26
+ * The reversibility is REQUIRED rather than defaulted, because it is what bounds the
27
+ * posture: defaulting it would let an author quietly ship an irreversible act under an
28
+ * advisory posture, which is the one combination that must never reach a human.
29
+ */
30
+ export function parseOption(raw) {
31
+ const parts = raw.split(":");
32
+ if (parts.length < 4) {
33
+ throw new Error(`Invalid --option "${raw}". Expected id:Label:effect:reversibility[:compensating action]`);
34
+ }
35
+ const [id, label, effectKind, reversibility, ...rest] = parts;
36
+ if (!EFFECT_KINDS.has(effectKind)) {
37
+ throw new Error(`Invalid effect "${effectKind}" in --option "${raw}"`);
38
+ }
39
+ if (!REVERSIBILITIES.has(reversibility)) {
40
+ throw new Error(`Invalid reversibility "${reversibility}" in --option "${raw}"`);
41
+ }
42
+ const compensatingAction = rest.join(":").trim();
43
+ return {
44
+ id: id.trim(),
45
+ label: label.trim(),
46
+ effect: compensatingAction
47
+ ? {
48
+ kind: effectKind,
49
+ reversibility: reversibility,
50
+ compensatingAction,
51
+ }
52
+ : { kind: effectKind, reversibility: reversibility },
53
+ };
54
+ }
55
+ export function parseAskOptions(name, options) {
56
+ if (!options.question?.trim()) {
57
+ throw new Error("--question is required");
58
+ }
59
+ const kind = options.kind ?? "action-approval";
60
+ if (!KINDS.has(kind)) {
61
+ throw new Error(`Invalid --kind "${kind}". One of: ${[...KINDS].join(", ")}`);
62
+ }
63
+ const posture = options.posture ?? "blocking";
64
+ if (!POSTURES.has(posture)) {
65
+ throw new Error(`Invalid --posture "${posture}". One of: ${[...POSTURES].join(", ")}`);
66
+ }
67
+ const parsedOptions = (options.option ?? []).map(parseOption);
68
+ if (parsedOptions.length === 0) {
69
+ throw new Error("At least one --option is required");
70
+ }
71
+ return {
72
+ ask: {
73
+ name,
74
+ kind: kind,
75
+ question: options.question.trim(),
76
+ posture: posture,
77
+ options: parsedOptions,
78
+ itemKey: options.itemKey,
79
+ runId: options.runId,
80
+ evidence: options.evidence,
81
+ onExpiry: options.onExpiry,
82
+ expiresInMs: options.expiresIn ? parseDuration(options.expiresIn) : undefined,
83
+ },
84
+ wait: options.wait ?? false,
85
+ pollMs: options.pollMs ? Number(options.pollMs) : 5000,
86
+ };
87
+ }
88
+ //# sourceMappingURL=ask-options.js.map
@@ -0,0 +1,29 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult, OutputSchema } from "../../output/index.js";
3
+ import { type FleetCommandOptions } from "./shared.js";
4
+ import { type AskRow } from "./ask-options.js";
5
+ export declare const askSchema: OutputSchema<AskRow>;
6
+ export interface AskCommandOptions extends FleetCommandOptions {
7
+ kind?: string;
8
+ question?: string;
9
+ posture?: string;
10
+ option?: string[];
11
+ onExpiry?: string;
12
+ expiresIn?: string;
13
+ itemKey?: string;
14
+ runId?: string;
15
+ evidence?: string;
16
+ wait?: boolean;
17
+ pollMs?: string;
18
+ }
19
+ /**
20
+ * Raise a decision from a loop, and optionally block until a person (or the clock)
21
+ * settles it.
22
+ *
23
+ * This is what makes the human-in-the-loop story reachable from a bash runner: the
24
+ * runner asks, waits, and branches on the chosen option id printed to stdout. Without
25
+ * `--wait` it returns immediately, which is the right shape for an advisory decision
26
+ * that has already acted.
27
+ */
28
+ export declare function runAskCommand(name: string, options: AskCommandOptions, _command: Command): Promise<ListResult<AskRow>>;
29
+ //# sourceMappingURL=ask.d.ts.map
@@ -0,0 +1,82 @@
1
+ import { connectFleetClient, toFleetCommandError } from "./shared.js";
2
+ import { parseAskOptions } from "./ask-options.js";
3
+ export const askSchema = {
4
+ idField: "decision",
5
+ columns: [
6
+ { header: "DECISION", field: "decision", width: 38 },
7
+ { header: "STATUS", field: "status", width: 10 },
8
+ { header: "CHOSEN", field: "chosen", width: 18 },
9
+ { header: "BY", field: "resolvedBy", width: 10 },
10
+ ],
11
+ };
12
+ /**
13
+ * Raise a decision from a loop, and optionally block until a person (or the clock)
14
+ * settles it.
15
+ *
16
+ * This is what makes the human-in-the-loop story reachable from a bash runner: the
17
+ * runner asks, waits, and branches on the chosen option id printed to stdout. Without
18
+ * `--wait` it returns immediately, which is the right shape for an advisory decision
19
+ * that has already acted.
20
+ */
21
+ export async function runAskCommand(name, options, _command) {
22
+ // Parse before connecting, and wrap the failure the same way the rest of the fleet
23
+ // commands do: a loop author who mistypes an option should get one readable line,
24
+ // not a stack trace.
25
+ let parsed;
26
+ try {
27
+ parsed = parseAskOptions(name, options);
28
+ }
29
+ catch (error) {
30
+ throw toFleetCommandError("FLEET_ASK_INVALID", "parse decision options", error);
31
+ }
32
+ const { client } = await connectFleetClient(options.host);
33
+ try {
34
+ const created = await client.fleetAsk(parsed.ask);
35
+ if (created.error) {
36
+ throw new Error(created.error);
37
+ }
38
+ const decision = created.decision;
39
+ if (!decision) {
40
+ throw new Error("Daemon did not return a decision");
41
+ }
42
+ if (!parsed.wait) {
43
+ return {
44
+ type: "list",
45
+ data: [{ decision: decision.id, status: "pending", chosen: "", resolvedBy: "" }],
46
+ schema: askSchema,
47
+ };
48
+ }
49
+ // Poll until it is settled. The daemon applies `onExpiry` on read, so a decision
50
+ // with a clock always terminates; a blocking decision with no clock waits, which is
51
+ // exactly what it declared.
52
+ for (;;) {
53
+ const payload = await client.fleetDecisions({ name, includeResolved: true });
54
+ if (payload.error) {
55
+ throw new Error(payload.error);
56
+ }
57
+ const current = payload.decisions.find((entry) => entry.id === decision.id);
58
+ if (current?.resolvedOptionId) {
59
+ return {
60
+ type: "list",
61
+ data: [
62
+ {
63
+ decision: current.id,
64
+ status: "resolved",
65
+ chosen: current.resolvedOptionId,
66
+ resolvedBy: current.resolvedBy?.kind ?? "",
67
+ },
68
+ ],
69
+ schema: askSchema,
70
+ };
71
+ }
72
+ await new Promise((resolve) => setTimeout(resolve, parsed.pollMs));
73
+ }
74
+ }
75
+ catch (error) {
76
+ throw toFleetCommandError("FLEET_ASK_FAILED", "raise loop decision", error);
77
+ }
78
+ finally {
79
+ await client.close().catch(() => { });
80
+ }
81
+ }
82
+ //# sourceMappingURL=ask.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function createFleetCommand(): Command;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { Command } from "commander";
2
+ import { withOutput } from "../../output/index.js";
3
+ import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
4
+ import { runLsCommand } from "./ls.js";
5
+ import { runInspectCommand } from "./inspect.js";
6
+ import { runRunCommand } from "./run.js";
7
+ import { runPauseCommand } from "./pause.js";
8
+ import { runResumeCommand } from "./resume.js";
9
+ import { runNewCommand } from "./new.js";
10
+ import { runAskCommand } from "./ask.js";
11
+ export function createFleetCommand() {
12
+ const fleet = new Command("fleet").description("View the super-repo automation loop fleet (Loops)");
13
+ addJsonAndDaemonHostOptions(fleet.command("ls").description("List all loops with status and findings")).action(withOutput(runLsCommand));
14
+ addJsonAndDaemonHostOptions(fleet
15
+ .command("inspect")
16
+ .description("Inspect a single loop by name")
17
+ .argument("<name>", "Loop name (for example: stage-gc)")).action(withOutput(runInspectCommand));
18
+ addJsonAndDaemonHostOptions(fleet
19
+ .command("run")
20
+ .description("Trigger a one-off dry-run of a loop now")
21
+ .argument("<name>", "Loop name")).action(withOutput(runRunCommand));
22
+ addJsonAndDaemonHostOptions(fleet
23
+ .command("pause")
24
+ .description("Pause a loop by commenting its cron line (backs up the crontab first)")
25
+ .argument("<name>", "Loop name")).action(withOutput(runPauseCommand));
26
+ addJsonAndDaemonHostOptions(fleet
27
+ .command("resume")
28
+ .description("Resume a paused loop by uncommenting its cron line")
29
+ .argument("<name>", "Loop name")).action(withOutput(runResumeCommand));
30
+ addJsonAndDaemonHostOptions(fleet
31
+ .command("new")
32
+ .description("Scaffold a new loop (prints the cron line to install)")
33
+ .argument("<name>", "Loop name (kebab-case)")
34
+ .option("--cron <expr>", "Cron cadence, e.g. '7 4 * * *'")
35
+ .option("--action <prompt>", "The LLM action prompt for fresh items")
36
+ .option("--description <text>", "Short description of the loop")
37
+ .option("--mode <live|dry-run>", "Initial mode (default: dry-run)")).action(withOutput(runNewCommand));
38
+ addJsonAndDaemonHostOptions(fleet
39
+ .command("ask")
40
+ .description("Raise a decision from a loop, optionally blocking until it is answered")
41
+ .argument("<name>", "Loop name")
42
+ .requiredOption("--question <text>", "The question a person has to answer")
43
+ .option("--option <spec...>", "id:Label:effect:reversibility[:compensating action] (repeatable)")
44
+ .option("--kind <kind>", "gate-approval | action-approval | outcome-review")
45
+ .option("--posture <posture>", "blocking | deferring | advisory (default: blocking)")
46
+ .option("--on-expiry <optionId>", "Option applied when the clock runs out")
47
+ .option("--expires-in <duration>", "Lifetime, e.g. 45m, 2h")
48
+ .option("--item-key <key>", "Ledger item this decision is about")
49
+ .option("--run-id <id>", "Run/tick this decision belongs to")
50
+ .option("--evidence <text>", "What the gate saw")
51
+ .option("--wait", "Block until the decision is resolved, then print the chosen option")
52
+ .option("--poll-ms <ms>", "Poll interval while waiting (default: 5000)")).action(withOutput(runAskCommand));
53
+ return fleet;
54
+ }
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult } from "../../output/index.js";
3
+ import { type FleetInspectRow } from "./schema.js";
4
+ import { type FleetCommandOptions } from "./shared.js";
5
+ export declare function runInspectCommand(name: string, options: FleetCommandOptions, _command: Command): Promise<ListResult<FleetInspectRow>>;
6
+ //# sourceMappingURL=inspect.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { createFleetInspectRows, createFleetInspectSchema, } from "./schema.js";
2
+ import { connectFleetClient, toFleetCommandError } from "./shared.js";
3
+ export async function runInspectCommand(name, options, _command) {
4
+ const { client } = await connectFleetClient(options.host);
5
+ try {
6
+ const payload = await client.fleetInspect({ name });
7
+ if (payload.error) {
8
+ throw new Error(payload.error);
9
+ }
10
+ if (!payload.loop) {
11
+ throw new Error(`Loop not found: ${name}`);
12
+ }
13
+ return {
14
+ type: "list",
15
+ data: createFleetInspectRows(payload.loop),
16
+ schema: createFleetInspectSchema(payload.loop),
17
+ };
18
+ }
19
+ catch (error) {
20
+ throw toFleetCommandError("FLEET_INSPECT_FAILED", "inspect loop", error);
21
+ }
22
+ finally {
23
+ await client.close().catch(() => { });
24
+ }
25
+ }
26
+ //# sourceMappingURL=inspect.js.map
@@ -0,0 +1,5 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult } from "../../output/index.js";
3
+ import { type FleetCommandOptions, type FleetRow } from "./shared.js";
4
+ export declare function runLsCommand(options: FleetCommandOptions, _command: Command): Promise<ListResult<FleetRow>>;
5
+ //# sourceMappingURL=ls.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { fleetSchema } from "./schema.js";
2
+ import { connectFleetClient, toFleetCommandError, toFleetRow, } from "./shared.js";
3
+ export async function runLsCommand(options, _command) {
4
+ const { client } = await connectFleetClient(options.host);
5
+ try {
6
+ const payload = await client.fleetList();
7
+ if (payload.error) {
8
+ throw new Error(payload.error);
9
+ }
10
+ return {
11
+ type: "list",
12
+ data: payload.loops.map(toFleetRow),
13
+ schema: fleetSchema,
14
+ };
15
+ }
16
+ catch (error) {
17
+ throw toFleetCommandError("FLEET_LIST_FAILED", "list loops", error);
18
+ }
19
+ finally {
20
+ await client.close().catch(() => { });
21
+ }
22
+ }
23
+ //# sourceMappingURL=ls.js.map
@@ -0,0 +1,13 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult } from "../../output/index.js";
3
+ import { type FleetInspectRow } from "./schema.js";
4
+ import { type FleetCommandOptions } from "./shared.js";
5
+ interface FleetNewOptions extends FleetCommandOptions {
6
+ cron?: string;
7
+ action?: string;
8
+ description?: string;
9
+ mode?: string;
10
+ }
11
+ export declare function runNewCommand(name: string, options: FleetNewOptions, _command: Command): Promise<ListResult<FleetInspectRow>>;
12
+ export {};
13
+ //# sourceMappingURL=new.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { createFleetInspectRows, createFleetInspectSchema, } from "./schema.js";
2
+ import { connectFleetClient, toFleetCommandError } from "./shared.js";
3
+ export async function runNewCommand(name, options, _command) {
4
+ if (!options.cron) {
5
+ throw { code: "MISSING_CRON", message: "--cron is required" };
6
+ }
7
+ if (!options.action) {
8
+ throw {
9
+ code: "MISSING_ACTION",
10
+ message: "--action is required",
11
+ };
12
+ }
13
+ const mode = options.mode === "live" ? "live" : "dry-run";
14
+ const { client } = await connectFleetClient(options.host);
15
+ try {
16
+ const payload = await client.fleetCreate({
17
+ name,
18
+ description: options.description ?? name,
19
+ cron: options.cron,
20
+ action: options.action,
21
+ mode,
22
+ });
23
+ if (payload.error) {
24
+ throw new Error(payload.error);
25
+ }
26
+ if (!payload.loop) {
27
+ throw new Error("Loop was not created");
28
+ }
29
+ const rows = createFleetInspectRows(payload.loop);
30
+ rows.push({ key: "CronLine", value: payload.cronLine ?? "(none)" });
31
+ return {
32
+ type: "list",
33
+ data: rows,
34
+ schema: createFleetInspectSchema(payload.loop),
35
+ };
36
+ }
37
+ catch (error) {
38
+ throw toFleetCommandError("FLEET_CREATE_FAILED", "create loop", error);
39
+ }
40
+ finally {
41
+ await client.close().catch(() => { });
42
+ }
43
+ }
44
+ //# sourceMappingURL=new.js.map
@@ -0,0 +1,5 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult } from "../../output/index.js";
3
+ import { type FleetCommandOptions, type FleetRow } from "./shared.js";
4
+ export declare function runPauseCommand(name: string, options: FleetCommandOptions, _command: Command): Promise<ListResult<FleetRow>>;
5
+ //# sourceMappingURL=pause.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { fleetSchema } from "./schema.js";
2
+ import { connectFleetClient, toFleetCommandError, toFleetRow, } from "./shared.js";
3
+ export async function runPauseCommand(name, options, _command) {
4
+ const { client } = await connectFleetClient(options.host);
5
+ try {
6
+ const payload = await client.fleetPause({ name });
7
+ if (payload.error) {
8
+ throw new Error(payload.error);
9
+ }
10
+ if (!payload.loop) {
11
+ throw new Error(`Loop not found: ${name}`);
12
+ }
13
+ return { type: "list", data: [toFleetRow(payload.loop)], schema: fleetSchema };
14
+ }
15
+ catch (error) {
16
+ throw toFleetCommandError("FLEET_PAUSE_FAILED", "pause loop", error);
17
+ }
18
+ finally {
19
+ await client.close().catch(() => { });
20
+ }
21
+ }
22
+ //# sourceMappingURL=pause.js.map
@@ -0,0 +1,5 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult } from "../../output/index.js";
3
+ import { type FleetCommandOptions, type FleetRow } from "./shared.js";
4
+ export declare function runResumeCommand(name: string, options: FleetCommandOptions, _command: Command): Promise<ListResult<FleetRow>>;
5
+ //# sourceMappingURL=resume.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { fleetSchema } from "./schema.js";
2
+ import { connectFleetClient, toFleetCommandError, toFleetRow, } from "./shared.js";
3
+ export async function runResumeCommand(name, options, _command) {
4
+ const { client } = await connectFleetClient(options.host);
5
+ try {
6
+ const payload = await client.fleetResume({ name });
7
+ if (payload.error) {
8
+ throw new Error(payload.error);
9
+ }
10
+ if (!payload.loop) {
11
+ throw new Error(`Loop not found: ${name}`);
12
+ }
13
+ return { type: "list", data: [toFleetRow(payload.loop)], schema: fleetSchema };
14
+ }
15
+ catch (error) {
16
+ throw toFleetCommandError("FLEET_RESUME_FAILED", "resume loop", error);
17
+ }
18
+ finally {
19
+ await client.close().catch(() => { });
20
+ }
21
+ }
22
+ //# sourceMappingURL=resume.js.map
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ import type { ListResult } from "../../output/index.js";
3
+ import { type FleetInspectRow } from "./schema.js";
4
+ import { type FleetCommandOptions } from "./shared.js";
5
+ export declare function runRunCommand(name: string, options: FleetCommandOptions, _command: Command): Promise<ListResult<FleetInspectRow>>;
6
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { createFleetInspectRows, createFleetInspectSchema, } from "./schema.js";
2
+ import { connectFleetClient, toFleetCommandError } from "./shared.js";
3
+ export async function runRunCommand(name, options, _command) {
4
+ const { client } = await connectFleetClient(options.host);
5
+ try {
6
+ const payload = await client.fleetRunNow({ name });
7
+ if (payload.error) {
8
+ throw new Error(payload.error);
9
+ }
10
+ if (!payload.loop) {
11
+ throw new Error(`Loop not found: ${name}`);
12
+ }
13
+ return {
14
+ type: "list",
15
+ data: createFleetInspectRows(payload.loop),
16
+ schema: createFleetInspectSchema(payload.loop),
17
+ };
18
+ }
19
+ catch (error) {
20
+ throw toFleetCommandError("FLEET_RUN_FAILED", "run loop", error);
21
+ }
22
+ finally {
23
+ await client.close().catch(() => { });
24
+ }
25
+ }
26
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,11 @@
1
+ import type { OutputSchema } from "../../output/index.js";
2
+ import type { FleetRow } from "./shared.js";
3
+ import type { StoredFleetLoop } from "./types.js";
4
+ export declare const fleetSchema: OutputSchema<FleetRow>;
5
+ export interface FleetInspectRow {
6
+ key: string;
7
+ value: string;
8
+ }
9
+ export declare function createFleetInspectSchema(record: StoredFleetLoop): OutputSchema<FleetInspectRow>;
10
+ export declare function createFleetInspectRows(loop: StoredFleetLoop): FleetInspectRow[];
11
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,42 @@
1
+ export const fleetSchema = {
2
+ idField: "name",
3
+ columns: [
4
+ { header: "LOOP", field: "name", width: 26 },
5
+ { header: "STATUS", field: "status", width: 12 },
6
+ { header: "SCHEDULE", field: "schedule", width: 18 },
7
+ { header: "MODE", field: "mode", width: 10 },
8
+ { header: "FINDINGS", field: "findings", width: 9 },
9
+ { header: "LAST RUN", field: "lastRun", width: 24 },
10
+ ],
11
+ };
12
+ export function createFleetInspectSchema(record) {
13
+ return {
14
+ idField: "key",
15
+ columns: [
16
+ { header: "KEY", field: "key", width: 16 },
17
+ { header: "VALUE", field: "value", width: 80 },
18
+ ],
19
+ serialize: () => record,
20
+ };
21
+ }
22
+ export function createFleetInspectRows(loop) {
23
+ const findings = loop.openFindings
24
+ .slice(0, 8)
25
+ .map((f) => `${f.key}${f.outcome ? ` (${f.outcome})` : ""}`)
26
+ .join(", ");
27
+ return [
28
+ { key: "Name", value: loop.name },
29
+ { key: "Status", value: loop.status },
30
+ { key: "Scheduled", value: loop.scheduled ? "yes" : "no" },
31
+ { key: "Cadence", value: loop.cadence ?? "null" },
32
+ { key: "Mode", value: loop.mode },
33
+ { key: "LastRunAt", value: loop.lastRunAt ?? "null" },
34
+ { key: "Description", value: loop.description ?? "null" },
35
+ {
36
+ key: "Findings",
37
+ value: `${loop.findingCount}${findings ? `: ${findings}` : ""}`,
38
+ },
39
+ { key: "Dir", value: loop.dir },
40
+ ];
41
+ }
42
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,20 @@
1
+ import type { CommandError, CommandOptions } from "../../output/index.js";
2
+ import type { FleetDaemonClient, FleetLoopSummary, StoredFleetLoop } from "./types.js";
3
+ export interface FleetCommandOptions extends CommandOptions {
4
+ host?: string;
5
+ }
6
+ export declare function connectFleetClient(host: string | undefined): Promise<{
7
+ client: FleetDaemonClient;
8
+ host: string;
9
+ }>;
10
+ export declare function toFleetCommandError(code: string, action: string, error: unknown): CommandError;
11
+ export interface FleetRow {
12
+ name: string;
13
+ status: string;
14
+ schedule: string;
15
+ mode: string;
16
+ findings: string;
17
+ lastRun: string;
18
+ }
19
+ export declare function toFleetRow(loop: FleetLoopSummary | StoredFleetLoop): FleetRow;
20
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
2
+ export async function connectFleetClient(host) {
3
+ const resolvedHost = getDaemonHost({ host });
4
+ try {
5
+ const client = (await connectToDaemon({
6
+ host,
7
+ }));
8
+ return { client, host: resolvedHost };
9
+ }
10
+ catch (error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ throw {
13
+ code: "DAEMON_NOT_RUNNING",
14
+ message: `Cannot connect to daemon at ${resolvedHost}: ${message}`,
15
+ details: "Start the daemon with: paseo daemon start",
16
+ };
17
+ }
18
+ }
19
+ export function toFleetCommandError(code, action, error) {
20
+ if (error && typeof error === "object" && "code" in error) {
21
+ return error;
22
+ }
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ return { code, message: `Failed to ${action}: ${message}` };
25
+ }
26
+ export function toFleetRow(loop) {
27
+ return {
28
+ name: loop.name,
29
+ status: loop.status,
30
+ schedule: loop.scheduled ? (loop.cadence ?? "scheduled") : "unscheduled",
31
+ mode: loop.mode,
32
+ findings: String(loop.findingCount),
33
+ lastRun: loop.lastRunAt ?? "never",
34
+ };
35
+ }
36
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1,82 @@
1
+ import type { LoopDecision, LoopDecisionKind, LoopDecisionOption, LoopDecisionPosture } from "@hyperdrive.bot/paseo-protocol/fleet/decisions";
2
+ import type { FleetLoopSummary, StoredFleetLoop } from "@hyperdrive.bot/paseo-protocol/fleet/types";
3
+ export type { FleetLoopSummary, StoredFleetLoop };
4
+ export interface FleetListPayload {
5
+ requestId: string;
6
+ loops: FleetLoopSummary[];
7
+ error: string | null;
8
+ }
9
+ export interface FleetInspectPayload {
10
+ requestId: string;
11
+ loop: StoredFleetLoop | null;
12
+ error: string | null;
13
+ }
14
+ export interface FleetRunNowPayload {
15
+ requestId: string;
16
+ loop: StoredFleetLoop | null;
17
+ started: boolean;
18
+ error: string | null;
19
+ }
20
+ export interface FleetControlPayload {
21
+ requestId: string;
22
+ loop: FleetLoopSummary | null;
23
+ error: string | null;
24
+ }
25
+ export interface FleetCreatePayload {
26
+ requestId: string;
27
+ loop: StoredFleetLoop | null;
28
+ cronLine: string | null;
29
+ error: string | null;
30
+ }
31
+ export interface CreateFleetLoopInput {
32
+ name: string;
33
+ description: string;
34
+ cron: string;
35
+ action: string;
36
+ mode?: "live" | "dry-run";
37
+ rails?: string;
38
+ allowed?: string;
39
+ }
40
+ export interface FleetDaemonClient {
41
+ fleetList(): Promise<FleetListPayload>;
42
+ fleetInspect(input: {
43
+ name: string;
44
+ }): Promise<FleetInspectPayload>;
45
+ fleetRunNow(input: {
46
+ name: string;
47
+ }): Promise<FleetRunNowPayload>;
48
+ fleetPause(input: {
49
+ name: string;
50
+ }): Promise<FleetControlPayload>;
51
+ fleetResume(input: {
52
+ name: string;
53
+ }): Promise<FleetControlPayload>;
54
+ fleetCreate(input: CreateFleetLoopInput): Promise<FleetCreatePayload>;
55
+ fleetAsk(input: AskFleetDecisionInput): Promise<FleetAskPayload>;
56
+ fleetDecisions(input: {
57
+ name?: string;
58
+ includeResolved?: boolean;
59
+ }): Promise<FleetDecisionsPayload>;
60
+ close(): Promise<void>;
61
+ }
62
+ export interface AskFleetDecisionInput {
63
+ name: string;
64
+ kind: LoopDecisionKind;
65
+ question: string;
66
+ posture: LoopDecisionPosture;
67
+ options: LoopDecisionOption[];
68
+ runId?: string;
69
+ itemKey?: string;
70
+ evidence?: string;
71
+ onExpiry?: string;
72
+ expiresInMs?: number;
73
+ }
74
+ export interface FleetAskPayload {
75
+ decision: LoopDecision | null;
76
+ error: string | null;
77
+ }
78
+ export interface FleetDecisionsPayload {
79
+ decisions: LoopDecision[];
80
+ error: string | null;
81
+ }
82
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,23 @@
1
+ import type { Command } from "commander";
2
+ import type { CommandOptions, SingleResult, OutputSchema } from "../../output/index.js";
3
+ export interface ProviderAddRow {
4
+ provider: string;
5
+ label: string;
6
+ extends: string;
7
+ models: string;
8
+ reloaded: string;
9
+ }
10
+ export declare const providerAddSchema: OutputSchema<ProviderAddRow>;
11
+ export type ProviderAddResult = SingleResult<ProviderAddRow>;
12
+ export interface ProviderAddOptions extends CommandOptions {
13
+ host?: string;
14
+ extends?: string;
15
+ label?: string;
16
+ description?: string;
17
+ env?: string[];
18
+ model?: string[];
19
+ disallowedTool?: string[];
20
+ force?: boolean;
21
+ }
22
+ export declare function runAddCommand(providerId: string, options: ProviderAddOptions, _command: Command): Promise<ProviderAddResult>;
23
+ //# sourceMappingURL=add.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { loadPersistedConfig, resolvePaseoHome, savePersistedConfig, } from "@hyperdrive.bot/paseo-server";
2
+ import { ProviderOverridesSchema } from "@hyperdrive.bot/paseo-protocol/provider-config";
3
+ import { tryConnectToDaemon } from "../../utils/client.js";
4
+ export const providerAddSchema = {
5
+ idField: "provider",
6
+ columns: [
7
+ { header: "PROVIDER", field: "provider", width: 20 },
8
+ { header: "LABEL", field: "label", width: 24 },
9
+ { header: "EXTENDS", field: "extends", width: 12 },
10
+ { header: "MODELS", field: "models", width: 30 },
11
+ {
12
+ header: "RELOADED",
13
+ field: "reloaded",
14
+ width: 24,
15
+ color: (value) => (value === "yes" ? "green" : "yellow"),
16
+ },
17
+ ],
18
+ };
19
+ /** `--env KEY=VALUE` (repeatable). Values may contain `=`; only the first splits. */
20
+ function parseEnvPairs(pairs) {
21
+ if (!pairs || pairs.length === 0)
22
+ return undefined;
23
+ const env = {};
24
+ for (const pair of pairs) {
25
+ const separator = pair.indexOf("=");
26
+ if (separator <= 0) {
27
+ throw new Error(`Invalid --env "${pair}". Expected KEY=VALUE.`);
28
+ }
29
+ env[pair.slice(0, separator)] = pair.slice(separator + 1);
30
+ }
31
+ return env;
32
+ }
33
+ /** `--model id` or `--model id:Label` or `--model id:Label:default` (repeatable). */
34
+ function parseModels(models) {
35
+ if (!models || models.length === 0)
36
+ return undefined;
37
+ return models.map((raw) => {
38
+ const [id, label, flag] = raw.split(":");
39
+ if (!id) {
40
+ throw new Error(`Invalid --model "${raw}". Expected id[:Label[:default]].`);
41
+ }
42
+ return {
43
+ id,
44
+ label: label && label.length > 0 ? label : id,
45
+ ...(flag === "default" ? { isDefault: true } : {}),
46
+ };
47
+ });
48
+ }
49
+ export async function runAddCommand(providerId, options, _command) {
50
+ if (!options.extends) {
51
+ throw new Error(`--extends is required. Choose a built-in provider (claude, codex, copilot, opencode, pi) or "acp".`);
52
+ }
53
+ const label = options.label ?? providerId;
54
+ const paseoHome = resolvePaseoHome();
55
+ const persisted = loadPersistedConfig(paseoHome);
56
+ const existingProviders = (persisted.agents?.providers ?? {});
57
+ if (existingProviders[providerId] && !options.force) {
58
+ throw new Error(`Provider "${providerId}" already exists in config.json. Pass --force to overwrite.`);
59
+ }
60
+ const entry = {
61
+ extends: options.extends,
62
+ label,
63
+ ...(options.description ? { description: options.description } : {}),
64
+ ...(parseEnvPairs(options.env) ? { env: parseEnvPairs(options.env) } : {}),
65
+ ...(parseModels(options.model) ? { models: parseModels(options.model) } : {}),
66
+ ...(options.disallowedTool?.length ? { disallowedTools: options.disallowedTool } : {}),
67
+ };
68
+ const nextProviders = { ...existingProviders, [providerId]: entry };
69
+ // Validate the WHOLE map, not just the new entry: ProviderOverridesSchema's
70
+ // superRefine enforces the id pattern and the extends/label requirement for
71
+ // custom providers, and we would rather fail here than write a config.json
72
+ // the daemon will reject on reload.
73
+ const parsed = ProviderOverridesSchema.safeParse(nextProviders);
74
+ if (!parsed.success) {
75
+ throw new Error(`Invalid provider config: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
76
+ }
77
+ savePersistedConfig(paseoHome, {
78
+ ...persisted,
79
+ agents: { ...persisted.agents, providers: parsed.data },
80
+ });
81
+ // Hot-reload if a daemon is up; if not, the file write still stands and the
82
+ // provider is picked up on next start. Say which happened — never imply the
83
+ // running daemon saw it when there was no daemon to see it.
84
+ let reloaded = "no daemon running";
85
+ const client = await tryConnectToDaemon({ host: options.host });
86
+ if (client) {
87
+ try {
88
+ const result = await client.reloadProviderConfig();
89
+ reloaded =
90
+ result.added.includes(providerId) || result.updated.includes(providerId)
91
+ ? "yes"
92
+ : "daemon did not pick it up";
93
+ }
94
+ finally {
95
+ await client.close().catch(() => { });
96
+ }
97
+ }
98
+ return {
99
+ type: "single",
100
+ data: {
101
+ provider: providerId,
102
+ label,
103
+ extends: options.extends,
104
+ models: (parseModels(options.model) ?? []).map((m) => m.id).join(", ") || "—",
105
+ reloaded,
106
+ },
107
+ schema: providerAddSchema,
108
+ };
109
+ }
110
+ //# sourceMappingURL=add.js.map
@@ -1,6 +1,8 @@
1
1
  import { Command } from "commander";
2
+ import { runAddCommand } from "./add.js";
2
3
  import { runLsCommand } from "./ls.js";
3
4
  import { runModelsCommand } from "./models.js";
5
+ import { runReloadCommand } from "./reload.js";
4
6
  import { withOutput } from "../../output/index.js";
5
7
  import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
6
8
  export function createProviderCommand() {
@@ -11,6 +13,25 @@ export function createProviderCommand() {
11
13
  .description("List models for a provider")
12
14
  .argument("<provider>", "Provider name (claude, codex, opencode)")
13
15
  .option("--thinking", "Include thinking option IDs for each model")).action(withOutput(runModelsCommand));
16
+ addJsonAndDaemonHostOptions(provider
17
+ .command("reload")
18
+ .description("Re-read providers from config.json into the running daemon (no restart)")
19
+ .option("--all", "Also list providers whose config did not change")).action(withOutput(runReloadCommand));
20
+ addJsonAndDaemonHostOptions(provider
21
+ .command("add")
22
+ .description("Add a custom provider to config.json and load it without a restart")
23
+ .argument("<id>", "Provider id (lowercase, e.g. zai)")
24
+ .requiredOption("--extends <base>", "Built-in to inherit from: claude, codex, copilot, opencode, pi, or acp")
25
+ .option("--label <label>", "Display name (defaults to the id)")
26
+ .option("--description <text>", "Description shown in the provider list")
27
+ .option("--env <KEY=VALUE>", "Environment variable (repeatable)", collect, [])
28
+ .option("--model <id[:Label[:default]]>", "Model definition (repeatable)", collect, [])
29
+ .option("--disallowed-tool <tool>", "Tool to disallow (repeatable)", collect, [])
30
+ .option("--force", "Overwrite an existing provider entry")).action(withOutput(runAddCommand));
14
31
  return provider;
15
32
  }
33
+ /** commander repeatable-option accumulator. */
34
+ function collect(value, previous) {
35
+ return [...previous, value];
36
+ }
16
37
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ import type { Command } from "commander";
2
+ import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
3
+ /**
4
+ * One row per provider whose config.json entry changed. `unchanged` providers
5
+ * are omitted from the table by default — a reload that read back an identical
6
+ * file should look like a no-op, not a wall of rows.
7
+ */
8
+ export interface ProviderReloadRow {
9
+ provider: string;
10
+ change: "added" | "updated" | "removed" | "unchanged";
11
+ label: string;
12
+ enabled: string;
13
+ }
14
+ export declare const providerReloadSchema: OutputSchema<ProviderReloadRow>;
15
+ export type ProviderReloadResult = ListResult<ProviderReloadRow>;
16
+ export interface ProviderReloadOptions extends CommandOptions {
17
+ host?: string;
18
+ all?: boolean;
19
+ }
20
+ export declare function runReloadCommand(options: ProviderReloadOptions, _command: Command): Promise<ProviderReloadResult>;
21
+ //# sourceMappingURL=reload.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { connectToDaemon } from "../../utils/client.js";
2
+ export const providerReloadSchema = {
3
+ idField: "provider",
4
+ columns: [
5
+ { header: "PROVIDER", field: "provider", width: 20 },
6
+ {
7
+ header: "CHANGE",
8
+ field: "change",
9
+ width: 10,
10
+ color: (value) => {
11
+ if (value === "added")
12
+ return "green";
13
+ if (value === "removed")
14
+ return "red";
15
+ if (value === "updated")
16
+ return "yellow";
17
+ return undefined;
18
+ },
19
+ },
20
+ { header: "LABEL", field: "label", width: 24 },
21
+ { header: "ENABLED", field: "enabled", width: 10 },
22
+ ],
23
+ };
24
+ export async function runReloadCommand(options, _command) {
25
+ // Deliberately connectToDaemon, not tryConnectToDaemon: a reload with no
26
+ // daemon running is a no-op the user must hear about, not a silent success.
27
+ const client = await connectToDaemon({ host: options.host });
28
+ try {
29
+ const result = await client.reloadProviderConfig();
30
+ const labels = new Map(result.providers.map((entry) => [entry.provider, entry]));
31
+ const rows = [];
32
+ const push = (provider, change) => {
33
+ const entry = labels.get(provider);
34
+ // A removed provider is gone from the registry, so it has no label or
35
+ // enabled state left to report.
36
+ const removed = change === "removed";
37
+ let enabled = "—";
38
+ if (!removed) {
39
+ enabled = entry?.enabled === false ? "Disabled" : "Enabled";
40
+ }
41
+ rows.push({
42
+ provider,
43
+ change,
44
+ label: entry?.label ?? (removed ? "—" : provider),
45
+ enabled,
46
+ });
47
+ };
48
+ for (const provider of result.added)
49
+ push(provider, "added");
50
+ for (const provider of result.updated)
51
+ push(provider, "updated");
52
+ for (const provider of result.removed)
53
+ push(provider, "removed");
54
+ if (options.all) {
55
+ for (const provider of result.unchanged)
56
+ push(provider, "unchanged");
57
+ }
58
+ return { type: "list", data: rows, schema: providerReloadSchema };
59
+ }
60
+ finally {
61
+ await client.close().catch(() => { });
62
+ }
63
+ }
64
+ //# sourceMappingURL=reload.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/paseo-cli",
3
- "version": "0.3.34",
3
+ "version": "0.3.36",
4
4
  "description": "Paseo CLI - control your AI coding agents from the command line",
5
5
  "bin": {
6
6
  "paseo": "bin/paseo"
@@ -27,9 +27,9 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.0.0",
30
- "@hyperdrive.bot/paseo-client": "0.3.34",
31
- "@hyperdrive.bot/paseo-protocol": "0.3.34",
32
- "@hyperdrive.bot/paseo-server": "0.3.34",
30
+ "@hyperdrive.bot/paseo-client": "0.3.36",
31
+ "@hyperdrive.bot/paseo-protocol": "0.3.36",
32
+ "@hyperdrive.bot/paseo-server": "0.3.36",
33
33
  "chalk": "^5.3.0",
34
34
  "commander": "^12.0.0",
35
35
  "mime-types": "^2.1.35",