"use strict"; /** * HarborForge Calendar Bridge Client * * PLG-CAL-001: Handles HTTP communication between the OpenClaw plugin * and the HarborForge backend for Calendar heartbeat and slot updates. * * Request authentication: * • X-Agent-ID header — set to process.env.AGENT_ID * • X-Claw-Identifier header — set to the server's claw_identifier * (from plugin config or hostname fallback) * * Base URL: * Derived from plugin config: backendUrl + "/calendar" * Default backendUrl: "https://monitor.hangman-lab.top" * * Endpoints used: * GET /calendar/agent/heartbeat — fetch pending slots * PATCH /calendar/slots/{id}/agent-update — update real slot status * PATCH /calendar/slots/virtual/{vid}/agent-update — update virtual slot status * * References: * • NEXT_WAVE_DEV_DIRECTION.md §6.1 (Heartbeat flow) * • HarborForge.Backend/app/services/agent_heartbeat.py (BE-AGT-001) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CalendarBridgeClient = void 0; exports.createCalendarBridgeClient = createCalendarBridgeClient; class CalendarBridgeClient { baseUrl; config; timeoutMs; constructor(config) { this.baseUrl = config.backendUrl.replace(/\/$/, ''); // strip trailing slash this.config = { timeoutMs: 5000, ...config, }; this.timeoutMs = this.config.timeoutMs; } /** * Fetch today's pending calendar slots for this agent. * * Heartbeat flow (§6.1): * 1. Plugin sends heartbeat every minute * 2. Backend returns slots where status is NotStarted or Deferred * AND scheduled_at <= now * 3. Plugin selects highest-priority slot (if any) * 4. For remaining slots, plugin sets status = Deferred + priority += 1 * * @returns CalendarHeartbeatResponse or null if the backend is unreachable */ async heartbeat() { const url = `${this.baseUrl}/calendar/agent/heartbeat`; const body = { claw_identifier: this.config.clawIdentifier, agent_id: this.config.agentId, }; try { const response = await this.fetchJson(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Agent-ID': this.config.agentId, 'X-Claw-Identifier': this.config.clawIdentifier, }, body: JSON.stringify(body), }); return response; } catch (err) { // Non-fatal: backend unreachable — return null, plugin continues return null; } } /** * Update a real (materialized) slot's status after agent execution. * * Used by the plugin to report: * - Slot attended (attended=true, started_at=now, status=Ongoing) * - Slot finished (actual_duration set, status=Finished) * - Slot deferred (status=Deferred, priority += 1) * - Slot aborted (status=Aborted) * * @param slotId Real slot DB id * @param update Status update payload * @returns true on success, false on failure */ async updateSlot(slotId, update) { const url = `${this.baseUrl}/calendar/slots/${slotId}/agent-update`; return this.postBoolean(url, update); } /** * Update a virtual (plan-generated) slot's status after agent execution. * * When updating a virtual slot, the backend first materializes it * (creates a real TimeSlot row), then applies the update. * The returned slot will have a real id on subsequent calls. * * @param virtualId Virtual slot id in format "plan-{plan_id}-{date}" * @param update Status update payload * @returns Updated CalendarSlotResponse on success, null on failure */ async updateVirtualSlot(virtualId, update) { const url = `${this.baseUrl}/calendar/slots/virtual/${encodeURIComponent(virtualId)}/agent-update`; try { const response = await this.fetchJson(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-Agent-ID': this.config.agentId, 'X-Claw-Identifier': this.config.clawIdentifier, }, body: JSON.stringify(update), }); return response?.slot ?? null; } catch { return null; } } /** * Report the agent's current runtime status to HarborForge. * * Used to push agent status transitions: * idle → busy / on_call (when starting a slot) * busy / on_call → idle (when finishing a slot) * → exhausted (on rate-limit / billing error, with recovery_at) * → offline (after 2 min with no heartbeat) * * @param status New agent status * @param recoveryAt ISO timestamp for expected Exhausted recovery (optional) * @param exhaustReason "rate_limit" | "billing" (required if status=exhausted) */ async reportAgentStatus(params) { const url = `${this.baseUrl}/calendar/agent/status`; const body = { agent_id: this.config.agentId, claw_identifier: this.config.clawIdentifier, ...params, }; return this.postBoolean(url, body); } // ------------------------------------------------------------------------- // Internal helpers // ------------------------------------------------------------------------- async fetchJson(url, init) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.timeoutMs); try { const response = await fetch(url, { ...init, signal: controller.signal, }); clearTimeout(timeout); if (!response.ok) return null; return (await response.json()); } catch { clearTimeout(timeout); return null; } } async postBoolean(url, body) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.timeoutMs); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Agent-ID': this.config.agentId, 'X-Claw-Identifier': this.config.clawIdentifier, }, body: JSON.stringify(body), signal: controller.signal, }); clearTimeout(timeout); return response.ok; } catch { clearTimeout(timeout); return false; } } } exports.CalendarBridgeClient = CalendarBridgeClient; // --------------------------------------------------------------------------- // Utility: build CalendarBridgeConfig from plugin API context // --------------------------------------------------------------------------- const os_1 = require("os"); const live_config_1 = require("../core/live-config"); /** * Build a CalendarBridgeClient from the OpenClaw plugin API context. * * @param api OpenClaw plugin API (register() receives this) * @param fallbackUrl Fallback backend URL if not configured * @param agentId $AGENT_ID from OpenClaw environment */ function createCalendarBridgeClient(api, fallbackUrl, agentId) { const baseConfig = (0, live_config_1.getLivePluginConfig)(api, { backendUrl: fallbackUrl, identifier: (0, os_1.hostname)(), }); const clawIdentifier = baseConfig.identifier || (0, os_1.hostname)(); return new CalendarBridgeClient({ backendUrl: baseConfig.backendUrl || fallbackUrl, clawIdentifier, agentId, timeoutMs: 5000, }); } //# sourceMappingURL=calendar-bridge.js.map