@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 +3 -0
- package/dist/commands/fleet/ask-options.d.ts +39 -0
- package/dist/commands/fleet/ask-options.js +88 -0
- package/dist/commands/fleet/ask.d.ts +29 -0
- package/dist/commands/fleet/ask.js +82 -0
- package/dist/commands/fleet/index.d.ts +3 -0
- package/dist/commands/fleet/index.js +55 -0
- package/dist/commands/fleet/inspect.d.ts +6 -0
- package/dist/commands/fleet/inspect.js +26 -0
- package/dist/commands/fleet/ls.d.ts +5 -0
- package/dist/commands/fleet/ls.js +23 -0
- package/dist/commands/fleet/new.d.ts +13 -0
- package/dist/commands/fleet/new.js +44 -0
- package/dist/commands/fleet/pause.d.ts +5 -0
- package/dist/commands/fleet/pause.js +22 -0
- package/dist/commands/fleet/resume.d.ts +5 -0
- package/dist/commands/fleet/resume.js +22 -0
- package/dist/commands/fleet/run.d.ts +6 -0
- package/dist/commands/fleet/run.js +26 -0
- package/dist/commands/fleet/schema.d.ts +11 -0
- package/dist/commands/fleet/schema.js +42 -0
- package/dist/commands/fleet/shared.d.ts +20 -0
- package/dist/commands/fleet/shared.js +36 -0
- package/dist/commands/fleet/types.d.ts +82 -0
- package/dist/commands/fleet/types.js +2 -0
- package/dist/commands/provider/add.d.ts +23 -0
- package/dist/commands/provider/add.js +110 -0
- package/dist/commands/provider/index.js +21 -0
- package/dist/commands/provider/reload.d.ts +21 -0
- package/dist/commands/provider/reload.js +64 -0
- package/package.json +4 -4
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,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,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.
|
|
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.
|
|
31
|
-
"@hyperdrive.bot/paseo-protocol": "0.3.
|
|
32
|
-
"@hyperdrive.bot/paseo-server": "0.3.
|
|
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",
|