feat: url watch — broker polls URLs, notifies on change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1554,6 +1554,35 @@ Your message mode is "${messageMode}".
|
||||
return text(`Skill "${result.name}" deployed.\nFiles: ${result.files.join(", ")}`);
|
||||
}
|
||||
|
||||
// --- URL Watch ---
|
||||
case "mesh_watch": {
|
||||
const { url, mode, extract, interval, notify_on, headers, label } = (args ?? {}) as any;
|
||||
if (!url) return text("mesh_watch: `url` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_watch: not connected", true);
|
||||
const result = await client.watch(url, { mode, extract, interval, notify_on, headers, label });
|
||||
if (result.error) return text(`mesh_watch: ${result.error}`, true);
|
||||
return text(`Watching "${label ?? url}" (${result.mode}, every ${result.interval}s)\nWatch ID: ${result.watchId}`);
|
||||
}
|
||||
case "mesh_unwatch": {
|
||||
const { watch_id } = (args ?? {}) as { watch_id?: string };
|
||||
if (!watch_id) return text("mesh_unwatch: `watch_id` required", true);
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_unwatch: not connected", true);
|
||||
await client.unwatch(watch_id);
|
||||
return text(`Watch ${watch_id} stopped.`);
|
||||
}
|
||||
case "mesh_watches": {
|
||||
const client = allClients()[0];
|
||||
if (!client) return text("mesh_watches: not connected", true);
|
||||
const watches = await client.watchList();
|
||||
if (watches.length === 0) return text("No active watches.");
|
||||
const lines = watches.map((w: any) =>
|
||||
`- **${w.id}** ${w.label ? `(${w.label}) ` : ""}${w.url}\n mode: ${w.mode} | interval: ${w.interval}s | last: ${w.lastValue?.slice(0, 30) ?? "pending"} | checked: ${w.lastCheck ?? "never"}`
|
||||
);
|
||||
return text(`${watches.length} active watch(es):\n\n${lines.join("\n")}`);
|
||||
}
|
||||
|
||||
default:
|
||||
return text(`Unknown tool: ${name}`, true);
|
||||
}
|
||||
|
||||
@@ -972,4 +972,38 @@ export const TOOLS: Tool[] = [
|
||||
description: "Remove a credential from your vault.",
|
||||
inputSchema: { type: "object", properties: { key: { type: "string" } }, required: ["key"] },
|
||||
},
|
||||
|
||||
// --- URL Watch tools ---
|
||||
|
||||
{
|
||||
name: "mesh_watch",
|
||||
description: "Watch a URL for changes. The broker polls it at the given interval and notifies you when the response changes. Works with any URL — websites (hash mode), JSON APIs (json mode), or status codes (status mode).",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string", description: "URL to watch" },
|
||||
mode: { type: "string", enum: ["hash", "json", "status"], description: "Detection mode: hash (SHA-256 of body), json (extract jsonpath value), status (HTTP status code). Default: hash" },
|
||||
extract: { type: "string", description: "For json mode: dot path to extract (e.g. 'status' or 'data.deployments[0].status')" },
|
||||
interval: { type: "number", description: "Poll interval in seconds (min: 5, default: 30)" },
|
||||
notify_on: { type: "string", description: "When to notify: 'change' (default), 'match:<value>', 'not_match:<value>'" },
|
||||
headers: { type: "object", description: "Optional HTTP headers (e.g. for auth)" },
|
||||
label: { type: "string", description: "Human-readable label for this watch" },
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mesh_unwatch",
|
||||
description: "Stop watching a URL.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { watch_id: { type: "string" } },
|
||||
required: ["watch_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mesh_watches",
|
||||
description: "List your active URL watches.",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1346,6 +1346,38 @@ export class BrokerClient {
|
||||
});
|
||||
}
|
||||
|
||||
// --- URL Watch ---
|
||||
|
||||
private watchAckResolvers = new Map<string, { resolve: (result: any) => void; timer: NodeJS.Timeout }>();
|
||||
private watchListResolvers = new Map<string, { resolve: (watches: any[]) => void; timer: NodeJS.Timeout }>();
|
||||
|
||||
async watch(url: string, opts?: { mode?: string; extract?: string; interval?: number; notify_on?: string; headers?: Record<string, string>; label?: string }): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
const reqId = `watch_${Date.now()}`;
|
||||
const timer = setTimeout(() => { this.watchAckResolvers.delete(reqId); resolve({ error: "timeout" }); }, 10_000);
|
||||
this.watchAckResolvers.set(reqId, { resolve, timer });
|
||||
this.sendRaw({ type: "watch", url, ...opts, _reqId: reqId } as any);
|
||||
});
|
||||
}
|
||||
|
||||
async unwatch(watchId: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const reqId = `unwatch_${Date.now()}`;
|
||||
const timer = setTimeout(() => { this.watchAckResolvers.delete(reqId); resolve(false); }, 10_000);
|
||||
this.watchAckResolvers.set(reqId, { resolve: () => resolve(true), timer });
|
||||
this.sendRaw({ type: "unwatch", watchId, _reqId: reqId } as any);
|
||||
});
|
||||
}
|
||||
|
||||
async watchList(): Promise<any[]> {
|
||||
return new Promise(resolve => {
|
||||
const reqId = `watchlist_${Date.now()}`;
|
||||
const timer = setTimeout(() => { this.watchListResolvers.delete(reqId); resolve([]); }, 10_000);
|
||||
this.watchListResolvers.set(reqId, { resolve, timer });
|
||||
this.sendRaw({ type: "watch_list", _reqId: reqId } as any);
|
||||
});
|
||||
}
|
||||
|
||||
async getServiceTools(serviceName: string): Promise<any[]> {
|
||||
// Check cached catalog first
|
||||
const cached = this._serviceCatalog.find(s => s.name === serviceName);
|
||||
@@ -1993,6 +2025,24 @@ export class BrokerClient {
|
||||
r.resolve({ name: (msg as any).name, files: (msg as any).files ?? [] });
|
||||
}
|
||||
}
|
||||
if (msg.type === "watch_ack") {
|
||||
const reqId = (msg as any)._reqId;
|
||||
if (reqId && this.watchAckResolvers.has(reqId)) {
|
||||
const r = this.watchAckResolvers.get(reqId)!;
|
||||
clearTimeout(r.timer);
|
||||
this.watchAckResolvers.delete(reqId);
|
||||
r.resolve(msg);
|
||||
}
|
||||
}
|
||||
if (msg.type === "watch_list_result") {
|
||||
const reqId = (msg as any)._reqId;
|
||||
if (reqId && this.watchListResolvers.has(reqId)) {
|
||||
const r = this.watchListResolvers.get(reqId)!;
|
||||
clearTimeout(r.timer);
|
||||
this.watchListResolvers.delete(reqId);
|
||||
r.resolve((msg as any).watches ?? []);
|
||||
}
|
||||
}
|
||||
if (msg.type === "error") {
|
||||
this.debug(`broker error: ${msg.code} ${msg.message}`);
|
||||
const id = msg.id ? String(msg.id) : null;
|
||||
|
||||
Reference in New Issue
Block a user