Skip to content

Commit 83f96d3

Browse files
shreyas-lyzrclaude
andcommitted
v1.1.0 — cron scheduler, installer auto-resume
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c7937ae commit 83f96d3

7 files changed

Lines changed: 756 additions & 5 deletions

File tree

install.sh

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ rows=(
4545
text=(
4646
""
4747
""
48-
"${RED}${BOLD}GitClaw v0.4.0${RESET}"
48+
"${RED}${BOLD}GitClaw v1.1.0${RESET}"
4949
"${GRAY}A universal git-native multimodal always learning AI Agent${RESET}"
5050
"${GRAY}(TinyHuman)${RESET}"
5151
""
@@ -114,6 +114,37 @@ else
114114
fi
115115
echo ""
116116

117+
# ── Auto-resume existing setup ──────────────────────────────────
118+
PROJECT_DIR="${HOME}/assistant"
119+
if [ -d "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/agent.yaml" ]; then
120+
echo -e " ${GREEN}${NC} Found existing assistant at ${DIM}${PROJECT_DIR}${NC}"
121+
122+
# Re-export .env keys into current shell
123+
if [ -f "$PROJECT_DIR/.env" ]; then
124+
set -a
125+
source "$PROJECT_DIR/.env"
126+
set +a
127+
echo -e " ${GREEN}${NC} Loaded keys from ${DIM}${PROJECT_DIR}/.env${NC}"
128+
fi
129+
130+
# Extract model from agent.yaml (look for preferred: "..." under model:)
131+
MODEL=$(grep -A1 '^model:' "$PROJECT_DIR/agent.yaml" | grep 'preferred:' | sed 's/.*preferred:[[:space:]]*["'"'"']\?\([^"'"'"']*\)["'"'"']\?.*/\1/' | head -1)
132+
MODEL="${MODEL:-anthropic:claude-sonnet-4-6}"
133+
134+
# Determine adapter from available keys
135+
if [ -n "${GEMINI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
136+
ADAPTER_LABEL="Gemini Live"
137+
else
138+
ADAPTER_LABEL="OpenAI Realtime"
139+
fi
140+
141+
PORT="${PORT:-3333}"
142+
143+
echo -e " ${DIM}Resuming with: ${MODEL} on port ${PORT}${NC}"
144+
echo ""
145+
146+
else
147+
117148
# ── Setup Mode Selection ─────────────────────────────────────────
118149
echo -e " ${BOLD}How would you like to set up?${NC}"
119150
echo ""
@@ -308,6 +339,8 @@ else
308339

309340
fi
310341

342+
fi # end auto-resume / interactive setup
343+
311344
# ═══════════════════════════════════════════════════════════════════
312345
# LAUNCH SUMMARY
313346
# ═══════════════════════════════════════════════════════════════════

package-lock.json

Lines changed: 32 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitclaw",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "A universal git-native multimodal always learning AI Agent (TinyHuman)",
55
"author": "shreyaskapale",
66
"license": "MIT",
@@ -52,6 +52,7 @@
5252
"@sinclair/typebox": "^0.34.41",
5353
"baileys": "^7.0.0-rc.9",
5454
"js-yaml": "^4.1.0",
55+
"node-cron": "^3.0.3",
5556
"ws": "^8.19.0"
5657
},
5758
"peerDependencies": {
@@ -65,6 +66,7 @@
6566
"devDependencies": {
6667
"@types/js-yaml": "^4.0.9",
6768
"@types/node": "^22.0.0",
69+
"@types/node-cron": "^3.0.11",
6870
"@types/ws": "^8.18.1",
6971
"typescript": "^5.7.0"
7072
}

src/schedule-runner.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import cron, { type ScheduledTask } from "node-cron";
2+
import { discoverSchedules, updateScheduleMeta, type ScheduleDefinition } from "./schedules.js";
3+
import { mkdirSync, appendFileSync } from "fs";
4+
import { join } from "path";
5+
import type { ServerMessage } from "./voice/adapter.js";
6+
7+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
8+
9+
export interface SchedulerOptions {
10+
agentDir: string;
11+
model?: string;
12+
env?: string;
13+
runPrompt: (prompt: string) => Promise<string>;
14+
broadcastToBrowsers: (msg: ServerMessage) => void;
15+
appendToHistory: (msg: any) => void;
16+
}
17+
18+
const activeTasks = new Map<string, ScheduledTask>();
19+
const activeTimers = new Map<string, ReturnType<typeof setTimeout>>();
20+
const runningJobs = new Set<string>();
21+
22+
export async function startScheduler(opts: SchedulerOptions): Promise<void> {
23+
const schedules = await discoverSchedules(opts.agentDir);
24+
let activeCount = 0;
25+
26+
for (const schedule of schedules) {
27+
if (!schedule.enabled) continue;
28+
29+
if (schedule.mode === "once" && schedule.runAt) {
30+
// One-time schedule via runAt datetime
31+
const delay = new Date(schedule.runAt).getTime() - Date.now();
32+
if (delay <= 0) {
33+
console.log(dim(`[scheduler] "${schedule.id}" runAt is in the past — skipping`));
34+
continue;
35+
}
36+
const timer = setTimeout(() => {
37+
executeScheduledJob(schedule, opts, true);
38+
}, delay);
39+
activeTimers.set(schedule.id, timer);
40+
const when = new Date(schedule.runAt).toLocaleString();
41+
console.log(dim(`[scheduler] "${schedule.id}" scheduled once at ${when} (in ${Math.round(delay / 1000)}s)`));
42+
activeCount++;
43+
} else if (schedule.mode === "once" && schedule.cron) {
44+
// One-time schedule via cron — fires once then auto-disables
45+
if (!cron.validate(schedule.cron)) {
46+
console.log(dim(`[scheduler] Invalid cron for "${schedule.id}": ${schedule.cron} — skipping`));
47+
continue;
48+
}
49+
const task = cron.schedule(schedule.cron, () => {
50+
executeScheduledJob(schedule, opts, true);
51+
});
52+
activeTasks.set(schedule.id, task);
53+
activeCount++;
54+
} else {
55+
// Repeating cron schedule
56+
if (!cron.validate(schedule.cron)) {
57+
console.log(dim(`[scheduler] Invalid cron for "${schedule.id}": ${schedule.cron} — skipping`));
58+
continue;
59+
}
60+
const task = cron.schedule(schedule.cron, () => {
61+
executeScheduledJob(schedule, opts, false);
62+
});
63+
activeTasks.set(schedule.id, task);
64+
activeCount++;
65+
}
66+
}
67+
68+
console.log(dim(`[scheduler] Loaded ${schedules.length} schedules (${activeCount} active)`));
69+
}
70+
71+
export function stopScheduler(): void {
72+
for (const [, task] of activeTasks) {
73+
task.stop();
74+
}
75+
activeTasks.clear();
76+
for (const [, timer] of activeTimers) {
77+
clearTimeout(timer);
78+
}
79+
activeTimers.clear();
80+
console.log(dim("[scheduler] Stopped all scheduled tasks"));
81+
}
82+
83+
export async function reloadSchedules(opts: SchedulerOptions): Promise<void> {
84+
stopScheduler();
85+
await startScheduler(opts);
86+
}
87+
88+
export async function executeScheduledJob(schedule: ScheduleDefinition, opts: SchedulerOptions, disableAfterRun = false): Promise<void> {
89+
if (runningJobs.has(schedule.id)) {
90+
console.log(dim(`[scheduler] Skipping "${schedule.id}" — already running`));
91+
return;
92+
}
93+
runningJobs.add(schedule.id);
94+
const ts = new Date().toISOString();
95+
console.log(dim(`[scheduler] Running "${schedule.id}" at ${ts}`));
96+
97+
// Broadcast schedule start to chat
98+
const startMsg = { type: "schedule_start", id: schedule.id, prompt: schedule.prompt, ts } as any;
99+
opts.broadcastToBrowsers(startMsg as ServerMessage);
100+
opts.appendToHistory(startMsg);
101+
102+
let result = "";
103+
let success = true;
104+
105+
try {
106+
result = await opts.runPrompt(schedule.prompt);
107+
} catch (err: any) {
108+
result = err.message || "Unknown error";
109+
success = false;
110+
}
111+
112+
// Write to JSONL log
113+
try {
114+
const logDir = join(opts.agentDir, ".gitagent", "schedule-logs");
115+
mkdirSync(logDir, { recursive: true });
116+
const logFile = join(logDir, `${schedule.id}.jsonl`);
117+
const logEntry = JSON.stringify({ ts, success, result: result.slice(0, 5000) }) + "\n";
118+
appendFileSync(logFile, logEntry, "utf-8");
119+
} catch {
120+
// Log write failure is non-fatal
121+
}
122+
123+
// Update schedule metadata (and auto-disable for "once" mode)
124+
try {
125+
await updateScheduleMeta(opts.agentDir, schedule.id, {
126+
lastRunAt: ts,
127+
lastResult: success ? "success" : "error",
128+
...(disableAfterRun ? { enabled: false } : {}),
129+
});
130+
} catch {
131+
// Meta update failure is non-fatal
132+
}
133+
134+
// Stop the cron task / clear timer if this was a one-time job
135+
if (disableAfterRun) {
136+
const task = activeTasks.get(schedule.id);
137+
if (task) { task.stop(); activeTasks.delete(schedule.id); }
138+
const timer = activeTimers.get(schedule.id);
139+
if (timer) { clearTimeout(timer); activeTimers.delete(schedule.id); }
140+
console.log(dim(`[scheduler] "${schedule.id}" auto-disabled (run-once)`));
141+
}
142+
143+
// Broadcast to connected browsers and persist to chat history
144+
const endMsg = {
145+
type: "schedule_result",
146+
id: schedule.id,
147+
result: result.slice(0, 2000),
148+
success,
149+
ts,
150+
} as any;
151+
opts.broadcastToBrowsers(endMsg as ServerMessage);
152+
opts.appendToHistory(endMsg);
153+
154+
runningJobs.delete(schedule.id);
155+
console.log(dim(`[scheduler] "${schedule.id}" completed (${success ? "success" : "error"})`));
156+
}

0 commit comments

Comments
 (0)