multiclaws 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createStructuredLogger = createStructuredLogger;
4
+ /**
5
+ * Creates a structured logger that delegates to OpenClaw's base logger.
6
+ * Only outputs via baseLogger to avoid duplicate stdout writes.
7
+ */
8
+ function createStructuredLogger(baseLogger, _name = "multiclaws") {
9
+ const bridge = {
10
+ info: (message) => baseLogger.info(message),
11
+ warn: (message) => baseLogger.warn(message),
12
+ error: (message) => baseLogger.error(message),
13
+ debug: (message) => baseLogger.debug?.(message),
14
+ };
15
+ return {
16
+ logger: bridge,
17
+ };
18
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Simple sliding-window rate limiter keyed by peer ID.
3
+ * Returns true if the request should be allowed, false if rate-limited.
4
+ * Periodically prunes empty/stale keys to prevent unbounded memory growth.
5
+ */
6
+ export declare class RateLimiter {
7
+ private readonly windowMs;
8
+ private readonly maxRequests;
9
+ private readonly windows;
10
+ private pruneTimer;
11
+ constructor(opts?: {
12
+ windowMs?: number;
13
+ maxRequests?: number;
14
+ });
15
+ allow(key: string): boolean;
16
+ reset(key: string): void;
17
+ destroy(): void;
18
+ private pruneStaleKeys;
19
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RateLimiter = void 0;
4
+ /**
5
+ * Simple sliding-window rate limiter keyed by peer ID.
6
+ * Returns true if the request should be allowed, false if rate-limited.
7
+ * Periodically prunes empty/stale keys to prevent unbounded memory growth.
8
+ */
9
+ class RateLimiter {
10
+ windowMs;
11
+ maxRequests;
12
+ windows = new Map();
13
+ pruneTimer = null;
14
+ constructor(opts) {
15
+ this.windowMs = opts?.windowMs ?? 60_000;
16
+ this.maxRequests = opts?.maxRequests ?? 60;
17
+ // Prune stale keys every 5 minutes
18
+ this.pruneTimer = setInterval(() => this.pruneStaleKeys(), 5 * 60_000);
19
+ if (this.pruneTimer && typeof this.pruneTimer === "object" && "unref" in this.pruneTimer) {
20
+ this.pruneTimer.unref();
21
+ }
22
+ }
23
+ allow(key) {
24
+ const now = Date.now();
25
+ const cutoff = now - this.windowMs;
26
+ let timestamps = this.windows.get(key);
27
+ if (!timestamps) {
28
+ timestamps = [];
29
+ this.windows.set(key, timestamps);
30
+ }
31
+ // Binary search for the first timestamp within the window, then
32
+ // bulk-remove all expired entries in one splice (O(log n) + O(k)).
33
+ let lo = 0;
34
+ let hi = timestamps.length;
35
+ while (lo < hi) {
36
+ const mid = (lo + hi) >>> 1;
37
+ if (timestamps[mid] < cutoff)
38
+ lo = mid + 1;
39
+ else
40
+ hi = mid;
41
+ }
42
+ if (lo > 0)
43
+ timestamps.splice(0, lo);
44
+ if (timestamps.length >= this.maxRequests) {
45
+ return false;
46
+ }
47
+ timestamps.push(now);
48
+ return true;
49
+ }
50
+ reset(key) {
51
+ this.windows.delete(key);
52
+ }
53
+ destroy() {
54
+ if (this.pruneTimer) {
55
+ clearInterval(this.pruneTimer);
56
+ this.pruneTimer = null;
57
+ }
58
+ this.windows.clear();
59
+ }
60
+ pruneStaleKeys() {
61
+ const cutoff = Date.now() - this.windowMs;
62
+ for (const [key, timestamps] of this.windows) {
63
+ if (timestamps.length === 0 || timestamps[timestamps.length - 1] < cutoff) {
64
+ this.windows.delete(key);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ exports.RateLimiter = RateLimiter;
@@ -0,0 +1,19 @@
1
+ export type TailscaleStatus = {
2
+ status: "ready";
3
+ ip: string;
4
+ } | {
5
+ status: "needs_auth";
6
+ authUrl: string;
7
+ } | {
8
+ status: "not_installed";
9
+ } | {
10
+ status: "unavailable";
11
+ reason: string;
12
+ };
13
+ /** Check network interfaces for a Tailscale IP (100.x.x.x) — exported for fast-path checks */
14
+ export declare function getTailscaleIpFromInterfaces(): string | null;
15
+ /**
16
+ * Detect Tailscale status — does NOT install or modify system state.
17
+ * Returns one of: ready | needs_auth | not_installed | unavailable
18
+ */
19
+ export declare function detectTailscale(): Promise<TailscaleStatus>;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getTailscaleIpFromInterfaces = getTailscaleIpFromInterfaces;
7
+ exports.detectTailscale = detectTailscale;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ function run(cmd, timeoutMs = 5_000) {
11
+ return (0, node_child_process_1.execSync)(cmd, { timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"] })
12
+ .toString()
13
+ .trim();
14
+ }
15
+ function commandExists(cmd) {
16
+ try {
17
+ run(`which ${cmd}`);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ /** Check network interfaces for a Tailscale IP (100.x.x.x) — exported for fast-path checks */
25
+ function getTailscaleIpFromInterfaces() {
26
+ const interfaces = node_os_1.default.networkInterfaces();
27
+ for (const addrs of Object.values(interfaces)) {
28
+ if (!addrs)
29
+ continue;
30
+ for (const addr of addrs) {
31
+ if (addr.family === "IPv4" && addr.address.startsWith("100.")) {
32
+ return addr.address;
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ /** Ask tailscale CLI for the IP */
39
+ function getTailscaleIpFromCli() {
40
+ try {
41
+ const ip = run("tailscale ip -4").split("\n")[0];
42
+ return ip || null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** Check if tailscale daemon is running and authenticated */
49
+ function isAuthenticated() {
50
+ try {
51
+ const out = run("tailscale status --json");
52
+ const json = JSON.parse(out);
53
+ return json.BackendState === "Running";
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** Get auth URL from `tailscale up` output (non-blocking, reads stderr/stdout for a few seconds) */
60
+ async function getAuthUrl() {
61
+ return new Promise((resolve) => {
62
+ try {
63
+ // tailscale up prints the auth URL to stderr
64
+ const { spawn } = require("node:child_process");
65
+ const proc = spawn("tailscale", ["up"], { stdio: ["ignore", "pipe", "pipe"] });
66
+ let output = "";
67
+ let resolved = false;
68
+ const tryResolve = (text) => {
69
+ const match = text.match(/https:\/\/login\.tailscale\.com\/[^\s]+/);
70
+ if (match && !resolved) {
71
+ resolved = true;
72
+ proc.kill();
73
+ resolve(match[0]);
74
+ }
75
+ };
76
+ proc.stdout?.on("data", (d) => { output += d.toString(); tryResolve(output); });
77
+ proc.stderr?.on("data", (d) => { output += d.toString(); tryResolve(output); });
78
+ proc.on("close", () => { if (!resolved) {
79
+ resolved = true;
80
+ resolve(null);
81
+ } });
82
+ setTimeout(() => { if (!resolved) {
83
+ resolved = true;
84
+ proc.kill();
85
+ resolve(null);
86
+ } }, 8_000);
87
+ }
88
+ catch {
89
+ resolve(null);
90
+ }
91
+ });
92
+ }
93
+ /**
94
+ * Detect Tailscale status — does NOT install or modify system state.
95
+ * Returns one of: ready | needs_auth | not_installed | unavailable
96
+ */
97
+ async function detectTailscale() {
98
+ // Fast path: check network interfaces first (no subprocess)
99
+ const ifaceIp = getTailscaleIpFromInterfaces();
100
+ if (ifaceIp) {
101
+ return { status: "ready", ip: ifaceIp };
102
+ }
103
+ // Not installed
104
+ if (!commandExists("tailscale")) {
105
+ return { status: "not_installed" };
106
+ }
107
+ // Installed but check auth
108
+ if (isAuthenticated()) {
109
+ const ip = getTailscaleIpFromCli();
110
+ if (ip)
111
+ return { status: "ready", ip };
112
+ return { status: "unavailable", reason: "authenticated but no IP assigned" };
113
+ }
114
+ // Needs auth — try to get login URL
115
+ const authUrl = await getAuthUrl();
116
+ if (authUrl) {
117
+ return { status: "needs_auth", authUrl };
118
+ }
119
+ return { status: "unavailable", reason: "could not determine auth URL" };
120
+ }
@@ -0,0 +1,3 @@
1
+ export declare function initializeTelemetry(params?: {
2
+ enableConsoleExporter?: boolean;
3
+ }): void;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initializeTelemetry = initializeTelemetry;
4
+ const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
5
+ const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
6
+ let initialized = false;
7
+ function initializeTelemetry(params) {
8
+ if (initialized) {
9
+ return;
10
+ }
11
+ const spanProcessors = (params?.enableConsoleExporter || process.env.MULTICLAWS_OTEL_CONSOLE === "1")
12
+ ? [new sdk_trace_base_1.SimpleSpanProcessor(new sdk_trace_base_1.ConsoleSpanExporter())]
13
+ : [];
14
+ const provider = new sdk_trace_node_1.NodeTracerProvider({ spanProcessors });
15
+ provider.register();
16
+ initialized = true;
17
+ }
@@ -0,0 +1,44 @@
1
+ import type { AgentExecutor, ExecutionEventBus, RequestContext } from "@a2a-js/sdk/server";
2
+ import { type GatewayConfig } from "../infra/gateway-client";
3
+ import type { TaskTracker } from "../task/tracker";
4
+ export type A2AAdapterOptions = {
5
+ gatewayConfig: GatewayConfig | null;
6
+ taskTracker: TaskTracker;
7
+ logger: {
8
+ info: (msg: string) => void;
9
+ warn: (msg: string) => void;
10
+ error: (msg: string) => void;
11
+ };
12
+ };
13
+ /**
14
+ * Bridges the A2A protocol to OpenClaw's sessions_spawn gateway tool.
15
+ *
16
+ * When a remote agent sends a task via A2A `message/send`,
17
+ * this executor:
18
+ * 1. Records the task via TaskTracker
19
+ * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
20
+ * 3. Polls `sessions_history` until the subagent completes
21
+ * 4. Returns the final result as a Message
22
+ */
23
+ export declare class OpenClawAgentExecutor implements AgentExecutor {
24
+ private gatewayConfig;
25
+ private readonly taskTracker;
26
+ private readonly logger;
27
+ constructor(options: A2AAdapterOptions);
28
+ execute(context: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
29
+ /**
30
+ * Poll sessions_history until the subagent produces a final assistant message.
31
+ * Uses backoff: 2s, 3s, 4s, then 5s intervals.
32
+ */
33
+ private waitForCompletion;
34
+ /**
35
+ * Extract the final assistant response from session history.
36
+ * Returns null if the session is still running.
37
+ *
38
+ * Gateway /tools/invoke returns: { content: [...], details: { messages: [...], isComplete?: boolean } }
39
+ */
40
+ private extractCompletedResult;
41
+ cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void>;
42
+ updateGatewayConfig(config: GatewayConfig): void;
43
+ private publishMessage;
44
+ }
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenClawAgentExecutor = void 0;
4
+ const gateway_client_1 = require("../infra/gateway-client");
5
+ function extractTextFromMessage(message) {
6
+ if (!message.parts)
7
+ return "";
8
+ return message.parts
9
+ .filter((p) => p.kind === "text")
10
+ .map((p) => p.text)
11
+ .join("\n");
12
+ }
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ /**
17
+ * Extract the details object from a gateway /tools/invoke result.
18
+ * The result shape is: { content: [...], details: { ...actual data... } }
19
+ */
20
+ function extractDetails(result) {
21
+ if (!result || typeof result !== "object")
22
+ return null;
23
+ const r = result;
24
+ // Direct details from /tools/invoke
25
+ if (r.details && typeof r.details === "object") {
26
+ return r.details;
27
+ }
28
+ // Fallback: result itself might be the details
29
+ return r;
30
+ }
31
+ /**
32
+ * Bridges the A2A protocol to OpenClaw's sessions_spawn gateway tool.
33
+ *
34
+ * When a remote agent sends a task via A2A `message/send`,
35
+ * this executor:
36
+ * 1. Records the task via TaskTracker
37
+ * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
38
+ * 3. Polls `sessions_history` until the subagent completes
39
+ * 4. Returns the final result as a Message
40
+ */
41
+ class OpenClawAgentExecutor {
42
+ gatewayConfig;
43
+ taskTracker;
44
+ logger;
45
+ constructor(options) {
46
+ this.gatewayConfig = options.gatewayConfig;
47
+ this.taskTracker = options.taskTracker;
48
+ this.logger = options.logger;
49
+ }
50
+ async execute(context, eventBus) {
51
+ const taskText = extractTextFromMessage(context.userMessage);
52
+ const taskId = context.taskId;
53
+ if (!taskText.trim()) {
54
+ this.publishMessage(eventBus, "Error: empty task received.");
55
+ eventBus.finished();
56
+ return;
57
+ }
58
+ const fromAgent = context.userMessage.metadata?.agentUrl ?? "unknown";
59
+ this.taskTracker.create({
60
+ fromPeerId: fromAgent,
61
+ toPeerId: "local",
62
+ task: taskText,
63
+ });
64
+ if (!this.gatewayConfig) {
65
+ this.logger.error("[a2a-adapter] gateway config not available, cannot execute task");
66
+ this.taskTracker.update(taskId, { status: "failed", error: "gateway config not available" });
67
+ this.publishMessage(eventBus, "Error: gateway config not available, cannot execute task.");
68
+ eventBus.finished();
69
+ return;
70
+ }
71
+ try {
72
+ this.logger.info(`[a2a-adapter] executing task ${taskId}: ${taskText.slice(0, 100)}`);
73
+ // 1. Spawn the subagent
74
+ const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
75
+ gateway: this.gatewayConfig,
76
+ tool: "sessions_spawn",
77
+ args: {
78
+ task: taskText,
79
+ mode: "run",
80
+ },
81
+ timeoutMs: 15_000,
82
+ });
83
+ // Extract details from gateway response: { content: [...], details: { childSessionKey, ... } }
84
+ const details = extractDetails(spawnResult);
85
+ const childSessionKey = details?.childSessionKey;
86
+ if (!childSessionKey) {
87
+ throw new Error("sessions_spawn did not return a childSessionKey");
88
+ }
89
+ // 2. Poll for completion
90
+ this.logger.info(`[a2a-adapter] task ${taskId} spawned as ${childSessionKey}, waiting for result...`);
91
+ const output = await this.waitForCompletion(childSessionKey, 180_000);
92
+ // 3. Return result
93
+ this.taskTracker.update(taskId, { status: "completed", result: output });
94
+ this.logger.info(`[a2a-adapter] task ${taskId} completed`);
95
+ this.publishMessage(eventBus, output || "Task completed with no output.");
96
+ }
97
+ catch (err) {
98
+ const errorMsg = err instanceof Error ? err.message : String(err);
99
+ this.logger.error(`[a2a-adapter] task execution failed: ${errorMsg}`);
100
+ this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
101
+ this.publishMessage(eventBus, `Error: ${errorMsg}`);
102
+ }
103
+ eventBus.finished();
104
+ }
105
+ /**
106
+ * Poll sessions_history until the subagent produces a final assistant message.
107
+ * Uses backoff: 2s, 3s, 4s, then 5s intervals.
108
+ */
109
+ async waitForCompletion(sessionKey, timeoutMs) {
110
+ const gateway = this.gatewayConfig;
111
+ const startTime = Date.now();
112
+ let attempt = 0;
113
+ // Aggressive early polls, then back off: 300ms, 500ms, 1s, 2s, 3s, 3s...
114
+ const pollDelays = [300, 500, 1000, 2000, 3000];
115
+ while (Date.now() - startTime < timeoutMs) {
116
+ const delay = pollDelays[Math.min(attempt, pollDelays.length - 1)];
117
+ await sleep(delay);
118
+ attempt++;
119
+ try {
120
+ const histResult = await (0, gateway_client_1.invokeGatewayTool)({
121
+ gateway,
122
+ tool: "sessions_history",
123
+ args: {
124
+ sessionKey,
125
+ limit: 20,
126
+ includeTools: false,
127
+ },
128
+ timeoutMs: 8_000,
129
+ });
130
+ const result = this.extractCompletedResult(histResult);
131
+ if (result !== null) {
132
+ return result;
133
+ }
134
+ this.logger.info(`[a2a-adapter] poll attempt ${attempt}: session ${sessionKey} still running...`);
135
+ }
136
+ catch (err) {
137
+ this.logger.warn(`[a2a-adapter] poll attempt ${attempt} error: ${err instanceof Error ? err.message : err}`);
138
+ }
139
+ }
140
+ throw new Error(`task timed out after ${Math.round(timeoutMs / 1000)}s waiting for subagent`);
141
+ }
142
+ /**
143
+ * Extract the final assistant response from session history.
144
+ * Returns null if the session is still running.
145
+ *
146
+ * Gateway /tools/invoke returns: { content: [...], details: { messages: [...], isComplete?: boolean } }
147
+ */
148
+ extractCompletedResult(histResult) {
149
+ const details = extractDetails(histResult);
150
+ if (!details)
151
+ return null;
152
+ // Respect explicit completion flag from gateway
153
+ if (details.isComplete === false)
154
+ return null;
155
+ const messages = (details.messages ?? []);
156
+ if (messages.length === 0)
157
+ return null;
158
+ // If no explicit flag, check the last message for signs of ongoing execution
159
+ if (details.isComplete === undefined) {
160
+ const lastMsg = messages[messages.length - 1];
161
+ if (lastMsg && Array.isArray(lastMsg.content)) {
162
+ const content = lastMsg.content;
163
+ const hasToolCalls = content.some((c) => c?.type === "toolCall" || c?.type === "tool_use");
164
+ const hasText = content.some((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
165
+ if (hasToolCalls && !hasText)
166
+ return null;
167
+ }
168
+ }
169
+ // Walk backwards to find the last assistant message with text content
170
+ for (let i = messages.length - 1; i >= 0; i--) {
171
+ const msg = messages[i];
172
+ if (msg.role !== "assistant")
173
+ continue;
174
+ const content = msg.content;
175
+ if (typeof content === "string" && content.trim()) {
176
+ return content;
177
+ }
178
+ if (Array.isArray(content)) {
179
+ const parts = content;
180
+ const textParts = parts
181
+ .filter((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim())
182
+ .map((c) => c.text);
183
+ if (textParts.length > 0) {
184
+ return textParts.join("\n");
185
+ }
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ async cancelTask(taskId, eventBus) {
191
+ this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
192
+ this.publishMessage(eventBus, "Task was canceled.");
193
+ eventBus.finished();
194
+ }
195
+ updateGatewayConfig(config) {
196
+ this.gatewayConfig = config;
197
+ }
198
+ publishMessage(eventBus, text) {
199
+ const message = {
200
+ kind: "message",
201
+ role: "agent",
202
+ messageId: `msg-${Date.now()}`,
203
+ parts: [{ kind: "text", text }],
204
+ };
205
+ eventBus.publish(message);
206
+ }
207
+ }
208
+ exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
@@ -0,0 +1,13 @@
1
+ export type AgentProfile = {
2
+ ownerName: string;
3
+ /** Free-form markdown describing this agent: role, capabilities, data sources, etc. */
4
+ bio: string;
5
+ };
6
+ export declare function renderProfileDescription(profile: AgentProfile): string;
7
+ export declare class ProfileStore {
8
+ private readonly filePath;
9
+ constructor(filePath: string);
10
+ load(): Promise<AgentProfile>;
11
+ save(profile: AgentProfile): Promise<void>;
12
+ update(patch: Partial<AgentProfile>): Promise<AgentProfile>;
13
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProfileStore = void 0;
4
+ exports.renderProfileDescription = renderProfileDescription;
5
+ const json_store_1 = require("../infra/json-store");
6
+ function emptyProfile() {
7
+ return { ownerName: "", bio: "" };
8
+ }
9
+ function renderProfileDescription(profile) {
10
+ const parts = [];
11
+ if (profile.ownerName)
12
+ parts.push(profile.ownerName);
13
+ if (profile.bio)
14
+ parts.push(profile.bio);
15
+ return parts.join("\n\n") || "OpenClaw agent";
16
+ }
17
+ class ProfileStore {
18
+ filePath;
19
+ constructor(filePath) {
20
+ this.filePath = filePath;
21
+ }
22
+ async load() {
23
+ return await (0, json_store_1.readJsonWithFallback)(this.filePath, emptyProfile());
24
+ }
25
+ async save(profile) {
26
+ await (0, json_store_1.writeJsonAtomically)(this.filePath, profile);
27
+ }
28
+ async update(patch) {
29
+ const profile = await this.load();
30
+ if (patch.ownerName !== undefined)
31
+ profile.ownerName = patch.ownerName;
32
+ if (patch.bio !== undefined)
33
+ profile.bio = patch.bio;
34
+ await this.save(profile);
35
+ return profile;
36
+ }
37
+ }
38
+ exports.ProfileStore = ProfileStore;
@@ -0,0 +1,26 @@
1
+ export type AgentRecord = {
2
+ url: string;
3
+ name: string;
4
+ description: string;
5
+ skills: string[];
6
+ apiKey?: string;
7
+ addedAtMs: number;
8
+ lastSeenAtMs: number;
9
+ };
10
+ export declare class AgentRegistry {
11
+ private readonly filePath;
12
+ constructor(filePath: string);
13
+ private readStore;
14
+ add(params: {
15
+ url: string;
16
+ name: string;
17
+ description?: string;
18
+ skills?: string[];
19
+ apiKey?: string;
20
+ }): Promise<AgentRecord>;
21
+ remove(url: string): Promise<boolean>;
22
+ list(): Promise<AgentRecord[]>;
23
+ get(url: string): Promise<AgentRecord | null>;
24
+ updateDescription(url: string, description: string): Promise<void>;
25
+ updateLastSeen(url: string): Promise<void>;
26
+ }