@hasna/connectors 1.1.14 → 1.1.15

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.
package/bin/index.js CHANGED
@@ -10117,6 +10117,24 @@ function migrate(db) {
10117
10117
  created_at TEXT NOT NULL
10118
10118
  )
10119
10119
  `);
10120
+ db.run(`
10121
+ CREATE TABLE IF NOT EXISTS resource_locks (
10122
+ id TEXT PRIMARY KEY,
10123
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('connector', 'agent', 'profile', 'token')),
10124
+ resource_id TEXT NOT NULL,
10125
+ agent_id TEXT NOT NULL,
10126
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
10127
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
10128
+ expires_at TEXT NOT NULL
10129
+ )
10130
+ `);
10131
+ db.run(`
10132
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
10133
+ ON resource_locks(resource_type, resource_id)
10134
+ WHERE lock_type = 'exclusive'
10135
+ `);
10136
+ db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id)`);
10137
+ db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at)`);
10120
10138
  }
10121
10139
  var DB_DIR, DB_PATH, _db = null;
10122
10140
  var init_database = __esm(() => {
@@ -10190,6 +10208,78 @@ var init_agents = __esm(() => {
10190
10208
  AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
10191
10209
  });
10192
10210
 
10211
+ // src/db/rate.ts
10212
+ function ensureRateTable(db) {
10213
+ db.run(`
10214
+ CREATE TABLE IF NOT EXISTS connector_rate_usage (
10215
+ agent_id TEXT NOT NULL,
10216
+ connector TEXT NOT NULL,
10217
+ window_start TEXT NOT NULL,
10218
+ call_count INTEGER NOT NULL DEFAULT 0,
10219
+ PRIMARY KEY (agent_id, connector, window_start)
10220
+ )
10221
+ `);
10222
+ db.run(`CREATE INDEX IF NOT EXISTS idx_rate_usage_window ON connector_rate_usage(connector, window_start)`);
10223
+ }
10224
+ function countActiveAgents(db) {
10225
+ const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS2).toISOString();
10226
+ const row = db.query("SELECT COUNT(*) as count FROM agents WHERE last_seen_at > ?").get(cutoff);
10227
+ return Math.max(1, row?.count ?? 1);
10228
+ }
10229
+ function currentWindowStart() {
10230
+ const now3 = Date.now();
10231
+ const windowMs = WINDOW_SECONDS * 1000;
10232
+ return new Date(Math.floor(now3 / windowMs) * windowMs).toISOString();
10233
+ }
10234
+ function checkRateBudget(agentId, connector, connectorLimit, consume = true, db) {
10235
+ const d = db ?? getDatabase();
10236
+ ensureRateTable(d);
10237
+ const activeAgents = countActiveAgents(d);
10238
+ const budget = Math.max(1, Math.floor(connectorLimit / activeAgents));
10239
+ const windowStart = currentWindowStart();
10240
+ const windowMs = WINDOW_SECONDS * 1000;
10241
+ const windowEnd = new Date(Math.floor(Date.now() / windowMs) * windowMs + windowMs);
10242
+ const windowResetsIn = windowEnd.getTime() - Date.now();
10243
+ const row = d.query("SELECT call_count FROM connector_rate_usage WHERE agent_id = ? AND connector = ? AND window_start = ?").get(agentId, connector, windowStart);
10244
+ const used = row?.call_count ?? 0;
10245
+ if (used >= budget) {
10246
+ return {
10247
+ exceeded: true,
10248
+ connector,
10249
+ agent_id: agentId,
10250
+ budget,
10251
+ used,
10252
+ active_agents: activeAgents,
10253
+ window_resets_in_ms: windowResetsIn,
10254
+ message: `Rate budget exceeded for "${connector}" (${used}/${budget} calls used, ${activeAgents} active agent${activeAgents === 1 ? "" : "s"} sharing limit of ${connectorLimit}/min). Resets in ${Math.ceil(windowResetsIn / 1000)}s.`
10255
+ };
10256
+ }
10257
+ if (consume) {
10258
+ d.run(`INSERT INTO connector_rate_usage (agent_id, connector, window_start, call_count)
10259
+ VALUES (?, ?, ?, 1)
10260
+ ON CONFLICT(agent_id, connector, window_start) DO UPDATE SET call_count = call_count + 1`, [agentId, connector, windowStart]);
10261
+ }
10262
+ return {
10263
+ connector,
10264
+ agent_id: agentId,
10265
+ limit: connectorLimit,
10266
+ active_agents: activeAgents,
10267
+ budget,
10268
+ used: consume ? used + 1 : used,
10269
+ remaining: consume ? budget - used - 1 : budget - used,
10270
+ window_start: windowStart,
10271
+ window_resets_in_ms: windowResetsIn
10272
+ };
10273
+ }
10274
+ function getRateBudget(agentId, connector, connectorLimit, db) {
10275
+ return checkRateBudget(agentId, connector, connectorLimit, false, db);
10276
+ }
10277
+ var AGENT_ACTIVE_WINDOW_MS2, WINDOW_SECONDS = 60;
10278
+ var init_rate = __esm(() => {
10279
+ init_database();
10280
+ AGENT_ACTIVE_WINDOW_MS2 = 30 * 60 * 1000;
10281
+ });
10282
+
10193
10283
  // src/server/serve.ts
10194
10284
  var exports_serve = {};
10195
10285
  __export(exports_serve, {
@@ -10493,6 +10583,14 @@ Dashboard not found at: ${dashboardDir}`);
10493
10583
  deleteAgent(agent.id);
10494
10584
  return json({ success: true }, 200, port);
10495
10585
  }
10586
+ const rateMatch = path.match(/^\/api\/rate\/([^/]+)\/([^/]+)$/);
10587
+ if (rateMatch && method === "GET") {
10588
+ const [, agentId, connector] = rateMatch;
10589
+ const limit = parseInt(url2.searchParams.get("limit") || "60", 10);
10590
+ const consume = url2.searchParams.get("consume") === "true";
10591
+ const result = consume ? checkRateBudget(agentId, connector, limit) : getRateBudget(agentId, connector, limit);
10592
+ return json(result, 200, port);
10593
+ }
10496
10594
  const profilesMatch = path.match(/^\/api\/connectors\/([^/]+)\/profiles$/);
10497
10595
  if (profilesMatch && method === "GET") {
10498
10596
  const name = profilesMatch[1];
@@ -10712,6 +10810,7 @@ Dashboard not found at: ${dashboardDir}`);
10712
10810
  var activityLog, MAX_ACTIVITY_LOG = 100, MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE;
10713
10811
  var init_serve = __esm(() => {
10714
10812
  init_agents();
10813
+ init_rate();
10715
10814
  init_registry();
10716
10815
  init_installer();
10717
10816
  init_auth();
package/bin/mcp.js CHANGED
@@ -26009,6 +26009,24 @@ function migrate(db) {
26009
26009
  created_at TEXT NOT NULL
26010
26010
  )
26011
26011
  `);
26012
+ db.run(`
26013
+ CREATE TABLE IF NOT EXISTS resource_locks (
26014
+ id TEXT PRIMARY KEY,
26015
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('connector', 'agent', 'profile', 'token')),
26016
+ resource_id TEXT NOT NULL,
26017
+ agent_id TEXT NOT NULL,
26018
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
26019
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
26020
+ expires_at TEXT NOT NULL
26021
+ )
26022
+ `);
26023
+ db.run(`
26024
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
26025
+ ON resource_locks(resource_type, resource_id)
26026
+ WHERE lock_type = 'exclusive'
26027
+ `);
26028
+ db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id)`);
26029
+ db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at)`);
26012
26030
  }
26013
26031
 
26014
26032
  // src/db/agents.ts
@@ -26068,10 +26086,79 @@ function listAgents(db) {
26068
26086
  const d = db ?? getDatabase();
26069
26087
  return d.query("SELECT * FROM agents ORDER BY name").all();
26070
26088
  }
26089
+
26090
+ // src/db/rate.ts
26091
+ var AGENT_ACTIVE_WINDOW_MS2 = 30 * 60 * 1000;
26092
+ var WINDOW_SECONDS = 60;
26093
+ function ensureRateTable(db) {
26094
+ db.run(`
26095
+ CREATE TABLE IF NOT EXISTS connector_rate_usage (
26096
+ agent_id TEXT NOT NULL,
26097
+ connector TEXT NOT NULL,
26098
+ window_start TEXT NOT NULL,
26099
+ call_count INTEGER NOT NULL DEFAULT 0,
26100
+ PRIMARY KEY (agent_id, connector, window_start)
26101
+ )
26102
+ `);
26103
+ db.run(`CREATE INDEX IF NOT EXISTS idx_rate_usage_window ON connector_rate_usage(connector, window_start)`);
26104
+ }
26105
+ function countActiveAgents(db) {
26106
+ const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS2).toISOString();
26107
+ const row = db.query("SELECT COUNT(*) as count FROM agents WHERE last_seen_at > ?").get(cutoff);
26108
+ return Math.max(1, row?.count ?? 1);
26109
+ }
26110
+ function currentWindowStart() {
26111
+ const now3 = Date.now();
26112
+ const windowMs = WINDOW_SECONDS * 1000;
26113
+ return new Date(Math.floor(now3 / windowMs) * windowMs).toISOString();
26114
+ }
26115
+ function checkRateBudget(agentId, connector, connectorLimit, consume = true, db) {
26116
+ const d = db ?? getDatabase();
26117
+ ensureRateTable(d);
26118
+ const activeAgents = countActiveAgents(d);
26119
+ const budget = Math.max(1, Math.floor(connectorLimit / activeAgents));
26120
+ const windowStart = currentWindowStart();
26121
+ const windowMs = WINDOW_SECONDS * 1000;
26122
+ const windowEnd = new Date(Math.floor(Date.now() / windowMs) * windowMs + windowMs);
26123
+ const windowResetsIn = windowEnd.getTime() - Date.now();
26124
+ const row = d.query("SELECT call_count FROM connector_rate_usage WHERE agent_id = ? AND connector = ? AND window_start = ?").get(agentId, connector, windowStart);
26125
+ const used = row?.call_count ?? 0;
26126
+ if (used >= budget) {
26127
+ return {
26128
+ exceeded: true,
26129
+ connector,
26130
+ agent_id: agentId,
26131
+ budget,
26132
+ used,
26133
+ active_agents: activeAgents,
26134
+ window_resets_in_ms: windowResetsIn,
26135
+ message: `Rate budget exceeded for "${connector}" (${used}/${budget} calls used, ${activeAgents} active agent${activeAgents === 1 ? "" : "s"} sharing limit of ${connectorLimit}/min). Resets in ${Math.ceil(windowResetsIn / 1000)}s.`
26136
+ };
26137
+ }
26138
+ if (consume) {
26139
+ d.run(`INSERT INTO connector_rate_usage (agent_id, connector, window_start, call_count)
26140
+ VALUES (?, ?, ?, 1)
26141
+ ON CONFLICT(agent_id, connector, window_start) DO UPDATE SET call_count = call_count + 1`, [agentId, connector, windowStart]);
26142
+ }
26143
+ return {
26144
+ connector,
26145
+ agent_id: agentId,
26146
+ limit: connectorLimit,
26147
+ active_agents: activeAgents,
26148
+ budget,
26149
+ used: consume ? used + 1 : used,
26150
+ remaining: consume ? budget - used - 1 : budget - used,
26151
+ window_start: windowStart,
26152
+ window_resets_in_ms: windowResetsIn
26153
+ };
26154
+ }
26155
+ function getRateBudget(agentId, connector, connectorLimit, db) {
26156
+ return checkRateBudget(agentId, connector, connectorLimit, false, db);
26157
+ }
26071
26158
  // package.json
26072
26159
  var package_default = {
26073
26160
  name: "@hasna/connectors",
26074
- version: "1.1.13",
26161
+ version: "1.1.14",
26075
26162
  description: "Open source connector library - Install API connectors with a single command",
26076
26163
  type: "module",
26077
26164
  bin: {
@@ -26668,6 +26755,30 @@ server.registerTool("list_agents", {
26668
26755
  const agents = listAgents();
26669
26756
  return { content: [{ type: "text", text: JSON.stringify(agents, null, 2) }] };
26670
26757
  });
26758
+ server.registerTool("check_rate_budget", {
26759
+ title: "Check Rate Budget",
26760
+ description: "Consume one rate budget unit for an agent+connector. Returns budget status or RateExceededError.",
26761
+ inputSchema: {
26762
+ agent_id: exports_external.string(),
26763
+ connector: exports_external.string(),
26764
+ limit: exports_external.number().describe("Connector's documented rate limit (calls/min)")
26765
+ }
26766
+ }, async ({ agent_id, connector, limit }) => {
26767
+ const result = checkRateBudget(agent_id, connector, limit);
26768
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
26769
+ });
26770
+ server.registerTool("get_rate_budget", {
26771
+ title: "Get Rate Budget",
26772
+ description: "Peek at rate budget status without consuming a unit.",
26773
+ inputSchema: {
26774
+ agent_id: exports_external.string(),
26775
+ connector: exports_external.string(),
26776
+ limit: exports_external.number().describe("Connector's documented rate limit (calls/min)")
26777
+ }
26778
+ }, async ({ agent_id, connector, limit }) => {
26779
+ const result = getRateBudget(agent_id, connector, limit);
26780
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
26781
+ });
26671
26782
  async function main() {
26672
26783
  const transport = new StdioServerTransport;
26673
26784
  await server.connect(transport);
package/bin/serve.js CHANGED
@@ -53,6 +53,24 @@ function migrate(db) {
53
53
  created_at TEXT NOT NULL
54
54
  )
55
55
  `);
56
+ db.run(`
57
+ CREATE TABLE IF NOT EXISTS resource_locks (
58
+ id TEXT PRIMARY KEY,
59
+ resource_type TEXT NOT NULL CHECK(resource_type IN ('connector', 'agent', 'profile', 'token')),
60
+ resource_id TEXT NOT NULL,
61
+ agent_id TEXT NOT NULL,
62
+ lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(lock_type IN ('advisory', 'exclusive')),
63
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
64
+ expires_at TEXT NOT NULL
65
+ )
66
+ `);
67
+ db.run(`
68
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_resource_locks_exclusive
69
+ ON resource_locks(resource_type, resource_id)
70
+ WHERE lock_type = 'exclusive'
71
+ `);
72
+ db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id)`);
73
+ db.run(`CREATE INDEX IF NOT EXISTS idx_resource_locks_expires ON resource_locks(expires_at)`);
56
74
  }
57
75
 
58
76
  // src/db/agents.ts
@@ -117,6 +135,75 @@ function deleteAgent(id, db) {
117
135
  return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
118
136
  }
119
137
 
138
+ // src/db/rate.ts
139
+ var AGENT_ACTIVE_WINDOW_MS2 = 30 * 60 * 1000;
140
+ var WINDOW_SECONDS = 60;
141
+ function ensureRateTable(db) {
142
+ db.run(`
143
+ CREATE TABLE IF NOT EXISTS connector_rate_usage (
144
+ agent_id TEXT NOT NULL,
145
+ connector TEXT NOT NULL,
146
+ window_start TEXT NOT NULL,
147
+ call_count INTEGER NOT NULL DEFAULT 0,
148
+ PRIMARY KEY (agent_id, connector, window_start)
149
+ )
150
+ `);
151
+ db.run(`CREATE INDEX IF NOT EXISTS idx_rate_usage_window ON connector_rate_usage(connector, window_start)`);
152
+ }
153
+ function countActiveAgents(db) {
154
+ const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS2).toISOString();
155
+ const row = db.query("SELECT COUNT(*) as count FROM agents WHERE last_seen_at > ?").get(cutoff);
156
+ return Math.max(1, row?.count ?? 1);
157
+ }
158
+ function currentWindowStart() {
159
+ const now3 = Date.now();
160
+ const windowMs = WINDOW_SECONDS * 1000;
161
+ return new Date(Math.floor(now3 / windowMs) * windowMs).toISOString();
162
+ }
163
+ function checkRateBudget(agentId, connector, connectorLimit, consume = true, db) {
164
+ const d = db ?? getDatabase();
165
+ ensureRateTable(d);
166
+ const activeAgents = countActiveAgents(d);
167
+ const budget = Math.max(1, Math.floor(connectorLimit / activeAgents));
168
+ const windowStart = currentWindowStart();
169
+ const windowMs = WINDOW_SECONDS * 1000;
170
+ const windowEnd = new Date(Math.floor(Date.now() / windowMs) * windowMs + windowMs);
171
+ const windowResetsIn = windowEnd.getTime() - Date.now();
172
+ const row = d.query("SELECT call_count FROM connector_rate_usage WHERE agent_id = ? AND connector = ? AND window_start = ?").get(agentId, connector, windowStart);
173
+ const used = row?.call_count ?? 0;
174
+ if (used >= budget) {
175
+ return {
176
+ exceeded: true,
177
+ connector,
178
+ agent_id: agentId,
179
+ budget,
180
+ used,
181
+ active_agents: activeAgents,
182
+ window_resets_in_ms: windowResetsIn,
183
+ message: `Rate budget exceeded for "${connector}" (${used}/${budget} calls used, ${activeAgents} active agent${activeAgents === 1 ? "" : "s"} sharing limit of ${connectorLimit}/min). Resets in ${Math.ceil(windowResetsIn / 1000)}s.`
184
+ };
185
+ }
186
+ if (consume) {
187
+ d.run(`INSERT INTO connector_rate_usage (agent_id, connector, window_start, call_count)
188
+ VALUES (?, ?, ?, 1)
189
+ ON CONFLICT(agent_id, connector, window_start) DO UPDATE SET call_count = call_count + 1`, [agentId, connector, windowStart]);
190
+ }
191
+ return {
192
+ connector,
193
+ agent_id: agentId,
194
+ limit: connectorLimit,
195
+ active_agents: activeAgents,
196
+ budget,
197
+ used: consume ? used + 1 : used,
198
+ remaining: consume ? budget - used - 1 : budget - used,
199
+ window_start: windowStart,
200
+ window_resets_in_ms: windowResetsIn
201
+ };
202
+ }
203
+ function getRateBudget(agentId, connector, connectorLimit, db) {
204
+ return checkRateBudget(agentId, connector, connectorLimit, false, db);
205
+ }
206
+
120
207
  // src/server/serve.ts
121
208
  import { join as join6, dirname as dirname3, extname, basename } from "path";
122
209
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -6979,6 +7066,14 @@ Dashboard not found at: ${dashboardDir}`);
6979
7066
  deleteAgent(agent.id);
6980
7067
  return json({ success: true }, 200, port);
6981
7068
  }
7069
+ const rateMatch = path.match(/^\/api\/rate\/([^/]+)\/([^/]+)$/);
7070
+ if (rateMatch && method === "GET") {
7071
+ const [, agentId, connector] = rateMatch;
7072
+ const limit = parseInt(url2.searchParams.get("limit") || "60", 10);
7073
+ const consume = url2.searchParams.get("consume") === "true";
7074
+ const result = consume ? checkRateBudget(agentId, connector, limit) : getRateBudget(agentId, connector, limit);
7075
+ return json(result, 200, port);
7076
+ }
6982
7077
  const profilesMatch = path.match(/^\/api\/connectors\/([^/]+)\/profiles$/);
6983
7078
  if (profilesMatch && method === "GET") {
6984
7079
  const name = profilesMatch[1];
@@ -3,3 +3,5 @@ export declare function getDatabase(path?: string): Database;
3
3
  export declare function closeDatabase(): void;
4
4
  /** ISO timestamp string */
5
5
  export declare function now(): string;
6
+ /** 8-char UUID prefix */
7
+ export declare function shortUuid(): string;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Resource lock helpers for open-connectors.
3
+ *
4
+ * Same interface as open-mementos src/db/locks.ts — standard pattern across
5
+ * all @hasna apps. Resource types are scoped to what connectors manages:
6
+ * connector — the connector itself (install/uninstall ops)
7
+ * agent — agent records
8
+ * profile — connector auth profiles
9
+ * token — OAuth token refresh operations
10
+ */
11
+ import type { Database } from "bun:sqlite";
12
+ export type ResourceType = "connector" | "agent" | "profile" | "token";
13
+ export type LockType = "advisory" | "exclusive";
14
+ export interface ResourceLock {
15
+ id: string;
16
+ resource_type: ResourceType;
17
+ resource_id: string;
18
+ agent_id: string;
19
+ lock_type: LockType;
20
+ locked_at: string;
21
+ expires_at: string;
22
+ }
23
+ /**
24
+ * Acquire a lock on a resource.
25
+ * - advisory: multiple agents can hold advisory locks simultaneously
26
+ * - exclusive: only one agent can hold an exclusive lock at a time
27
+ *
28
+ * Returns the lock if acquired, null if blocked by an existing exclusive lock.
29
+ * TTL is in seconds (default: 5 minutes).
30
+ */
31
+ export declare function acquireLock(agentId: string, resourceType: ResourceType, resourceId: string, lockType?: LockType, ttlSeconds?: number, db?: Database): ResourceLock | null;
32
+ /**
33
+ * Release a specific lock by ID. Only the owning agent can release it.
34
+ */
35
+ export declare function releaseLock(lockId: string, agentId: string, db?: Database): boolean;
36
+ /**
37
+ * Release all locks held by an agent on a specific resource.
38
+ */
39
+ export declare function releaseResourceLocks(agentId: string, resourceType: ResourceType, resourceId: string, db?: Database): number;
40
+ /**
41
+ * Release all locks held by an agent (e.g., on session end).
42
+ */
43
+ export declare function releaseAllAgentLocks(agentId: string, db?: Database): number;
44
+ /**
45
+ * Check if a resource is currently locked. Returns active lock(s) or empty array.
46
+ */
47
+ export declare function checkLock(resourceType: ResourceType, resourceId: string, lockType?: LockType, db?: Database): ResourceLock[];
48
+ /**
49
+ * Check if a specific agent holds a lock on a resource.
50
+ */
51
+ export declare function agentHoldsLock(agentId: string, resourceType: ResourceType, resourceId: string, lockType?: LockType, db?: Database): ResourceLock | null;
52
+ /**
53
+ * List all active locks for an agent.
54
+ */
55
+ export declare function listAgentLocks(agentId: string, db?: Database): ResourceLock[];
56
+ /**
57
+ * Delete all expired locks. Called automatically by other lock functions.
58
+ */
59
+ export declare function cleanExpiredLocks(db?: Database): number;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Agent-aware rate budget splitting for connector operations.
3
+ *
4
+ * When multiple agents share a connector, the connector's rate limit is split
5
+ * evenly among active agents (last heartbeat < 30min). Each agent gets:
6
+ * budget = Math.floor(connectorRateLimit / activeAgentCount)
7
+ *
8
+ * Rate usage is tracked per-agent-per-connector in the DB:
9
+ * connector_rate_usage(agent_id, connector, window_start, call_count)
10
+ *
11
+ * The window resets every WINDOW_SECONDS (default: 60s = per-minute rate).
12
+ */
13
+ import type { Database } from "bun:sqlite";
14
+ export interface RateBudget {
15
+ connector: string;
16
+ agent_id: string;
17
+ limit: number;
18
+ active_agents: number;
19
+ budget: number;
20
+ used: number;
21
+ remaining: number;
22
+ window_start: string;
23
+ window_resets_in_ms: number;
24
+ }
25
+ export interface RateExceededError {
26
+ exceeded: true;
27
+ connector: string;
28
+ agent_id: string;
29
+ budget: number;
30
+ used: number;
31
+ active_agents: number;
32
+ window_resets_in_ms: number;
33
+ message: string;
34
+ }
35
+ export declare function isRateExceeded(result: RateBudget | RateExceededError): result is RateExceededError;
36
+ /**
37
+ * Ensure the rate_usage table exists (idempotent).
38
+ */
39
+ export declare function ensureRateTable(db: Database): void;
40
+ /**
41
+ * Check and optionally consume one rate budget unit for an agent+connector.
42
+ *
43
+ * @param agentId - Agent ID (from agents table)
44
+ * @param connector - Connector name (e.g. "stripe")
45
+ * @param connectorLimit - The connector's documented rate limit (calls/min)
46
+ * @param consume - If true, increment the call counter. If false, just peek.
47
+ * @param db - Optional DB instance (defaults to singleton)
48
+ */
49
+ export declare function checkRateBudget(agentId: string, connector: string, connectorLimit: number, consume?: boolean, db?: Database): RateBudget | RateExceededError;
50
+ /**
51
+ * Get rate budget status without consuming a unit.
52
+ */
53
+ export declare function getRateBudget(agentId: string, connector: string, connectorLimit: number, db?: Database): RateBudget | RateExceededError;
54
+ /**
55
+ * Clean up old rate windows (older than 2 windows).
56
+ */
57
+ export declare function cleanExpiredRateWindows(db?: Database): number;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {