@alfe.ai/agent-api-client 0.1.4 → 0.2.1

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/dist/index.js CHANGED
@@ -6,6 +6,25 @@
6
6
  function encodeFilePath(filePath) {
7
7
  return filePath.split("/").map(encodeURIComponent).join("/");
8
8
  }
9
+ const REQUEST_TIMEOUT_MS = 2e4;
10
+ const RETRYABLE_STATUS = new Set([
11
+ 500,
12
+ 502,
13
+ 503,
14
+ 504
15
+ ]);
16
+ const RETRY_DELAY_MS = 500;
17
+ function sleep(ms) {
18
+ return new Promise((resolve) => {
19
+ setTimeout(resolve, ms);
20
+ });
21
+ }
22
+ function isRetryableNetworkError(err) {
23
+ if (!(err instanceof Error)) return false;
24
+ if (err.name === "TimeoutError" || err.name === "AbortError") return true;
25
+ if (err.name === "TypeError") return true;
26
+ return false;
27
+ }
9
28
  var AgentApiClient = class {
10
29
  apiKey;
11
30
  apiUrl;
@@ -18,15 +37,33 @@ var AgentApiClient = class {
18
37
  const headers = new Headers(options?.headers);
19
38
  headers.set("Authorization", `Bearer ${this.apiKey}`);
20
39
  headers.set("Content-Type", "application/json");
21
- const res = await fetch(url, {
22
- ...options,
23
- headers
24
- });
25
- if (!res.ok) {
26
- await res.text();
27
- throw new Error(`Agent API request failed (${String(res.status)})`);
40
+ let lastError;
41
+ for (let attempt = 1; attempt <= 2; attempt++) try {
42
+ const res = await fetch(url, {
43
+ ...options,
44
+ headers,
45
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
46
+ });
47
+ if (!res.ok) {
48
+ await res.text();
49
+ const error = /* @__PURE__ */ new Error(`Agent API request failed (${String(res.status)})`);
50
+ if (attempt === 1 && RETRYABLE_STATUS.has(res.status)) {
51
+ lastError = error;
52
+ await sleep(RETRY_DELAY_MS);
53
+ continue;
54
+ }
55
+ throw error;
56
+ }
57
+ return (await res.json()).data;
58
+ } catch (err) {
59
+ if (attempt === 1 && isRetryableNetworkError(err)) {
60
+ lastError = err;
61
+ await sleep(RETRY_DELAY_MS);
62
+ continue;
63
+ }
64
+ throw err;
28
65
  }
29
- return (await res.json()).data;
66
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
30
67
  }
31
68
  async syncRegister(args) {
32
69
  return this.request("/agent/sync/register", {
@@ -145,6 +182,14 @@ var AgentApiClient = class {
145
182
  async getGoogleChatCredentials() {
146
183
  return this.request("/agent/google-chat/credentials");
147
184
  }
185
+ /**
186
+ * @deprecated Returns a single primary credential blob (legacy "pick-the-
187
+ * default-connection" shape). Use `getGithubAccounts()` for the multi-
188
+ * account shape required by Pattern A — explicit selector args on every
189
+ * tool. Retained because the `@alfe.ai/openclaw-github` proxy is the
190
+ * only consumer that knows about Pattern A; legacy env-interpolation
191
+ * callers will keep hitting `/credentials` until they move to the proxy.
192
+ */
148
193
  async getGithubCredentials() {
149
194
  const raw = await this.request("/agent/connect/github/credentials");
150
195
  return {
@@ -152,6 +197,37 @@ var AgentApiClient = class {
152
197
  accessToken: raw.accessToken
153
198
  };
154
199
  }
200
+ /**
201
+ * Pattern A: multi-account credential fetch for GitHub.
202
+ *
203
+ * Returns every agent-scoped GitHub connection. The caller is expected
204
+ * to require a `login` selector on every credential-touching tool and
205
+ * look up the matching account at dispatch time.
206
+ *
207
+ * GitHub OAuth tokens have no expiry (`tokenLifecycle: "no_expiry"`),
208
+ * so there is intentionally no `refreshGithubAccountToken` method — if
209
+ * a token is revoked the user must re-run the OAuth flow.
210
+ *
211
+ * Returned `accounts[i].login` is the GitHub username — the stable
212
+ * cross-session identifier the LLM should pass.
213
+ */
214
+ async getGithubAccounts() {
215
+ return { accounts: (await this.request("/agent/connect/github/accounts")).accounts.map((a) => ({
216
+ connectionId: a.connectionId,
217
+ accountIdentifier: a.accountIdentifier,
218
+ displayName: a.displayName,
219
+ connectedAt: a.connectedAt,
220
+ accessToken: a.accessToken ?? "",
221
+ login: a.login ?? a.accountIdentifier,
222
+ scopes: a.scopes ?? ""
223
+ })) };
224
+ }
225
+ /**
226
+ * @deprecated Returns a single primary credential blob (legacy "pick-the-
227
+ * default-connection" shape). Use `getXeroAccounts()` for the multi-
228
+ * account shape required by Pattern A — explicit selector args on every
229
+ * tool. This method will be removed once all consumers migrate.
230
+ */
155
231
  async getXeroCredentials() {
156
232
  const raw = await this.request("/agent/connect/xero/credentials");
157
233
  return {
@@ -160,9 +236,49 @@ var AgentApiClient = class {
160
236
  xeroTenantId: raw.xeroTenantId ?? ""
161
237
  };
162
238
  }
239
+ /**
240
+ * Pattern A: multi-account credential fetch for Xero. Returns every
241
+ * agent-scoped Xero connection. The caller is expected to require a
242
+ * selector arg (e.g. `xeroTenantId`) on every credential-touching tool
243
+ * and look up the matching account by that selector at dispatch time.
244
+ *
245
+ * Returned `accounts[i].accountIdentifier` is the Xero tenantId — the
246
+ * stable cross-session identifier the LLM should pass.
247
+ */
248
+ async getXeroAccounts() {
249
+ return { accounts: (await this.request("/agent/connect/xero/accounts")).accounts.map((a) => ({
250
+ connectionId: a.connectionId,
251
+ accountIdentifier: a.accountIdentifier,
252
+ displayName: a.displayName,
253
+ connectedAt: a.connectedAt,
254
+ accessToken: a.accessToken,
255
+ accessTokenExpiresAt: a.accessTokenExpiresAt ?? "",
256
+ xeroTenantId: a.xeroTenantId ?? a.accountIdentifier
257
+ })) };
258
+ }
163
259
  async refreshXeroToken() {
164
260
  return this.request("/agent/connect/xero/refresh", { method: "POST" });
165
261
  }
262
+ /**
263
+ * Pattern A: refresh a specific Xero connection by its `accountIdentifier`
264
+ * (the Xero `tenantId`). The legacy `refreshXeroToken()` only refreshes
265
+ * the *primary* connection, which is wrong for multi-tenant Xero where
266
+ * each tenant has its own non-interchangeable access token.
267
+ */
268
+ async refreshXeroAccountToken(xeroTenantId) {
269
+ const path = `/agent/connect/xero/accounts/${encodeURIComponent(xeroTenantId)}/refresh`;
270
+ const raw = await this.request(path, { method: "POST" });
271
+ return {
272
+ accessToken: raw.accessToken,
273
+ accessTokenExpiresAt: raw.accessTokenExpiresAt ?? "",
274
+ expiresAt: raw.expiresAt ?? ""
275
+ };
276
+ }
277
+ /**
278
+ * @deprecated Returns a single primary credential blob (legacy "pick-the-
279
+ * default-connection" shape). Use `getNotionAccounts()` for the multi-
280
+ * account shape required by Pattern A.
281
+ */
166
282
  async getNotionCredentials() {
167
283
  const raw = await this.request("/agent/connect/notion/credentials");
168
284
  return {
@@ -171,6 +287,32 @@ var AgentApiClient = class {
171
287
  workspaceName: raw.workspaceName ?? ""
172
288
  };
173
289
  }
290
+ /**
291
+ * Pattern A: multi-account credential fetch for Notion. Returns every
292
+ * agent-scoped Notion connection. The caller is expected to require a
293
+ * selector arg (e.g. `workspaceId`) on every credential-touching tool.
294
+ *
295
+ * Returned `accounts[i].accountIdentifier` is the Notion workspaceId.
296
+ */
297
+ async getNotionAccounts() {
298
+ return { accounts: (await this.request("/agent/connect/notion/accounts")).accounts.map((a) => ({
299
+ connectionId: a.connectionId,
300
+ accountIdentifier: a.accountIdentifier,
301
+ displayName: a.displayName,
302
+ connectedAt: a.connectedAt,
303
+ accessToken: a.accessToken,
304
+ workspaceId: a.workspaceId ?? a.accountIdentifier,
305
+ workspaceName: a.workspaceName ?? a.displayName ?? ""
306
+ })) };
307
+ }
308
+ /**
309
+ * @deprecated Returns a single primary Atlassian Connection's credentials
310
+ * (one OAuth user, one cloudId) — the legacy "pick-the-default-connection"
311
+ * shape. Atlassian is multi-site by nature (each OAuth user may have
312
+ * access to multiple Cloud sites), so Pattern A plugins MUST use
313
+ * `getAtlassianAccounts()` to discover the full set and dispatch via
314
+ * the `cloudId` selector arg.
315
+ */
174
316
  async getAtlassianCredentials() {
175
317
  const raw = await this.request("/agent/connect/atlassian/credentials");
176
318
  return {
@@ -189,6 +331,72 @@ var AgentApiClient = class {
189
331
  async refreshAtlassianToken() {
190
332
  return this.request("/agent/connect/atlassian/refresh", { method: "POST" });
191
333
  }
334
+ /**
335
+ * Pattern A: multi-account / multi-site credential fetch for Atlassian.
336
+ *
337
+ * Returns every agent-scoped Atlassian Connection. Each Connection is
338
+ * one OAuth user with a single access token and N accessible Cloud
339
+ * sites (`availableSites`). The caller is expected to:
340
+ *
341
+ * 1. Flatten (connection × cloudId) into one MCP child per site.
342
+ * 2. Require a `cloudId` selector on every credential-touching tool.
343
+ * 3. Use the access token bound to the Connection that owns the
344
+ * requested `cloudId` (Atlassian shares one access token across
345
+ * all sites accessible to the OAuth user).
346
+ *
347
+ * Per-account token refresh uses `refreshAtlassianAccountToken(email)`
348
+ * — refreshing one Connection rotates its single access token, which
349
+ * then applies to every cloudId for that Connection.
350
+ *
351
+ * Returned `accounts[i].accountIdentifier` is the OAuth user's email
352
+ * — the stable cross-session identifier for refresh purposes. The LLM
353
+ * never sees this directly: it picks a site via the `cloudId` arg
354
+ * instead.
355
+ */
356
+ async getAtlassianAccounts() {
357
+ return { accounts: (await this.request("/agent/connect/atlassian/accounts")).accounts.map((a) => ({
358
+ connectionId: a.connectionId,
359
+ accountIdentifier: a.accountIdentifier,
360
+ displayName: a.displayName,
361
+ connectedAt: a.connectedAt,
362
+ accessToken: a.accessToken ?? "",
363
+ accessTokenExpiresAt: a.accessTokenExpiresAt ?? "",
364
+ clientId: a.clientId ?? "",
365
+ clientSecret: a.clientSecret ?? "",
366
+ cloudId: a.cloudId ?? "",
367
+ siteName: a.siteName ?? "",
368
+ siteUrl: a.siteUrl ?? "",
369
+ availableSites: a.availableSites ?? []
370
+ })) };
371
+ }
372
+ /**
373
+ * Pattern A: refresh a specific Atlassian Connection by `accountIdentifier`
374
+ * (the OAuth user's email).
375
+ *
376
+ * Atlassian rotates refresh tokens (`rotatesRefreshToken: true`); the
377
+ * server-side per-account refresh endpoint handles rotation and
378
+ * persistence. Refreshing one Connection updates its single access
379
+ * token, which applies to every accessible Cloud site (cloudId) for
380
+ * that OAuth user.
381
+ *
382
+ * Returns the new access token + expiry. The proxy is responsible for
383
+ * fanning the new token out to every child server it spawned for
384
+ * cloudIds owned by this Connection.
385
+ */
386
+ async refreshAtlassianAccountToken(accountIdentifier) {
387
+ const path = `/agent/connect/atlassian/accounts/${encodeURIComponent(accountIdentifier)}/refresh`;
388
+ const raw = await this.request(path, { method: "POST" });
389
+ return {
390
+ accessToken: raw.accessToken,
391
+ accessTokenExpiresAt: raw.accessTokenExpiresAt ?? "",
392
+ expiresAt: raw.expiresAt ?? ""
393
+ };
394
+ }
395
+ /**
396
+ * @deprecated Returns a single primary credential blob (legacy "pick-the-
397
+ * default-connection" shape). Use `getMYOBAccounts()` for the multi-
398
+ * account shape required by Pattern A.
399
+ */
192
400
  async getMYOBCredentials() {
193
401
  const raw = await this.request("/agent/connect/myob/credentials");
194
402
  return {
@@ -198,9 +406,109 @@ var AgentApiClient = class {
198
406
  clientId: raw.clientId
199
407
  };
200
408
  }
409
+ /**
410
+ * Pattern A: multi-account credential fetch for MYOB. Returns every
411
+ * agent-scoped MYOB connection. The caller is expected to require a
412
+ * selector arg (e.g. `myobBusinessId` / `accountIdentifier`) on every
413
+ * credential-touching tool.
414
+ *
415
+ * Returned `accounts[i].accountIdentifier` is the MYOB businessId.
416
+ */
417
+ async getMYOBAccounts() {
418
+ return { accounts: (await this.request("/agent/connect/myob/accounts")).accounts.map((a) => ({
419
+ connectionId: a.connectionId,
420
+ accountIdentifier: a.accountIdentifier,
421
+ displayName: a.displayName,
422
+ connectedAt: a.connectedAt,
423
+ accessToken: a.accessToken,
424
+ accessTokenExpiresAt: a.accessTokenExpiresAt ?? "",
425
+ myobBusinessId: a.myobBusinessId ?? a.accountIdentifier,
426
+ clientId: a.clientId
427
+ })) };
428
+ }
201
429
  async refreshMYOBToken() {
202
430
  return this.request("/agent/connect/myob/refresh", { method: "POST" });
203
431
  }
432
+ /**
433
+ * Microsoft 365 (delegated OAuth) credential fetch — single-account shape.
434
+ *
435
+ * @deprecated Use `getMicrosoftAccounts()` and dispatch via the `email`
436
+ * selector once per-account plugins land. Retained because the existing
437
+ * `integrations/connect/microsoft/hooks/post_activate.mjs` writes
438
+ * `mgc` credentials for the single (default) Microsoft account.
439
+ *
440
+ * Returns the agent's effective Microsoft delegated-OAuth credentials.
441
+ * Distinct from `getTeamsCredentials()` (Azure bot credentials for the
442
+ * Teams adapter, which is admin-consent flow on services/microsoft, not
443
+ * delegated OAuth on services/connect).
444
+ */
445
+ async getMicrosoftCredentials() {
446
+ const raw = await this.request("/agent/connect/microsoft/credentials");
447
+ return {
448
+ accessToken: raw.accessToken ?? "",
449
+ accessTokenExpiresAt: raw.accessTokenExpiresAt,
450
+ refreshToken: raw.refreshToken ?? "",
451
+ clientId: raw.clientId ?? "",
452
+ clientSecret: raw.clientSecret ?? "",
453
+ email: raw.email ?? raw.accountIdentifier,
454
+ microsoftTenantId: raw.microsoftTenantId,
455
+ workspaceDomain: raw.workspaceDomain
456
+ };
457
+ }
458
+ /**
459
+ * Pattern A: multi-account credential fetch for Microsoft 365.
460
+ *
461
+ * Returns every agent-scoped Microsoft connection. The caller is expected
462
+ * to require an `email` selector on every credential-touching tool and
463
+ * look up the matching account at dispatch time.
464
+ *
465
+ * Returned `accounts[i].accountIdentifier` is the user's primary email
466
+ * (or the tid claim as fallback) — the stable cross-session identifier
467
+ * the LLM should pass.
468
+ *
469
+ * Per-account token refresh is exposed via `refreshMicrosoftAccountToken`,
470
+ * NOT `refreshXeroAccountToken` — Microsoft refresh tokens are not
471
+ * interchangeable across (tenant, user) pairs.
472
+ */
473
+ async getMicrosoftAccounts() {
474
+ return { accounts: (await this.request("/agent/connect/microsoft/accounts")).accounts.map((a) => ({
475
+ connectionId: a.connectionId,
476
+ accountIdentifier: a.accountIdentifier,
477
+ displayName: a.displayName,
478
+ connectedAt: a.connectedAt,
479
+ accessToken: a.accessToken ?? "",
480
+ accessTokenExpiresAt: a.accessTokenExpiresAt ?? "",
481
+ refreshToken: a.refreshToken ?? "",
482
+ clientId: a.clientId ?? "",
483
+ clientSecret: a.clientSecret ?? "",
484
+ email: a.email ?? a.accountIdentifier,
485
+ microsoftTenantId: a.microsoftTenantId ?? "",
486
+ workspaceDomain: a.workspaceDomain ?? ""
487
+ })) };
488
+ }
489
+ /**
490
+ * Pattern A: refresh a specific Microsoft 365 connection by its
491
+ * `accountIdentifier`. For Microsoft, `accountIdentifier` is the user's
492
+ * email when the Graph profile fetch succeeded at connect time, and the
493
+ * Azure tenant id (`tid` claim) as fallback. Callers should pass the
494
+ * value returned by `getMicrosoftAccounts()` rather than synthesising
495
+ * an email locally.
496
+ *
497
+ * Microsoft refresh tokens are bound to a specific (tenant, user) pair —
498
+ * they are NOT interchangeable across accounts, so per-account refresh
499
+ * is mandatory. The generic /accounts/{accountIdentifier}/refresh
500
+ * endpoint walks the agent's full visible scope chain to find a matching
501
+ * connection (works for inherited team/project Microsoft connections).
502
+ */
503
+ async refreshMicrosoftAccountToken(accountIdentifier) {
504
+ const path = `/agent/connect/microsoft/accounts/${encodeURIComponent(accountIdentifier)}/refresh`;
505
+ const raw = await this.request(path, { method: "POST" });
506
+ return {
507
+ accessToken: raw.accessToken,
508
+ accessTokenExpiresAt: raw.accessTokenExpiresAt ?? "",
509
+ expiresAt: raw.expiresAt ?? ""
510
+ };
511
+ }
204
512
  async getTeamsCredentials() {
205
513
  return this.request("/agent/microsoft/credentials");
206
514
  }