@otto-code/cli 0.7.4 → 0.7.6
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 +25 -2
- package/dist/commands/agent/delete.js +1 -1
- package/dist/commands/agent/detach.d.ts +9 -0
- package/dist/commands/agent/detach.js +38 -0
- package/dist/commands/agent/index.js +9 -1
- package/dist/commands/agent/open.d.ts +11 -0
- package/dist/commands/agent/open.js +61 -0
- package/dist/commands/agent/run.d.ts +35 -0
- package/dist/commands/agent/run.js +142 -49
- package/dist/commands/agent/update.d.ts +33 -0
- package/dist/commands/agent/update.js +72 -25
- package/dist/commands/clone.d.ts +17 -0
- package/dist/commands/clone.js +65 -0
- package/dist/commands/daemon/local-daemon.d.ts +15 -1
- package/dist/commands/daemon/local-daemon.js +12 -5
- package/dist/commands/daemon/pair.js +6 -2
- package/dist/commands/heartbeat/index.d.ts +3 -0
- package/dist/commands/heartbeat/index.js +139 -0
- package/dist/commands/hub/cloud-device-authorization.d.ts +45 -0
- package/dist/commands/hub/cloud-device-authorization.js +92 -0
- package/dist/commands/hub/device-authorization.d.ts +37 -0
- package/dist/commands/hub/device-authorization.js +87 -0
- package/dist/commands/hub/index.d.ts +30 -0
- package/dist/commands/hub/index.js +85 -0
- package/dist/commands/hub-disabled.d.ts +22 -0
- package/dist/commands/hub-disabled.js +34 -0
- package/dist/commands/onboard.js +6 -2
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +22 -17
- package/dist/commands/schedule/create.d.ts +1 -0
- package/dist/commands/schedule/create.js +1 -0
- package/dist/commands/schedule/index.js +6 -6
- package/dist/commands/schedule/inspect.js +3 -0
- package/dist/commands/schedule/logs.js +2 -1
- package/dist/commands/schedule/ls.js +3 -1
- package/dist/commands/schedule/pause.js +2 -1
- package/dist/commands/schedule/resume.js +2 -1
- package/dist/commands/schedule/run-once.js +2 -1
- package/dist/commands/schedule/shared.d.ts +3 -0
- package/dist/commands/schedule/shared.js +35 -23
- package/dist/commands/schedule/update.js +2 -1
- package/dist/commands/script/index.d.ts +3 -0
- package/dist/commands/script/index.js +19 -0
- package/dist/commands/script/ls.d.ts +6 -0
- package/dist/commands/script/ls.js +20 -0
- package/dist/commands/script/schema.d.ts +5 -0
- package/dist/commands/script/schema.js +13 -0
- package/dist/commands/script/shared.d.ts +11 -0
- package/dist/commands/script/shared.js +59 -0
- package/dist/commands/script/start.d.ts +6 -0
- package/dist/commands/script/start.js +23 -0
- package/dist/commands/script/stop.d.ts +6 -0
- package/dist/commands/script/stop.js +23 -0
- package/dist/commands/workspace/archive.d.ts +12 -0
- package/dist/commands/workspace/archive.js +41 -0
- package/dist/commands/workspace/create.d.ts +49 -0
- package/dist/commands/workspace/create.js +114 -0
- package/dist/commands/workspace/index.d.ts +3 -0
- package/dist/commands/workspace/index.js +30 -0
- package/dist/commands/workspace/ls.d.ts +7 -0
- package/dist/commands/workspace/ls.js +28 -0
- package/dist/commands/workspace/shared.d.ts +12 -0
- package/dist/commands/workspace/shared.js +20 -0
- package/dist/output/pairing.d.ts +8 -0
- package/dist/output/pairing.js +24 -0
- package/dist/utils/client.d.ts +9 -0
- package/dist/utils/client.js +9 -0
- package/dist/utils/duration.d.ts +1 -1
- package/dist/utils/duration.js +8 -7
- package/package.json +10 -7
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { CloudDeviceAuthorizationClient, } from "./cloud-device-authorization.js";
|
|
4
|
+
export class SystemBrowser {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.hostPlatform = options.hostPlatform ?? platform();
|
|
7
|
+
this.launch = options.launch ?? launchDetached;
|
|
8
|
+
}
|
|
9
|
+
async open(url) {
|
|
10
|
+
if (this.hostPlatform === "win32") {
|
|
11
|
+
await this.launch("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
await this.launch(this.hostPlatform === "darwin" ? "open" : "xdg-open", [url]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class DeviceAuthorizationWorkflow {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
async authorize(hubUrl, displayName) {
|
|
22
|
+
const authorization = await this.options.cloud.start(hubUrl, displayName);
|
|
23
|
+
this.options.reporter.instructions(authorization.verificationUri, authorization.userCode);
|
|
24
|
+
if (this.options.openBrowser !== false) {
|
|
25
|
+
await this.options.browser.open(authorization.verificationUriComplete).catch(() => undefined);
|
|
26
|
+
}
|
|
27
|
+
let interval = authorization.interval;
|
|
28
|
+
const expiresAt = Date.parse(authorization.expiresAt);
|
|
29
|
+
while (true) {
|
|
30
|
+
const remaining = expiresAt - this.options.waiter.now();
|
|
31
|
+
if (remaining <= 0)
|
|
32
|
+
throw new Error("Daemon registration expired");
|
|
33
|
+
await this.options.waiter.wait(Math.min(interval * 1000, remaining));
|
|
34
|
+
if (this.options.waiter.now() >= expiresAt)
|
|
35
|
+
throw new Error("Daemon registration expired");
|
|
36
|
+
const pollLifetime = expiresAt - this.options.waiter.now();
|
|
37
|
+
if (pollLifetime <= 0)
|
|
38
|
+
throw new Error("Daemon registration expired");
|
|
39
|
+
const outcome = await this.options.cloud.poll(hubUrl, authorization.deviceCode, pollLifetime);
|
|
40
|
+
if (this.options.waiter.now() >= expiresAt)
|
|
41
|
+
throw new Error("Daemon registration expired");
|
|
42
|
+
if (outcome.status === "retry_later")
|
|
43
|
+
continue;
|
|
44
|
+
interval = outcome.interval;
|
|
45
|
+
if (outcome.status === "approved")
|
|
46
|
+
return outcome.enrollmentToken;
|
|
47
|
+
if (outcome.status === "denied")
|
|
48
|
+
throw new Error("Daemon registration was denied");
|
|
49
|
+
if (outcome.status === "expired")
|
|
50
|
+
throw new Error("Daemon registration expired");
|
|
51
|
+
if (outcome.status === "enrolled") {
|
|
52
|
+
throw new Error("Daemon registration was already used");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function createDeviceAuthorizationWorkflow() {
|
|
58
|
+
return new DeviceAuthorizationWorkflow({
|
|
59
|
+
cloud: new CloudDeviceAuthorizationClient(),
|
|
60
|
+
waiter: {
|
|
61
|
+
wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
62
|
+
now: Date.now,
|
|
63
|
+
},
|
|
64
|
+
browser: new SystemBrowser(),
|
|
65
|
+
reporter: {
|
|
66
|
+
instructions(verificationUri, userCode) {
|
|
67
|
+
process.stderr.write(`Open ${verificationUri} and enter code ${userCode}\n`);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
openBrowser: process.stderr.isTTY === true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function launchDetached(command, args) {
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
const child = spawn(command, args, {
|
|
76
|
+
detached: true,
|
|
77
|
+
shell: false,
|
|
78
|
+
stdio: "ignore",
|
|
79
|
+
});
|
|
80
|
+
child.once("spawn", () => {
|
|
81
|
+
child.unref();
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
child.once("error", reject);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=device-authorization.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
interface HubCommandClient {
|
|
3
|
+
connectHub(url: string, token: string): Promise<{
|
|
4
|
+
status: HubStatus;
|
|
5
|
+
}>;
|
|
6
|
+
getHubStatus(): Promise<{
|
|
7
|
+
status: HubStatus;
|
|
8
|
+
}>;
|
|
9
|
+
disconnectHub(force: boolean): Promise<{
|
|
10
|
+
status: HubStatus;
|
|
11
|
+
warning?: string;
|
|
12
|
+
}>;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
interface HubStatus {
|
|
16
|
+
state: string;
|
|
17
|
+
daemonId: string | null;
|
|
18
|
+
hubOrigin: string | null;
|
|
19
|
+
scopes: string[];
|
|
20
|
+
connectedAt: string | null;
|
|
21
|
+
lastError: string | null;
|
|
22
|
+
}
|
|
23
|
+
interface HubCommandEnvironment {
|
|
24
|
+
connect(host: string | undefined): Promise<HubCommandClient>;
|
|
25
|
+
authorize(url: string, displayName: string): Promise<string>;
|
|
26
|
+
displayName(): string;
|
|
27
|
+
}
|
|
28
|
+
export declare function createHubCommand(environment?: HubCommandEnvironment): Command;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { withOutput } from "../../output/index.js";
|
|
4
|
+
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
5
|
+
import { connectToDaemon } from "../../utils/client.js";
|
|
6
|
+
import { createDeviceAuthorizationWorkflow } from "./device-authorization.js";
|
|
7
|
+
const productionEnvironment = {
|
|
8
|
+
connect: (host) => connectToDaemon({ host }),
|
|
9
|
+
authorize: (url, displayName) => createDeviceAuthorizationWorkflow().authorize(url, displayName),
|
|
10
|
+
displayName: hostname,
|
|
11
|
+
};
|
|
12
|
+
const schema = {
|
|
13
|
+
idField: "state",
|
|
14
|
+
columns: [
|
|
15
|
+
{ header: "STATE", field: "state" },
|
|
16
|
+
{ header: "HUB", field: "hub" },
|
|
17
|
+
{ header: "DAEMON", field: "daemonId" },
|
|
18
|
+
{ header: "SCOPES", field: "scopes" },
|
|
19
|
+
{ header: "CONNECTED", field: "connectedAt" },
|
|
20
|
+
{ header: "ERROR", field: "error" },
|
|
21
|
+
{ header: "WARNING", field: "warning" },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
function result(status, warning) {
|
|
25
|
+
return {
|
|
26
|
+
type: "list",
|
|
27
|
+
data: [
|
|
28
|
+
{
|
|
29
|
+
state: status.state,
|
|
30
|
+
daemonId: status.daemonId,
|
|
31
|
+
hub: status.hubOrigin,
|
|
32
|
+
scopes: status.scopes.join(", "),
|
|
33
|
+
connectedAt: status.connectedAt,
|
|
34
|
+
error: status.lastError,
|
|
35
|
+
warning,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
schema,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function withClient(environment, host, action) {
|
|
42
|
+
const client = await environment.connect(host);
|
|
43
|
+
try {
|
|
44
|
+
return await action(client);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await client.close().catch(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function createHubCommand(environment = productionEnvironment) {
|
|
51
|
+
const hub = new Command("hub").description("Manage this daemon's Otto Hub relationship");
|
|
52
|
+
addJsonAndDaemonHostOptions(hub.command("connect").argument("<url>").option("--token <token>")).action(withOutput(async (...args) => {
|
|
53
|
+
const url = args[0];
|
|
54
|
+
const options = args.at(-2);
|
|
55
|
+
return withClient(environment, options.host, async (client) => {
|
|
56
|
+
if (options.token !== undefined) {
|
|
57
|
+
return result((await client.connectHub(url, options.token)).status);
|
|
58
|
+
}
|
|
59
|
+
const existing = (await client.getHubStatus()).status;
|
|
60
|
+
if (existing.state !== "not_connected" && existing.state !== "revoked") {
|
|
61
|
+
throw new Error("This daemon already has a Hub relationship");
|
|
62
|
+
}
|
|
63
|
+
const token = await environment.authorize(url, suggestedDisplayName(environment.displayName()));
|
|
64
|
+
return result((await client.connectHub(url, token)).status);
|
|
65
|
+
});
|
|
66
|
+
}));
|
|
67
|
+
addJsonAndDaemonHostOptions(hub.command("status")).action(withOutput(async (...args) => {
|
|
68
|
+
const options = args.at(-2);
|
|
69
|
+
return withClient(environment, options.host, async (client) => result((await client.getHubStatus()).status));
|
|
70
|
+
}));
|
|
71
|
+
addJsonAndDaemonHostOptions(hub
|
|
72
|
+
.command("disconnect")
|
|
73
|
+
.option("--force", "Remove local authority even if the Hub is offline")).action(withOutput(async (...args) => {
|
|
74
|
+
const options = args.at(-2);
|
|
75
|
+
return withClient(environment, options.host, async (client) => {
|
|
76
|
+
const response = await client.disconnectHub(options.force ?? false);
|
|
77
|
+
return result(response.status, response.warning);
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
80
|
+
return hub;
|
|
81
|
+
}
|
|
82
|
+
function suggestedDisplayName(value) {
|
|
83
|
+
return value.trim().slice(0, 100) || "Otto daemon";
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DISABLED(hub): CLI half of the Hub switch.
|
|
3
|
+
*
|
|
4
|
+
* `packages/server/src/server/hub-disabled.ts` carries the full rationale. The
|
|
5
|
+
* short version: Hub is a documented permanent exclusion that landed anyway in
|
|
6
|
+
* the Paseo v0.2.5 merge, and we turn it off by redirecting import specifiers
|
|
7
|
+
* rather than deleting wiring, so upstream's edits to the call sites keep
|
|
8
|
+
* auto-merging.
|
|
9
|
+
*
|
|
10
|
+
* `cli.ts` still calls `createHubCommand()` on a byte-identical line. It just
|
|
11
|
+
* resolves here instead of `./commands/hub/index.js`, which keeps that whole
|
|
12
|
+
* subtree, and the daemon client calls it makes, out of the CLI's module graph.
|
|
13
|
+
*
|
|
14
|
+
* The command stays registered, carries "(disabled in this build)" in its own
|
|
15
|
+
* description, and answers every invocation with one honest sentence. That
|
|
16
|
+
* beats removing it outright: `otto hub connect` explains itself instead of
|
|
17
|
+
* dying on "unknown command". Hiding it from `--help` would need an option on
|
|
18
|
+
* `addCommand`, and that call site is deliberately left untouched.
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
export declare function createHubCommand(_environment?: unknown): Command;
|
|
22
|
+
//# sourceMappingURL=hub-disabled.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DISABLED(hub): CLI half of the Hub switch.
|
|
3
|
+
*
|
|
4
|
+
* `packages/server/src/server/hub-disabled.ts` carries the full rationale. The
|
|
5
|
+
* short version: Hub is a documented permanent exclusion that landed anyway in
|
|
6
|
+
* the Paseo v0.2.5 merge, and we turn it off by redirecting import specifiers
|
|
7
|
+
* rather than deleting wiring, so upstream's edits to the call sites keep
|
|
8
|
+
* auto-merging.
|
|
9
|
+
*
|
|
10
|
+
* `cli.ts` still calls `createHubCommand()` on a byte-identical line. It just
|
|
11
|
+
* resolves here instead of `./commands/hub/index.js`, which keeps that whole
|
|
12
|
+
* subtree, and the daemon client calls it makes, out of the CLI's module graph.
|
|
13
|
+
*
|
|
14
|
+
* The command stays registered, carries "(disabled in this build)" in its own
|
|
15
|
+
* description, and answers every invocation with one honest sentence. That
|
|
16
|
+
* beats removing it outright: `otto hub connect` explains itself instead of
|
|
17
|
+
* dying on "unknown command". Hiding it from `--help` would need an option on
|
|
18
|
+
* `addCommand`, and that call site is deliberately left untouched.
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
const HUB_DISABLED_MESSAGE = "Otto Hub is disabled in this build. See docs/upstream-merges.md.";
|
|
22
|
+
export function createHubCommand(_environment) {
|
|
23
|
+
return new Command("hub")
|
|
24
|
+
.description("Manage this daemon's Otto Hub relationship (disabled in this build)")
|
|
25
|
+
.addHelpText("after", `\n${HUB_DISABLED_MESSAGE}`)
|
|
26
|
+
.allowUnknownOption()
|
|
27
|
+
.allowExcessArguments()
|
|
28
|
+
.argument("[command...]", "Hub subcommand")
|
|
29
|
+
.action(() => {
|
|
30
|
+
process.stderr.write(`${HUB_DISABLED_MESSAGE}\n`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=hub-disabled.js.map
|
package/dist/commands/onboard.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { generateLocalPairingOffer, loadConfig, loadPersistedConfig, } from "@otto-code/server";
|
|
6
6
|
import { resolveLocalOttoHome, resolveLocalDaemonState, resolveTcpHostFromListen, startLocalDaemonDetached, tailDaemonLog, } from "./daemon/local-daemon.js";
|
|
7
7
|
import { tryConnectToDaemon } from "../utils/client.js";
|
|
8
|
+
import { formatPairingInstructions } from "../output/pairing.js";
|
|
8
9
|
const DEFAULT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
10
|
const READY_PROBE_TIMEOUT_MS = 1200;
|
|
10
11
|
class OnboardCancelledError extends Error {
|
|
@@ -406,8 +407,11 @@ export async function runOnboard(options) {
|
|
|
406
407
|
}
|
|
407
408
|
return;
|
|
408
409
|
}
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
process.stdout.write(formatPairingInstructions({
|
|
411
|
+
url: pairing.url,
|
|
412
|
+
qr: pairing.qr,
|
|
413
|
+
columns: process.stdout.columns,
|
|
414
|
+
}));
|
|
411
415
|
printNextSteps(pairing.url, ottoHome, richUi);
|
|
412
416
|
if (richUi) {
|
|
413
417
|
outro("Otto is ready!");
|
package/dist/commands/open.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import { type AgentDeepLinkTarget } from "@otto-code/protocol/agent-deep-link";
|
|
1
2
|
export declare function openDesktopWithProject(projectPath: string): Promise<void>;
|
|
3
|
+
export declare function openDesktopWithAgent(target: AgentDeepLinkTarget): Promise<void>;
|
|
2
4
|
//# sourceMappingURL=open.d.ts.map
|
package/dist/commands/open.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnProcess } from "@otto-code/server";
|
|
5
|
+
import { buildAgentDeepLink } from "@otto-code/protocol/agent-deep-link";
|
|
5
6
|
function findDesktopApp() {
|
|
6
7
|
if (process.platform === "darwin") {
|
|
7
8
|
const candidates = ["/Applications/Otto.app", path.join(homedir(), "Applications", "Otto.app")];
|
|
@@ -52,25 +53,26 @@ function spawnDetached(command, args) {
|
|
|
52
53
|
env: cleanEnvForDesktopLaunch(),
|
|
53
54
|
}).unref();
|
|
54
55
|
}
|
|
56
|
+
function launchDesktop(args) {
|
|
57
|
+
if (process.env.OTTO_DESKTOP_CLI === "1") {
|
|
58
|
+
throw new Error("Cannot open Otto Desktop while running in desktop CLI passthrough mode.");
|
|
59
|
+
}
|
|
60
|
+
const desktopApp = findDesktopApp();
|
|
61
|
+
if (!desktopApp) {
|
|
62
|
+
throw new Error("Otto desktop app not found. Install it from https://github.com/Draek2077/otto-code/releases");
|
|
63
|
+
}
|
|
64
|
+
if (process.platform === "darwin") {
|
|
65
|
+
// -n forces a new instance even if the app is already running. The new
|
|
66
|
+
// instance relays its argv to the existing one through Electron's
|
|
67
|
+
// single-instance lock. -g keeps the terminal in the foreground.
|
|
68
|
+
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", ...args]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
spawnDetached(desktopApp, args);
|
|
72
|
+
}
|
|
55
73
|
export async function openDesktopWithProject(projectPath) {
|
|
56
74
|
try {
|
|
57
|
-
|
|
58
|
-
throw new Error("Cannot open a desktop project while running in desktop CLI passthrough mode.");
|
|
59
|
-
}
|
|
60
|
-
const desktopApp = findDesktopApp();
|
|
61
|
-
if (!desktopApp) {
|
|
62
|
-
throw new Error("Otto desktop app not found. Install it from https://github.com/Draek2077/otto-code/releases");
|
|
63
|
-
}
|
|
64
|
-
if (process.platform === "darwin") {
|
|
65
|
-
// -n forces a new instance even if the app is already running.
|
|
66
|
-
// The new instance hits requestSingleInstanceLock(), fails, and relays
|
|
67
|
-
// the argv to the first instance via the second-instance event.
|
|
68
|
-
// -g keeps the terminal in the foreground (better CLI UX).
|
|
69
|
-
// Without -n, macOS just activates the existing window and drops --args.
|
|
70
|
-
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", projectPath]);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
spawnDetached(desktopApp, [projectPath]);
|
|
75
|
+
launchDesktop([projectPath]);
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
76
78
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -78,4 +80,7 @@ export async function openDesktopWithProject(projectPath) {
|
|
|
78
80
|
process.exitCode = 1;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
83
|
+
export async function openDesktopWithAgent(target) {
|
|
84
|
+
launchDesktop([buildAgentDeepLink(target)]);
|
|
85
|
+
}
|
|
81
86
|
//# sourceMappingURL=open.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
2
|
import { withOutput } from "../../output/index.js";
|
|
3
3
|
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
4
4
|
import { runCreateCommand } from "./create.js";
|
|
@@ -16,16 +16,16 @@ export function createScheduleCommand() {
|
|
|
16
16
|
.command("create")
|
|
17
17
|
.description("Create a schedule")
|
|
18
18
|
.argument("<prompt>", "Prompt to run on the schedule")
|
|
19
|
-
.option("--every <duration>", "
|
|
19
|
+
.option("--every <duration>", "Cron-compatible cadence preset (for example: 5m, 1h)")
|
|
20
20
|
.option("--cron <expr>", "Cron cadence expression")
|
|
21
21
|
.option("--timezone <iana>", "IANA time zone for cron cadence (default: UTC)")
|
|
22
22
|
.option("--name <name>", "Optional schedule name")
|
|
23
|
-
.
|
|
23
|
+
.addOption(new Option("--target <target>", "Legacy schedule target").hideHelp())
|
|
24
24
|
.option("--provider <provider>", "Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)")
|
|
25
25
|
.option("--mode <mode>", "Provider-specific mode (e.g. claude bypassPermissions, opencode build)")
|
|
26
|
+
.option("--thinking <id>", "Thinking option ID for new-agent runs")
|
|
26
27
|
.option("--cwd <path>", "Working directory (default: current; required with --host)")
|
|
27
|
-
.option("--run-now", "Fire one immediate run on creation
|
|
28
|
-
.option("--no-run-now", "Wait the full interval before the first run (only with --every)")
|
|
28
|
+
.option("--run-now", "Fire one immediate run on creation")
|
|
29
29
|
.option("--max-runs <n>", "Maximum number of runs")
|
|
30
30
|
.option("--expires-in <duration>", "Time to live for the schedule")).action(withOutput(runCreateCommand));
|
|
31
31
|
addJsonAndDaemonHostOptions(schedule.command("ls").description("List schedules")).action(withOutput(runLsCommand));
|
|
@@ -48,7 +48,7 @@ export function createScheduleCommand() {
|
|
|
48
48
|
.command("update")
|
|
49
49
|
.description("Update an existing schedule in place")
|
|
50
50
|
.argument("<id>", "Schedule ID")
|
|
51
|
-
.option("--every <duration>", "
|
|
51
|
+
.option("--every <duration>", "Cron-compatible cadence preset (for example: 5m, 1h)")
|
|
52
52
|
.option("--cron <expr>", "Switch to cron cadence expression")
|
|
53
53
|
.option("--timezone <iana>", "IANA time zone for cron cadence (requires --cron)")
|
|
54
54
|
.option("--name <name>", "Rename the schedule (empty string clears the name)")
|
|
@@ -7,6 +7,9 @@ export async function runInspectCommand(id, options, _command) {
|
|
|
7
7
|
if (payload.error || !payload.schedule) {
|
|
8
8
|
throw new Error(payload.error ?? `Schedule not found: ${id}`);
|
|
9
9
|
}
|
|
10
|
+
if (payload.schedule.target.type !== "new-agent") {
|
|
11
|
+
throw new Error(`Schedule not found: ${id}`);
|
|
12
|
+
}
|
|
10
13
|
const rows = createScheduleInspectRows(payload.schedule);
|
|
11
14
|
return {
|
|
12
15
|
type: "list",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleLogSchema, toScheduleLogRow } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, } from "./shared.js";
|
|
3
3
|
export async function runLogsCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.scheduleLogs({ id });
|
|
7
8
|
if (payload.error) {
|
|
8
9
|
throw new Error(payload.error);
|
|
@@ -9,7 +9,9 @@ export async function runLsCommand(options, _command) {
|
|
|
9
9
|
}
|
|
10
10
|
return {
|
|
11
11
|
type: "list",
|
|
12
|
-
data: payload.schedules
|
|
12
|
+
data: payload.schedules
|
|
13
|
+
.filter((schedule) => schedule.target.type === "new-agent")
|
|
14
|
+
.map(toScheduleRow),
|
|
13
15
|
schema: scheduleSchema,
|
|
14
16
|
};
|
|
15
17
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleSchema } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
3
3
|
export async function runPauseCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.schedulePause({ id });
|
|
7
8
|
if (payload.error || !payload.schedule) {
|
|
8
9
|
throw new Error(payload.error ?? `Failed to pause schedule: ${id}`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleSchema } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
3
3
|
export async function runResumeCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.scheduleResume({ id });
|
|
7
8
|
if (payload.error || !payload.schedule) {
|
|
8
9
|
throw new Error(payload.error ?? `Failed to resume schedule: ${id}`);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { scheduleSchema } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, requireNewAgentSchedule, toScheduleCommandError, toScheduleRow, } from "./shared.js";
|
|
3
3
|
export async function runRunOnceCommand(id, options, _command) {
|
|
4
4
|
const { client } = await connectScheduleClient(options.host);
|
|
5
5
|
try {
|
|
6
|
+
await requireNewAgentSchedule(client, id);
|
|
6
7
|
const payload = await client.scheduleRunOnce({ id });
|
|
7
8
|
if (payload.error || !payload.schedule) {
|
|
8
9
|
throw new Error(payload.error ?? `Failed to run schedule once: ${id}`);
|
|
@@ -8,6 +8,7 @@ export declare function connectScheduleClient(host: string | undefined): Promise
|
|
|
8
8
|
host: string;
|
|
9
9
|
}>;
|
|
10
10
|
export declare function toScheduleCommandError(code: string, action: string, error: unknown): CommandError;
|
|
11
|
+
export declare function requireNewAgentSchedule(client: ScheduleDaemonClient, id: string): Promise<void>;
|
|
11
12
|
export declare function formatCadence(cadence: ScheduleCadence): string;
|
|
12
13
|
export declare function formatTarget(target: ScheduleTarget | ScheduleListItem["target"]): string;
|
|
13
14
|
export declare function formatDurationMs(durationMs: number): string;
|
|
@@ -20,6 +21,7 @@ export declare function parseScheduleCreateInput(options: {
|
|
|
20
21
|
target?: string;
|
|
21
22
|
provider?: string;
|
|
22
23
|
mode?: string;
|
|
24
|
+
thinking?: string;
|
|
23
25
|
cwd?: string;
|
|
24
26
|
host?: string;
|
|
25
27
|
maxRuns?: string;
|
|
@@ -43,6 +45,7 @@ export interface ScheduleUpdateOptionsInput {
|
|
|
43
45
|
clearExpires?: boolean;
|
|
44
46
|
}
|
|
45
47
|
export declare function parseScheduleUpdateInput(options: ScheduleUpdateOptionsInput): UpdateScheduleInput;
|
|
48
|
+
export declare function compileEveryPresetToCron(value: string): string;
|
|
46
49
|
export interface ScheduleRow {
|
|
47
50
|
id: string;
|
|
48
51
|
name: string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
|
2
2
|
import { parseDuration } from "../../utils/duration.js";
|
|
3
3
|
import { resolveProviderAndModel } from "../../utils/provider-model.js";
|
|
4
|
+
import { everyMsToFiveFieldCron } from "@otto-code/protocol/schedule/cadence";
|
|
4
5
|
export async function connectScheduleClient(host) {
|
|
5
6
|
const resolvedHost = getDaemonHost({ host });
|
|
6
7
|
try {
|
|
@@ -28,6 +29,12 @@ export function toScheduleCommandError(code, action, error) {
|
|
|
28
29
|
message: `Failed to ${action}: ${message}`,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
export async function requireNewAgentSchedule(client, id) {
|
|
33
|
+
const payload = await client.scheduleInspect({ id });
|
|
34
|
+
if (payload.error || !payload.schedule || payload.schedule.target.type !== "new-agent") {
|
|
35
|
+
throw new Error(payload.error ?? `Schedule not found: ${id}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
export function formatCadence(cadence) {
|
|
32
39
|
if (cadence.type === "cron") {
|
|
33
40
|
const timezoneSuffix = cadence.timezone ? ` (${cadence.timezone})` : "";
|
|
@@ -66,11 +73,7 @@ export function formatDurationMs(durationMs) {
|
|
|
66
73
|
}
|
|
67
74
|
function resolveScheduleTarget(args) {
|
|
68
75
|
const { targetValue, hasExplicitNewAgentOption, createNewAgentTarget } = args;
|
|
69
|
-
const currentAgentId = process.env.OTTO_AGENT_ID?.trim();
|
|
70
76
|
if (!targetValue) {
|
|
71
|
-
if (currentAgentId && !hasExplicitNewAgentOption) {
|
|
72
|
-
return { type: "self", agentId: currentAgentId };
|
|
73
|
-
}
|
|
74
77
|
return createNewAgentTarget();
|
|
75
78
|
}
|
|
76
79
|
if (targetValue === "new-agent") {
|
|
@@ -79,11 +82,14 @@ function resolveScheduleTarget(args) {
|
|
|
79
82
|
if (hasExplicitNewAgentOption) {
|
|
80
83
|
throw {
|
|
81
84
|
code: "INVALID_TARGET",
|
|
82
|
-
message: "--provider/--mode can only be used with a new-agent target",
|
|
85
|
+
message: "--provider/--mode/--thinking can only be used with a new-agent target",
|
|
83
86
|
details: "Use --target new-agent or omit --target to create a new agent schedule",
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
89
|
if (targetValue === "self") {
|
|
90
|
+
// COMPAT(scheduleSelfTarget): heartbeat creation moved to `otto heartbeat create`.
|
|
91
|
+
// Added in v0.2.0; remove after 2027-01-17.
|
|
92
|
+
const currentAgentId = process.env.OTTO_AGENT_ID?.trim();
|
|
87
93
|
if (!currentAgentId) {
|
|
88
94
|
throw {
|
|
89
95
|
code: "INVALID_TARGET",
|
|
@@ -119,7 +125,14 @@ export function parseScheduleCreateInput(options) {
|
|
|
119
125
|
const runOnCreate = resolveRunOnCreate(options.runNow, cadence.type);
|
|
120
126
|
const targetValue = options.target?.trim();
|
|
121
127
|
const modeId = options.mode?.trim();
|
|
122
|
-
const
|
|
128
|
+
const thinkingOptionId = options.thinking?.trim();
|
|
129
|
+
if (options.thinking !== undefined && !thinkingOptionId) {
|
|
130
|
+
throw {
|
|
131
|
+
code: "INVALID_THINKING_OPTION",
|
|
132
|
+
message: "--thinking cannot be empty",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const hasExplicitNewAgentOption = options.provider !== undefined || options.mode !== undefined || options.thinking !== undefined;
|
|
123
136
|
const createNewAgentTarget = () => {
|
|
124
137
|
const resolvedProviderModel = resolveProviderAndModel({
|
|
125
138
|
provider: options.provider,
|
|
@@ -131,6 +144,7 @@ export function parseScheduleCreateInput(options) {
|
|
|
131
144
|
cwd: cwdInput ?? process.cwd(),
|
|
132
145
|
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
|
133
146
|
...(modeId ? { modeId } : {}),
|
|
147
|
+
...(thinkingOptionId ? { thinkingOptionId } : {}),
|
|
134
148
|
},
|
|
135
149
|
};
|
|
136
150
|
};
|
|
@@ -153,22 +167,8 @@ export function parseScheduleCreateInput(options) {
|
|
|
153
167
|
...(expiresAt ? { expiresAt } : {}),
|
|
154
168
|
};
|
|
155
169
|
}
|
|
156
|
-
function resolveRunOnCreate(runNow,
|
|
157
|
-
|
|
158
|
-
throw {
|
|
159
|
-
code: "REDUNDANT_RUN_NOW",
|
|
160
|
-
message: "--run-now is redundant with --every (interval schedules already fire on creation)",
|
|
161
|
-
details: "Drop --run-now, or use --no-run-now to wait the full interval before the first run",
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (runNow === false && cadenceType === "cron") {
|
|
165
|
-
throw {
|
|
166
|
-
code: "REDUNDANT_NO_RUN_NOW",
|
|
167
|
-
message: "--no-run-now is redundant with --cron (cron schedules never fire on creation)",
|
|
168
|
-
details: "Drop --no-run-now, or use --run-now to fire one immediate run on creation",
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return runNow ?? cadenceType === "every";
|
|
170
|
+
function resolveRunOnCreate(runNow, _cadenceType) {
|
|
171
|
+
return runNow ?? false;
|
|
172
172
|
}
|
|
173
173
|
export function parseScheduleUpdateInput(options) {
|
|
174
174
|
const id = options.id.trim();
|
|
@@ -220,7 +220,7 @@ function parseCadenceFromFlags(every, cron, timezone) {
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
if (every !== undefined) {
|
|
223
|
-
return { type: "
|
|
223
|
+
return { type: "cron", expression: compileEveryPresetToCron(every) };
|
|
224
224
|
}
|
|
225
225
|
if (cron !== undefined) {
|
|
226
226
|
return {
|
|
@@ -231,6 +231,18 @@ function parseCadenceFromFlags(every, cron, timezone) {
|
|
|
231
231
|
}
|
|
232
232
|
return undefined;
|
|
233
233
|
}
|
|
234
|
+
export function compileEveryPresetToCron(value) {
|
|
235
|
+
const durationMs = parseDuration(value);
|
|
236
|
+
const cron = everyMsToFiveFieldCron(durationMs);
|
|
237
|
+
if (cron) {
|
|
238
|
+
return cron;
|
|
239
|
+
}
|
|
240
|
+
throw {
|
|
241
|
+
code: "UNREPRESENTABLE_CADENCE",
|
|
242
|
+
message: `${value} cannot be represented faithfully by five-field cron`,
|
|
243
|
+
details: "Use --cron for calendar schedules",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
234
246
|
function parseTimeZoneFlag(timeZone) {
|
|
235
247
|
if (timeZone === undefined) {
|
|
236
248
|
return undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createScheduleInspectRows, createScheduleInspectSchema, } from "./schema.js";
|
|
2
|
-
import { connectScheduleClient, parseScheduleUpdateInput, toScheduleCommandError, } from "./shared.js";
|
|
2
|
+
import { connectScheduleClient, parseScheduleUpdateInput, requireNewAgentSchedule, toScheduleCommandError, } from "./shared.js";
|
|
3
3
|
export async function runUpdateCommand(id, options, _command) {
|
|
4
4
|
const input = parseScheduleUpdateInput({
|
|
5
5
|
id,
|
|
@@ -19,6 +19,7 @@ export async function runUpdateCommand(id, options, _command) {
|
|
|
19
19
|
});
|
|
20
20
|
const { client } = await connectScheduleClient(options.host);
|
|
21
21
|
try {
|
|
22
|
+
await requireNewAgentSchedule(client, id);
|
|
22
23
|
const payload = await client.scheduleUpdate(input);
|
|
23
24
|
if (payload.error || !payload.schedule) {
|
|
24
25
|
throw new Error(payload.error ?? `Failed to update schedule: ${id}`);
|