Fix: Daemon startup and Qwen CLI path for Windows
This commit is contained in:
@@ -14,7 +14,7 @@ import type { Job } from "../jobs";
|
||||
const QWEN_DIR = join(process.cwd(), ".qwen");
|
||||
const HEARTBEAT_DIR = join(QWEN_DIR, "qwenclaw");
|
||||
const STATUSLINE_FILE = join(QWEN_DIR, "statusline.cjs");
|
||||
const QWEN_SETTINGS_FILE = join(QWEN_DIR, "settings.json");
|
||||
const QWEN_SETTINGS_FILE = join(HEARTBEAT_DIR, "settings.json");
|
||||
const PREFLIGHT_SCRIPT = fileURLToPath(new URL("../preflight.ts", import.meta.url));
|
||||
|
||||
// --- Statusline setup/teardown ---
|
||||
@@ -22,13 +22,16 @@ const PREFLIGHT_SCRIPT = fileURLToPath(new URL("../preflight.ts", import.meta.ur
|
||||
const STATUSLINE_SCRIPT = `#!/usr/bin/env node
|
||||
const { readFileSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const DIR = join(__dirname, "qwenclaw");
|
||||
const STATE_FILE = join(DIR, "state.json");
|
||||
const PID_FILE = join(DIR, "daemon.pid");
|
||||
|
||||
const R = "\\x1b[0m";
|
||||
const DIM = "\\x1b[2m";
|
||||
const RED = "\\x1b[31m";
|
||||
const GREEN = "\\x1b[32m";
|
||||
|
||||
function fmt(ms) {
|
||||
if (ms <= 0) return GREEN + "now!" + R;
|
||||
var s = Math.floor(ms / 1000);
|
||||
@@ -38,65 +41,58 @@ function fmt(ms) {
|
||||
if (m > 0) return m + "m";
|
||||
return (s % 60) + "s";
|
||||
}
|
||||
|
||||
function alive() {
|
||||
try {
|
||||
var pid = readFileSync(PID_FILE, "utf-8").trim();
|
||||
process.kill(Number(pid), 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
var B = DIM + "\\u2502" + R;
|
||||
var TL = DIM + "\\u256d" + R;
|
||||
var TR = DIM + "\\u256e" + R;
|
||||
var BL = DIM + "\\u2570" + R;
|
||||
var BR = DIM + "\\u256f" + R;
|
||||
var H = DIM + "\\u2500" + R;
|
||||
var HEADER = TL + H.repeat(6) + " \\ud83d\\udc3e QwenClaw \\ud83d\\udc3e " + H.repeat(6) + TR;
|
||||
var HEADER = TL + H.repeat(6) + " \\ud83e\\udd9e qwenclaw \\ud83e\\udd9e " + H.repeat(6) + TR;
|
||||
var FOOTER = BL + H.repeat(30) + BR;
|
||||
|
||||
if (!alive()) {
|
||||
process.stdout.write(
|
||||
HEADER +
|
||||
"\\n" +
|
||||
B +
|
||||
" " +
|
||||
RED +
|
||||
"\\u25cb offline" +
|
||||
R +
|
||||
" " +
|
||||
B +
|
||||
"\\n" +
|
||||
FOOTER
|
||||
HEADER + "\\n" +
|
||||
B + " " + RED + "\\u25cb offline" + R + " " + B + "\\n" +
|
||||
FOOTER
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
var state = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
||||
var now = Date.now();
|
||||
var info = [];
|
||||
|
||||
if (state.heartbeat) {
|
||||
info.push("\\ud83d\\udc93 " + fmt(state.heartbeat.nextAt - now));
|
||||
}
|
||||
|
||||
var jc = (state.jobs || []).length;
|
||||
info.push("\\ud83d\\udccb " + jc + " job" + (jc !== 1 ? "s" : ""));
|
||||
info.push(GREEN + "\\u25cf live" + R);
|
||||
|
||||
if (state.telegram) {
|
||||
info.push(GREEN + "\\ud83d\\udce1" + R);
|
||||
}
|
||||
|
||||
var mid = " " + info.join(" " + B + " ") + " ";
|
||||
|
||||
process.stdout.write(HEADER + "\\n" + B + mid + B + "\\n" + FOOTER);
|
||||
} catch {
|
||||
process.stdout.write(
|
||||
HEADER +
|
||||
"\\n" +
|
||||
B +
|
||||
DIM +
|
||||
" waiting... " +
|
||||
R +
|
||||
B +
|
||||
"\\n" +
|
||||
FOOTER
|
||||
HEADER + "\\n" +
|
||||
B + DIM + " waiting... " + R + B + "\\n" +
|
||||
FOOTER
|
||||
);
|
||||
}
|
||||
`;
|
||||
@@ -109,37 +105,23 @@ function parseClockMinutes(value: string): number | null {
|
||||
return Number(match[1]) * 60 + Number(match[2]);
|
||||
}
|
||||
|
||||
function isHeartbeatExcludedNow(
|
||||
config: HeartbeatConfig,
|
||||
timezoneOffsetMinutes: number
|
||||
): boolean {
|
||||
function isHeartbeatExcludedNow(config: HeartbeatConfig, timezoneOffsetMinutes: number): boolean {
|
||||
return isHeartbeatExcludedAt(config, timezoneOffsetMinutes, new Date());
|
||||
}
|
||||
|
||||
function isHeartbeatExcludedAt(
|
||||
config: HeartbeatConfig,
|
||||
timezoneOffsetMinutes: number,
|
||||
at: Date
|
||||
): boolean {
|
||||
if (!Array.isArray(config.excludeWindows) || config.excludeWindows.length === 0)
|
||||
return false;
|
||||
|
||||
function isHeartbeatExcludedAt(config: HeartbeatConfig, timezoneOffsetMinutes: number, at: Date): boolean {
|
||||
if (!Array.isArray(config.excludeWindows) || config.excludeWindows.length === 0) return false;
|
||||
const local = getDayAndMinuteAtOffset(at, timezoneOffsetMinutes);
|
||||
|
||||
for (const window of config.excludeWindows) {
|
||||
const start = parseClockMinutes(window.start);
|
||||
const end = parseClockMinutes(window.end);
|
||||
if (start == null || end == null) continue;
|
||||
|
||||
const days =
|
||||
Array.isArray(window.days) && window.days.length > 0
|
||||
? window.days
|
||||
: ALL_DAYS;
|
||||
|
||||
const days = Array.isArray(window.days) && window.days.length > 0 ? window.days : ALL_DAYS;
|
||||
const sameDay = start < end;
|
||||
|
||||
if (sameDay) {
|
||||
if (days.includes(local.day) && local.minute >= start && local.minute < end)
|
||||
return true;
|
||||
if (days.includes(local.day) && local.minute >= start && local.minute < end) return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -165,33 +147,40 @@ function nextAllowedHeartbeatAt(
|
||||
const interval = Math.max(60_000, Math.round(intervalMs));
|
||||
let candidate = fromMs + interval;
|
||||
let guard = 0;
|
||||
while (
|
||||
isHeartbeatExcludedAt(config, timezoneOffsetMinutes, new Date(candidate)) &&
|
||||
guard < 20_000
|
||||
) {
|
||||
|
||||
while (isHeartbeatExcludedAt(config, timezoneOffsetMinutes, new Date(candidate)) && guard < 20_000) {
|
||||
candidate += interval;
|
||||
guard++;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
async function setupStatusline() {
|
||||
console.log("[QwenClaw SetupStatusline] Starting...");
|
||||
await mkdir(QWEN_DIR, { recursive: true });
|
||||
console.log("[QwenClaw SetupStatusline] Directory created:", QWEN_DIR);
|
||||
|
||||
await writeFile(STATUSLINE_FILE, STATUSLINE_SCRIPT);
|
||||
console.log("[QwenClaw SetupStatusline] Statusline script written:", STATUSLINE_FILE);
|
||||
|
||||
let settings: Record<string, unknown> = {};
|
||||
try {
|
||||
settings = await Bun.file(QWEN_SETTINGS_FILE).json();
|
||||
} catch {
|
||||
// file doesn't exist or isn't valid JSON
|
||||
const settingsText = await Bun.file(QWEN_SETTINGS_FILE).text();
|
||||
console.log("[QwenClaw SetupStatusline] Settings text read, length:", settingsText.length);
|
||||
settings = JSON.parse(settingsText);
|
||||
console.log("[QwenClaw SetupStatusline] Settings parsed");
|
||||
} catch (err) {
|
||||
console.log("[QwenClaw SetupStatusline] No existing settings or error:", err);
|
||||
}
|
||||
|
||||
settings.statusLine = {
|
||||
type: "command",
|
||||
command: "node .qwen/statusline.cjs",
|
||||
};
|
||||
|
||||
await writeFile(QWEN_SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
||||
const settingsJson = JSON.stringify(settings, null, 2) + "\n";
|
||||
console.log("[QwenClaw SetupStatusline] Writing settings:", settingsJson.substring(0, 100));
|
||||
await writeFile(QWEN_SETTINGS_FILE, settingsJson);
|
||||
console.log("[QwenClaw SetupStatusline] Settings written");
|
||||
}
|
||||
|
||||
async function teardownStatusline() {
|
||||
@@ -213,6 +202,10 @@ async function teardownStatusline() {
|
||||
// --- Main ---
|
||||
|
||||
export async function start(args: string[] = []) {
|
||||
console.log("[QwenClaw Start] Function called with args:", args);
|
||||
|
||||
try {
|
||||
|
||||
let hasPromptFlag = false;
|
||||
let hasTriggerFlag = false;
|
||||
let telegramFlag = false;
|
||||
@@ -220,9 +213,9 @@ export async function start(args: string[] = []) {
|
||||
let webFlag = false;
|
||||
let replaceExistingFlag = false;
|
||||
let webPortFlag: number | null = null;
|
||||
|
||||
const payloadParts: string[] = [];
|
||||
|
||||
console.log("[QwenClaw Start] Parsing arguments...");
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--prompt") {
|
||||
@@ -254,46 +247,39 @@ export async function start(args: string[] = []) {
|
||||
payloadParts.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = payloadParts.join(" ").trim();
|
||||
|
||||
console.log("[QwenClaw Start] Payload:", payload, "hasPromptFlag:", hasPromptFlag, "webFlag:", webFlag);
|
||||
|
||||
if (hasPromptFlag && !payload) {
|
||||
console.error(
|
||||
"Usage: qwenclaw start --prompt [--trigger] [--telegram] [--debug] [--web] [--web-port <port>] [--replace-existing]"
|
||||
);
|
||||
console.error("Usage: qwenclaw start --prompt <prompt> [--trigger] [--telegram] [--debug] [--web] [--web-port <port>] [--replace-existing]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!hasPromptFlag && payload) {
|
||||
console.error("Prompt text requires `--prompt`.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (telegramFlag && !hasTriggerFlag) {
|
||||
console.error("`--telegram` with `start` requires `--trigger`.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (hasPromptFlag && !hasTriggerFlag && (webFlag || webPortFlag !== null)) {
|
||||
console.error("`--web` is daemon-only. Remove `--prompt`, or add `--trigger`.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("[QwenClaw Start] Checking existing daemon...");
|
||||
// One-shot mode: explicit prompt without trigger.
|
||||
if (hasPromptFlag && !hasTriggerFlag) {
|
||||
const existingPid = await checkExistingDaemon();
|
||||
if (existingPid) {
|
||||
console.error(
|
||||
`\x1b[31mAborted: daemon already running in this directory (PID ${existingPid})\x1b[0m`
|
||||
);
|
||||
console.error("Use `qwenclaw send [--telegram]` while daemon is running.");
|
||||
console.error(`\x1b[31mAborted: daemon already running in this directory (PID ${existingPid})\x1b[0m`);
|
||||
console.error("Use `qwenclaw send <message> [--telegram]` while daemon is running.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await initConfig();
|
||||
await loadSettings();
|
||||
await ensureProjectQwenMd();
|
||||
|
||||
const result = await runUserMessage("prompt", payload);
|
||||
console.log(result.stdout);
|
||||
if (result.exitCode !== 0) process.exit(result.exitCode);
|
||||
@@ -303,18 +289,18 @@ export async function start(args: string[] = []) {
|
||||
const existingPid = await checkExistingDaemon();
|
||||
if (existingPid) {
|
||||
if (!replaceExistingFlag) {
|
||||
console.error(
|
||||
`\x1b[31mAborted: daemon already running in this directory (PID ${existingPid})\x1b[0m`
|
||||
);
|
||||
console.error(`\x1b[31mAborted: daemon already running in this directory (PID ${existingPid})\x1b[0m`);
|
||||
console.error(`Use --stop first, or kill PID ${existingPid} manually.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Replacing existing daemon (PID ${existingPid})...`);
|
||||
try {
|
||||
process.kill(existingPid, "SIGTERM");
|
||||
} catch {
|
||||
// ignore if process is already dead
|
||||
}
|
||||
|
||||
const deadline = Date.now() + 4000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
@@ -324,20 +310,36 @@ export async function start(args: string[] = []) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await cleanupPidFile();
|
||||
}
|
||||
|
||||
await initConfig();
|
||||
console.log("[QwenClaw Start] Config initialized");
|
||||
|
||||
const settings = await loadSettings();
|
||||
console.log("[QwenClaw Start] Settings loaded, web:", settings.web);
|
||||
|
||||
await ensureProjectQwenMd();
|
||||
console.log("[QwenClaw Start] Project QWEN.md ensured");
|
||||
|
||||
const jobs = await loadJobs();
|
||||
|
||||
console.log("[QwenClaw Start] Jobs loaded:", jobs.length);
|
||||
|
||||
const webEnabled = webFlag || webPortFlag !== null || settings.web.enabled;
|
||||
const webPort = webPortFlag ?? settings.web.port;
|
||||
console.log("[QwenClaw Start] Web enabled:", webEnabled, "Port:", webPort);
|
||||
|
||||
await setupStatusline();
|
||||
try {
|
||||
await setupStatusline();
|
||||
console.log("[QwenClaw Start] Statusline setup complete");
|
||||
} catch (err) {
|
||||
console.error("[QwenClaw Start] Statusline setup error:", err);
|
||||
}
|
||||
|
||||
await writePidFile();
|
||||
|
||||
console.log("[QwenClaw Start] PID file written");
|
||||
|
||||
let web: WebServerHandle | null = null;
|
||||
|
||||
async function shutdown() {
|
||||
@@ -346,23 +348,18 @@ export async function start(args: string[] = []) {
|
||||
await cleanupPidFile();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
|
||||
console.log("QwenClaw daemon started");
|
||||
console.log("qwenclaw daemon started");
|
||||
console.log(` PID: ${process.pid}`);
|
||||
console.log(` Security: ${settings.security.level}`);
|
||||
if (settings.security.allowedTools.length > 0)
|
||||
console.log(` + allowed: ${settings.security.allowedTools.join(", ")}`);
|
||||
if (settings.security.disallowedTools.length > 0)
|
||||
console.log(` - blocked: ${settings.security.disallowedTools.join(", ")}`);
|
||||
console.log(
|
||||
` Heartbeat: ${settings.heartbeat.enabled ? `every ${settings.heartbeat.interval}m` : "disabled"}`
|
||||
);
|
||||
console.log(
|
||||
` Web UI: ${webEnabled ? `http://${settings.web.host}:${webPort}` : "disabled"}`
|
||||
);
|
||||
console.log(` Heartbeat: ${settings.heartbeat.enabled ? `every ${settings.heartbeat.interval}m` : "disabled"}`);
|
||||
console.log(` Web UI: ${webEnabled ? `http://${settings.web.host}:${webPort}` : "disabled"}`);
|
||||
if (debugFlag) console.log(" Debug: enabled");
|
||||
console.log(` Jobs loaded: ${jobs.length}`);
|
||||
jobs.forEach((j) => console.log(` - ${j.name} [${j.schedule}]`));
|
||||
@@ -427,41 +424,32 @@ export async function start(args: string[] = []) {
|
||||
},
|
||||
onHeartbeatSettingsChanged: (patch) => {
|
||||
let changed = false;
|
||||
if (
|
||||
typeof patch.enabled === "boolean" &&
|
||||
currentSettings.heartbeat.enabled !== patch.enabled
|
||||
) {
|
||||
if (typeof patch.enabled === "boolean" && currentSettings.heartbeat.enabled !== patch.enabled) {
|
||||
currentSettings.heartbeat.enabled = patch.enabled;
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
typeof patch.interval === "number" &&
|
||||
Number.isFinite(patch.interval)
|
||||
) {
|
||||
if (typeof patch.interval === "number" && Number.isFinite(patch.interval)) {
|
||||
const interval = Math.max(1, Math.min(1440, Math.round(patch.interval)));
|
||||
if (currentSettings.heartbeat.interval !== interval) {
|
||||
currentSettings.heartbeat.interval = interval;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof patch.prompt === "string" &&
|
||||
currentSettings.heartbeat.prompt !== patch.prompt
|
||||
) {
|
||||
currentSettings.heartbeat.prompt = patch.prompt;
|
||||
if (typeof patch.prompt === "string" && currentSettings.heartbeat.prompt !== patch.prompt) {
|
||||
currentSettings.heartbeat.prompt = patch.prompt;
|
||||
changed = true;
|
||||
}
|
||||
if (Array.isArray(patch.excludeWindows)) {
|
||||
const prev = JSON.stringify(currentSettings.heartbeat.excludeWindows);
|
||||
const next = JSON.stringify(patch.excludeWindows);
|
||||
if (prev !== next) {
|
||||
currentSettings.heartbeat.excludeWindows = patch.excludeWindows;
|
||||
changed = true;
|
||||
}
|
||||
if (Array.isArray(patch.excludeWindows)) {
|
||||
const prev = JSON.stringify(currentSettings.heartbeat.excludeWindows);
|
||||
const next = JSON.stringify(patch.excludeWindows);
|
||||
if (prev !== next) {
|
||||
currentSettings.heartbeat.excludeWindows = patch.excludeWindows;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) return;
|
||||
scheduleHeartbeat();
|
||||
updateState();
|
||||
}
|
||||
if (!changed) return;
|
||||
scheduleHeartbeat();
|
||||
updateState();
|
||||
console.log(`[${ts()}] Heartbeat settings updated from Web UI`);
|
||||
},
|
||||
onJobsChanged: async () => {
|
||||
@@ -476,6 +464,7 @@ export async function start(args: string[] = []) {
|
||||
if (!isAddrInUse(err) || i === maxAttempts - 1) throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
@@ -483,26 +472,19 @@ export async function start(args: string[] = []) {
|
||||
currentSettings.web.enabled = true;
|
||||
web = startWebWithFallback(currentSettings.web.host, webPort);
|
||||
currentSettings.web.port = web.port;
|
||||
console.log(
|
||||
`[${new Date().toLocaleTimeString()}] Web UI listening on http://${web.host}:${web.port}`
|
||||
);
|
||||
console.log(`[${new Date().toLocaleTimeString()}] Web UI listening on http://${web.host}:${web.port}`);
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
function ts() {
|
||||
return new Date().toLocaleTimeString();
|
||||
}
|
||||
function ts() { return new Date().toLocaleTimeString(); }
|
||||
|
||||
function startPreflightInBackground(projectPath: string): void {
|
||||
try {
|
||||
const proc = Bun.spawn(
|
||||
[process.execPath, "run", PREFLIGHT_SCRIPT, projectPath],
|
||||
{
|
||||
stdin: "ignore",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
}
|
||||
);
|
||||
const proc = Bun.spawn([process.execPath, "run", PREFLIGHT_SCRIPT, projectPath], {
|
||||
stdin: "ignore",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
proc.unref();
|
||||
console.log(`[${ts()}] Plugin preflight started in background`);
|
||||
} catch (err) {
|
||||
@@ -510,15 +492,11 @@ export async function start(args: string[] = []) {
|
||||
}
|
||||
}
|
||||
|
||||
function forwardToTelegram(
|
||||
label: string,
|
||||
result: { exitCode: number; stdout: string; stderr: string }
|
||||
) {
|
||||
function forwardToTelegram(label: string, result: { exitCode: number; stdout: string; stderr: string }) {
|
||||
if (!telegramSend || currentSettings.telegram.allowedUserIds.length === 0) return;
|
||||
const text =
|
||||
result.exitCode === 0
|
||||
? `${label ? `[${label}]\n` : ""}${result.stdout || "(empty)"}`
|
||||
: `${label ? `[${label}] ` : ""}error (exit ${result.exitCode}): ${result.stderr || "Unknown"}`;
|
||||
const text = result.exitCode === 0
|
||||
? `${label ? `[${label}]\n` : ""}${result.stdout || "(empty)"}`
|
||||
: `${label ? `[${label}] ` : ""}error (exit ${result.exitCode}): ${result.stderr || "Unknown"}`;
|
||||
for (const userId of currentSettings.telegram.allowedUserIds) {
|
||||
telegramSend(userId, text).catch((err) =>
|
||||
console.error(`[Telegram] Failed to forward to ${userId}: ${err}`)
|
||||
@@ -545,12 +523,7 @@ export async function start(args: string[] = []) {
|
||||
);
|
||||
|
||||
function tick() {
|
||||
if (
|
||||
isHeartbeatExcludedNow(
|
||||
currentSettings.heartbeat,
|
||||
currentSettings.timezoneOffsetMinutes
|
||||
)
|
||||
) {
|
||||
if (isHeartbeatExcludedNow(currentSettings.heartbeat, currentSettings.timezoneOffsetMinutes)) {
|
||||
console.log(`[${ts()}] Heartbeat skipped (excluded window)`);
|
||||
nextHeartbeatAt = nextAllowedHeartbeatAt(
|
||||
currentSettings.heartbeat,
|
||||
@@ -560,7 +533,6 @@ export async function start(args: string[] = []) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
resolvePrompt(currentSettings.heartbeat.prompt),
|
||||
loadHeartbeatPromptTemplate(),
|
||||
@@ -578,7 +550,6 @@ export async function start(args: string[] = []) {
|
||||
.then((r) => {
|
||||
if (r) forwardToTelegram("", r);
|
||||
});
|
||||
|
||||
nextHeartbeatAt = nextAllowedHeartbeatAt(
|
||||
currentSettings.heartbeat,
|
||||
currentSettings.timezoneOffsetMinutes,
|
||||
@@ -602,9 +573,7 @@ export async function start(args: string[] = []) {
|
||||
console.log(triggerResult.stdout);
|
||||
if (telegramFlag) forwardToTelegram("", triggerResult);
|
||||
if (triggerResult.exitCode !== 0) {
|
||||
console.error(
|
||||
`[${ts()}] Startup trigger failed (exit ${triggerResult.exitCode}). Daemon will continue running.`
|
||||
);
|
||||
console.error(`[${ts()}] Startup trigger failed (exit ${triggerResult.exitCode}). Daemon will continue running.`);
|
||||
}
|
||||
} else {
|
||||
// Bootstrap the session first so system prompt is initial context
|
||||
@@ -630,48 +599,36 @@ export async function start(args: string[] = []) {
|
||||
newSettings.heartbeat.prompt !== currentSettings.heartbeat.prompt ||
|
||||
newSettings.timezoneOffsetMinutes !== currentSettings.timezoneOffsetMinutes ||
|
||||
newSettings.timezone !== currentSettings.timezone ||
|
||||
JSON.stringify(newSettings.heartbeat.excludeWindows) !==
|
||||
JSON.stringify(currentSettings.heartbeat.excludeWindows);
|
||||
JSON.stringify(newSettings.heartbeat.excludeWindows) !== JSON.stringify(currentSettings.heartbeat.excludeWindows);
|
||||
|
||||
// Detect security config changes
|
||||
const secChanged =
|
||||
newSettings.security.level !== currentSettings.security.level ||
|
||||
newSettings.security.allowedTools.join(",") !==
|
||||
currentSettings.security.allowedTools.join(",") ||
|
||||
newSettings.security.disallowedTools.join(",") !==
|
||||
currentSettings.security.disallowedTools.join(",");
|
||||
newSettings.security.allowedTools.join(",") !== currentSettings.security.allowedTools.join(",") ||
|
||||
newSettings.security.disallowedTools.join(",") !== currentSettings.security.disallowedTools.join(",");
|
||||
|
||||
if (secChanged) {
|
||||
console.log(`[${ts()}] Security level changed → ${newSettings.security.level}`);
|
||||
}
|
||||
|
||||
if (hbChanged) {
|
||||
console.log(
|
||||
`[${ts()}] Config change detected — heartbeat: ${newSettings.heartbeat.enabled ? `every ${newSettings.heartbeat.interval}m` : "disabled"}`
|
||||
);
|
||||
console.log(`[${ts()}] Config change detected — heartbeat: ${newSettings.heartbeat.enabled ? `every ${newSettings.heartbeat.interval}m` : "disabled"}`);
|
||||
currentSettings = newSettings;
|
||||
scheduleHeartbeat();
|
||||
} else {
|
||||
currentSettings = newSettings;
|
||||
}
|
||||
|
||||
if (web) {
|
||||
currentSettings.web.enabled = true;
|
||||
currentSettings.web.port = web.port;
|
||||
}
|
||||
|
||||
// Detect job changes
|
||||
const jobNames = newJobs
|
||||
.map((j) => `${j.name}:${j.schedule}:${j.prompt}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
const oldJobNames = currentJobs
|
||||
.map((j) => `${j.name}:${j.schedule}:${j.prompt}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
const jobNames = newJobs.map((j) => `${j.name}:${j.schedule}:${j.prompt}`).sort().join("|");
|
||||
const oldJobNames = currentJobs.map((j) => `${j.name}:${j.schedule}:${j.prompt}`).sort().join("|");
|
||||
if (jobNames !== oldJobNames) {
|
||||
console.log(`[${ts()}] Jobs reloaded: ${newJobs.length} job(s)`);
|
||||
newJobs.forEach((j) => console.log(` - ${j.name} [${j.schedule}]`));
|
||||
newJobs.forEach((j) => console.log(` - ${j.name} [${j.schedule}]`));
|
||||
}
|
||||
currentJobs = newJobs;
|
||||
|
||||
@@ -724,14 +681,16 @@ export async function start(args: string[] = []) {
|
||||
await clearJobSchedule(job.name);
|
||||
console.log(`[${ts()}] Cleared schedule for one-time job: ${job.name}`);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[${ts()}] Failed to clear schedule for ${job.name}:`,
|
||||
err
|
||||
);
|
||||
console.error(`[${ts()}] Failed to clear schedule for ${job.name}:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
updateState();
|
||||
}, 60_000);
|
||||
|
||||
} catch (err) {
|
||||
console.error("[QwenClaw Start] Fatal error:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
50
src/index.ts
50
src/index.ts
@@ -7,20 +7,38 @@ import { send } from "./commands/send";
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
if (command === "--stop-all") {
|
||||
stopAll();
|
||||
} else if (command === "--stop") {
|
||||
stop();
|
||||
} else if (command === "--clear") {
|
||||
clear();
|
||||
} else if (command === "start") {
|
||||
start(args.slice(1));
|
||||
} else if (command === "status") {
|
||||
status();
|
||||
} else if (command === "telegram") {
|
||||
telegram();
|
||||
} else if (command === "send") {
|
||||
send(args.slice(1));
|
||||
} else {
|
||||
start();
|
||||
async function main() {
|
||||
console.log("[QwenClaw] Index.ts - Command:", command);
|
||||
|
||||
if (command === "--stop-all") {
|
||||
await stopAll();
|
||||
} else if (command === "--stop") {
|
||||
await stop();
|
||||
} else if (command === "--clear") {
|
||||
await clear();
|
||||
} else if (command === "start") {
|
||||
console.log("[QwenClaw] Calling start function...");
|
||||
await start(args.slice(1));
|
||||
console.log("[QwenClaw] Start function returned");
|
||||
} else if (command === "status") {
|
||||
await status();
|
||||
} else if (command === "telegram") {
|
||||
await telegram();
|
||||
} else if (command === "send") {
|
||||
await send(args.slice(1));
|
||||
} else {
|
||||
console.log("[QwenClaw] No command, starting default...");
|
||||
await start();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("[QwenClaw] Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
process.on('SIGINT', () => {
|
||||
console.log('[QwenClaw] Received SIGINT, shutting down...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -67,10 +67,14 @@ async function runQwenOnce(
|
||||
const args = [...baseArgs];
|
||||
if (model.trim()) args.push("--model", model.trim());
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
// Use qwen.cmd on Windows for proper execution
|
||||
const qwenCommand = process.platform === "win32" ? "qwen.cmd" : "qwen";
|
||||
|
||||
const proc = Bun.spawn([qwenCommand, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: buildChildEnv(baseEnv, model, api),
|
||||
shell: true,
|
||||
});
|
||||
|
||||
const [rawStdout, stderr] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user