Cron: add scheduler, wakeups, and run history
This commit is contained in:
@@ -5,15 +5,14 @@ import { healthCommand } from "../commands/health.js";
|
||||
import { sendCommand } from "../commands/send.js";
|
||||
import { sessionsCommand } from "../commands/sessions.js";
|
||||
import { statusCommand } from "../commands/status.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { startGatewayServer } from "../gateway/server.js";
|
||||
import { danger, info, setVerbose } from "../globals.js";
|
||||
import { GatewayLockError } from "../infra/gateway-lock.js";
|
||||
import { loginWeb, logoutWeb } from "../provider-web.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { startWebChatServer } from "../webchat/server.js";
|
||||
import { registerCronCli } from "./cron-cli.js";
|
||||
import { createDefaultDeps } from "./deps.js";
|
||||
import { registerGatewayCli } from "./gateway-cli.js";
|
||||
import { forceFreePort } from "./ports.js";
|
||||
|
||||
export { forceFreePort };
|
||||
@@ -209,266 +208,8 @@ Examples:
|
||||
}
|
||||
});
|
||||
|
||||
program;
|
||||
const gateway = program
|
||||
.command("gateway")
|
||||
.description("Run the WebSocket Gateway")
|
||||
.option("--port <port>", "Port for the gateway WebSocket", "18789")
|
||||
.option(
|
||||
"--webchat-port <port>",
|
||||
"Port for the loopback WebChat HTTP server (default 18788)",
|
||||
)
|
||||
.option(
|
||||
"--token <token>",
|
||||
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
||||
)
|
||||
.option(
|
||||
"--force",
|
||||
"Kill any existing listener on the target port before starting",
|
||||
false,
|
||||
)
|
||||
.option("--verbose", "Verbose logging to stdout/stderr", false)
|
||||
.action(async (opts) => {
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
const port = Number.parseInt(String(opts.port ?? "18789"), 10);
|
||||
if (Number.isNaN(port) || port <= 0) {
|
||||
defaultRuntime.error("Invalid port");
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
const webchatPort = opts.webchatPort
|
||||
? Number.parseInt(String(opts.webchatPort), 10)
|
||||
: undefined;
|
||||
if (
|
||||
webchatPort !== undefined &&
|
||||
(Number.isNaN(webchatPort) || webchatPort <= 0)
|
||||
) {
|
||||
defaultRuntime.error("Invalid webchat port");
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
if (opts.force) {
|
||||
try {
|
||||
const killed = forceFreePort(port);
|
||||
if (killed.length === 0) {
|
||||
defaultRuntime.log(info(`Force: no listeners on port ${port}`));
|
||||
} else {
|
||||
for (const proc of killed) {
|
||||
defaultRuntime.log(
|
||||
info(
|
||||
`Force: killed pid ${proc.pid}${proc.command ? ` (${proc.command})` : ""} on port ${port}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
}
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Force: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (opts.token) {
|
||||
process.env.CLAWDIS_GATEWAY_TOKEN = String(opts.token);
|
||||
}
|
||||
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>> | null = null;
|
||||
let shuttingDown = false;
|
||||
let forceExitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const onSigterm = () => shutdown("SIGTERM");
|
||||
const onSigint = () => shutdown("SIGINT");
|
||||
|
||||
const shutdown = (signal: string) => {
|
||||
// Ensure we don't leak listeners across restarts/tests.
|
||||
process.removeListener("SIGTERM", onSigterm);
|
||||
process.removeListener("SIGINT", onSigint);
|
||||
|
||||
if (shuttingDown) {
|
||||
defaultRuntime.log(
|
||||
info(`gateway: received ${signal} during shutdown; exiting now`),
|
||||
);
|
||||
defaultRuntime.exit(0);
|
||||
}
|
||||
shuttingDown = true;
|
||||
defaultRuntime.log(info(`gateway: received ${signal}; shutting down`));
|
||||
|
||||
// Avoid hanging forever if a provider task ignores abort.
|
||||
forceExitTimer = setTimeout(() => {
|
||||
defaultRuntime.error(
|
||||
"gateway: shutdown timed out; exiting without full cleanup",
|
||||
);
|
||||
defaultRuntime.exit(0);
|
||||
}, 5000);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
await server?.close();
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`gateway: shutdown error: ${String(err)}`);
|
||||
} finally {
|
||||
if (forceExitTimer) clearTimeout(forceExitTimer);
|
||||
defaultRuntime.exit(0);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
process.once("SIGTERM", onSigterm);
|
||||
process.once("SIGINT", onSigint);
|
||||
|
||||
try {
|
||||
server = await startGatewayServer(port, { webchatPort });
|
||||
} catch (err) {
|
||||
if (err instanceof GatewayLockError) {
|
||||
defaultRuntime.error(`Gateway failed to start: ${err.message}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
defaultRuntime.error(`Gateway failed to start: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
// Keep process alive
|
||||
await new Promise<never>(() => {});
|
||||
});
|
||||
|
||||
const gatewayCallOpts = (cmd: Command) =>
|
||||
cmd
|
||||
.option("--url <url>", "Gateway WebSocket URL", "ws://127.0.0.1:18789")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
.option("--expect-final", "Wait for final response (agent)", false);
|
||||
|
||||
const callGatewayCli = async (
|
||||
method: string,
|
||||
opts: {
|
||||
url?: string;
|
||||
token?: string;
|
||||
timeout?: string;
|
||||
expectFinal?: boolean;
|
||||
},
|
||||
params?: unknown,
|
||||
) =>
|
||||
callGateway({
|
||||
url: opts.url,
|
||||
token: opts.token,
|
||||
method,
|
||||
params,
|
||||
expectFinal: Boolean(opts.expectFinal),
|
||||
timeoutMs: Number(opts.timeout ?? 10_000),
|
||||
clientName: "cli",
|
||||
mode: "cli",
|
||||
});
|
||||
|
||||
gatewayCallOpts(
|
||||
gateway
|
||||
.command("call")
|
||||
.description("Call a Gateway method and print JSON")
|
||||
.argument(
|
||||
"<method>",
|
||||
"Method name (health/status/system-presence/send/agent)",
|
||||
)
|
||||
.option("--params <json>", "JSON object string for params", "{}")
|
||||
.action(async (method, opts) => {
|
||||
try {
|
||||
const params = JSON.parse(String(opts.params ?? "{}"));
|
||||
const result = await callGatewayCli(method, opts, params);
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway call failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
gatewayCallOpts(
|
||||
gateway
|
||||
.command("health")
|
||||
.description("Fetch Gateway health")
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const result = await callGatewayCli("health", opts);
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
gatewayCallOpts(
|
||||
gateway
|
||||
.command("status")
|
||||
.description("Fetch Gateway status")
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const result = await callGatewayCli("status", opts);
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
gatewayCallOpts(
|
||||
gateway
|
||||
.command("send")
|
||||
.description("Send a message via the Gateway")
|
||||
.requiredOption("--to <jidOrPhone>", "Destination (E.164 or jid)")
|
||||
.requiredOption("--message <text>", "Message text")
|
||||
.option("--media-url <url>", "Optional media URL")
|
||||
.option("--idempotency-key <key>", "Idempotency key")
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const idempotencyKey = opts.idempotencyKey ?? randomIdempotencyKey();
|
||||
const result = await callGatewayCli("send", opts, {
|
||||
to: opts.to,
|
||||
message: opts.message,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
idempotencyKey,
|
||||
});
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
gatewayCallOpts(
|
||||
gateway
|
||||
.command("agent")
|
||||
.description("Run an agent turn via the Gateway (waits for final)")
|
||||
.requiredOption("--message <text>", "User message")
|
||||
.option("--to <jidOrPhone>", "Destination")
|
||||
.option("--session-id <id>", "Session id")
|
||||
.option("--thinking <level>", "Thinking level")
|
||||
.option("--deliver", "Deliver response", false)
|
||||
.option("--timeout-seconds <n>", "Agent timeout seconds")
|
||||
.option("--idempotency-key <key>", "Idempotency key")
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const idempotencyKey = opts.idempotencyKey ?? randomIdempotencyKey();
|
||||
const result = await callGatewayCli(
|
||||
"agent",
|
||||
{ ...opts, expectFinal: true },
|
||||
{
|
||||
message: opts.message,
|
||||
to: opts.to,
|
||||
sessionId: opts.sessionId,
|
||||
thinking: opts.thinking,
|
||||
deliver: Boolean(opts.deliver),
|
||||
timeout: opts.timeoutSeconds
|
||||
? Number.parseInt(String(opts.timeoutSeconds), 10)
|
||||
: undefined,
|
||||
idempotencyKey,
|
||||
},
|
||||
);
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
registerGatewayCli(program);
|
||||
registerCronCli(program);
|
||||
program
|
||||
.command("status")
|
||||
.description("Show web session health and recent session recipients")
|
||||
|
||||
Reference in New Issue
Block a user