@okrlinkhub/agent-factory 0.2.0 → 0.2.2

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.
Files changed (36) hide show
  1. package/README.md +48 -9
  2. package/dist/client/index.d.ts +41 -15
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +18 -2
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/component.d.ts +46 -0
  7. package/dist/component/_generated/component.d.ts.map +1 -1
  8. package/dist/component/config.d.ts +1 -1
  9. package/dist/component/config.d.ts.map +1 -1
  10. package/dist/component/config.js +5 -5
  11. package/dist/component/config.js.map +1 -1
  12. package/dist/component/identity.d.ts +19 -6
  13. package/dist/component/identity.d.ts.map +1 -1
  14. package/dist/component/identity.js +85 -1
  15. package/dist/component/identity.js.map +1 -1
  16. package/dist/component/lib.d.ts +1 -1
  17. package/dist/component/lib.d.ts.map +1 -1
  18. package/dist/component/lib.js +1 -1
  19. package/dist/component/lib.js.map +1 -1
  20. package/dist/component/queue.d.ts +38 -28
  21. package/dist/component/queue.d.ts.map +1 -1
  22. package/dist/component/queue.js +3 -1
  23. package/dist/component/queue.js.map +1 -1
  24. package/dist/component/scheduler.d.ts +32 -32
  25. package/dist/component/scheduler.js +18 -2
  26. package/dist/component/scheduler.js.map +1 -1
  27. package/dist/component/schema.d.ts +45 -45
  28. package/package.json +5 -2
  29. package/src/client/index.ts +24 -1
  30. package/src/component/_generated/component.ts +52 -0
  31. package/src/component/config.ts +5 -5
  32. package/src/component/identity.ts +113 -1
  33. package/src/component/lib.test.ts +13 -10
  34. package/src/component/lib.ts +1 -0
  35. package/src/component/queue.ts +3 -1
  36. package/src/component/scheduler.ts +23 -2
@@ -1,5 +1,6 @@
1
1
  import { v } from "convex/values";
2
- import { mutation, query } from "./_generated/server.js";
2
+ import { internal } from "./_generated/api.js";
3
+ import { action, mutation, query } from "./_generated/server.js";
3
4
  import type { MutationCtx } from "./_generated/server.js";
4
5
 
5
6
  const bindingStatusValidator = v.union(v.literal("active"), v.literal("revoked"));
@@ -38,6 +39,17 @@ const pairingCodeViewValidator = v.object({
38
39
  telegramChatId: v.union(v.null(), v.string()),
39
40
  });
40
41
 
42
+ const telegramWebhookStatusValidator = v.object({
43
+ ok: v.boolean(),
44
+ webhookUrl: v.string(),
45
+ currentUrl: v.union(v.null(), v.string()),
46
+ isReady: v.boolean(),
47
+ pendingUpdateCount: v.number(),
48
+ lastErrorMessage: v.union(v.null(), v.string()),
49
+ lastErrorDate: v.union(v.null(), v.number()),
50
+ description: v.string(),
51
+ });
52
+
41
53
  type BindingSource = "manual" | "telegram_pairing" | "api";
42
54
 
43
55
  type UpsertBindingArgs = {
@@ -50,6 +62,106 @@ type UpsertBindingArgs = {
50
62
  nowMs?: number;
51
63
  };
52
64
 
65
+ export const configureTelegramWebhook = action({
66
+ args: {
67
+ convexSiteUrl: v.string(),
68
+ secretRef: v.optional(v.string()),
69
+ },
70
+ returns: telegramWebhookStatusValidator,
71
+ handler: async (ctx, args) => {
72
+ const secretRef = args.secretRef?.trim() || "telegram.botToken.default";
73
+ const token = await ctx.runQuery(internal.queue.getActiveSecretPlaintext, {
74
+ secretRef,
75
+ });
76
+ if (!token) {
77
+ throw new Error(
78
+ `Missing Telegram token. Import an active '${secretRef}' secret before pairing.`,
79
+ );
80
+ }
81
+
82
+ const rawSiteUrl = args.convexSiteUrl.trim();
83
+ if (!rawSiteUrl) {
84
+ throw new Error("convexSiteUrl is required.");
85
+ }
86
+
87
+ const normalizedSiteUrl = rawSiteUrl
88
+ .replace(/\/+$/, "")
89
+ .replace(/\.cloud$/i, ".site");
90
+ if (!normalizedSiteUrl.startsWith("https://")) {
91
+ throw new Error("convexSiteUrl must start with https://");
92
+ }
93
+
94
+ const webhookUrl = `${normalizedSiteUrl}/agent-factory/telegram/webhook`;
95
+ const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(token)}`;
96
+
97
+ const setWebhookResponse = await fetch(`${telegramApiBaseUrl}/setWebhook`, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ },
102
+ body: JSON.stringify({
103
+ url: webhookUrl,
104
+ }),
105
+ });
106
+
107
+ const setWebhookPayload = (await setWebhookResponse.json().catch(() => ({}))) as {
108
+ ok?: boolean;
109
+ description?: string;
110
+ };
111
+
112
+ if (!setWebhookResponse.ok || setWebhookPayload.ok !== true) {
113
+ const description =
114
+ typeof setWebhookPayload.description === "string"
115
+ ? setWebhookPayload.description
116
+ : "setWebhook failed";
117
+ throw new Error(`Telegram setWebhook failed: ${description}`);
118
+ }
119
+
120
+ const webhookInfoResponse = await fetch(`${telegramApiBaseUrl}/getWebhookInfo`);
121
+ const webhookInfoPayload = (await webhookInfoResponse.json().catch(() => ({}))) as {
122
+ ok?: boolean;
123
+ description?: string;
124
+ result?: {
125
+ url?: string;
126
+ pending_update_count?: number;
127
+ last_error_message?: string;
128
+ last_error_date?: number;
129
+ };
130
+ };
131
+
132
+ if (!webhookInfoResponse.ok || webhookInfoPayload.ok !== true) {
133
+ const description =
134
+ typeof webhookInfoPayload.description === "string"
135
+ ? webhookInfoPayload.description
136
+ : "getWebhookInfo failed";
137
+ throw new Error(`Telegram getWebhookInfo failed: ${description}`);
138
+ }
139
+
140
+ const currentUrl = typeof webhookInfoPayload.result?.url === "string"
141
+ ? webhookInfoPayload.result.url
142
+ : null;
143
+ const pendingUpdateCount = Number(webhookInfoPayload.result?.pending_update_count ?? 0);
144
+ const lastErrorMessage = typeof webhookInfoPayload.result?.last_error_message === "string"
145
+ ? webhookInfoPayload.result.last_error_message
146
+ : null;
147
+ const lastErrorDate = typeof webhookInfoPayload.result?.last_error_date === "number"
148
+ ? webhookInfoPayload.result.last_error_date
149
+ : null;
150
+ const isReady = currentUrl === webhookUrl;
151
+
152
+ return {
153
+ ok: true,
154
+ webhookUrl,
155
+ currentUrl,
156
+ isReady,
157
+ pendingUpdateCount,
158
+ lastErrorMessage,
159
+ lastErrorDate,
160
+ description: "Telegram webhook configured and verified.",
161
+ };
162
+ },
163
+ });
164
+
53
165
  export const createPairingCode = mutation({
54
166
  args: {
55
167
  consumerUserId: v.string(),
@@ -4,6 +4,17 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
4
4
  import { api, internal } from "./_generated/api.js";
5
5
  import { initConvexTest } from "./setup.test.js";
6
6
 
7
+ const TEST_PROVIDER_CONFIG = {
8
+ kind: "fly" as const,
9
+ appName: "agent-factory-workers-test",
10
+ organizationSlug: "personal",
11
+ image: "registry.fly.io/agent-factory-workers-test:test-image",
12
+ region: "iad",
13
+ volumeName: "openclaw_data_test",
14
+ volumePath: "/data",
15
+ volumeSizeGb: 10,
16
+ };
17
+
7
18
  describe("component lib", () => {
8
19
  beforeEach(async () => {
9
20
  vi.useFakeTimers();
@@ -361,6 +372,7 @@ describe("component lib", () => {
361
372
  idleTimeoutMs: 300_000,
362
373
  reconcileIntervalMs: 15_000,
363
374
  },
375
+ providerConfig: TEST_PROVIDER_CONFIG,
364
376
  });
365
377
  expect(reconcile.activeWorkers).toBe(3);
366
378
  expect(reconcile.spawned).toBe(0);
@@ -433,16 +445,7 @@ describe("component lib", () => {
433
445
  idleTimeoutMs: 300_000,
434
446
  reconcileIntervalMs: 15_000,
435
447
  },
436
- providerConfig: {
437
- kind: "fly",
438
- appName: "agent-factory-workers",
439
- organizationSlug: "personal",
440
- image: "registry.fly.io/agent-factory-workers:test-image",
441
- region: "iad",
442
- volumeName: "",
443
- volumePath: "",
444
- volumeSizeGb: 10,
445
- },
448
+ providerConfig: TEST_PROVIDER_CONFIG,
446
449
  });
447
450
  expect(reconcile.activeWorkers).toBe(3);
448
451
  expect(reconcile.spawned).toBe(0);
@@ -29,6 +29,7 @@ export {
29
29
  resolveAgentForUser,
30
30
  resolveAgentForTelegram,
31
31
  getUserAgentBinding,
32
+ configureTelegramWebhook,
32
33
  createPairingCode,
33
34
  consumePairingCode,
34
35
  getPairingCodeStatus,
@@ -6,7 +6,7 @@ import {
6
6
  mutation,
7
7
  query,
8
8
  } from "./_generated/server.js";
9
- import { computeRetryDelayMs, DEFAULT_CONFIG } from "./config.js";
9
+ import { computeRetryDelayMs, DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
10
10
 
11
11
  const queueStatusValidator = v.union(
12
12
  v.literal("queued"),
@@ -79,6 +79,7 @@ export const enqueueMessage = mutation({
79
79
  scheduledFor: v.optional(v.number()),
80
80
  maxAttempts: v.optional(v.number()),
81
81
  nowMs: v.optional(v.number()),
82
+ providerConfig: v.optional(providerConfigValidator),
82
83
  },
83
84
  returns: v.id("messageQueue"),
84
85
  handler: async (ctx, args) => {
@@ -125,6 +126,7 @@ export const enqueueMessage = mutation({
125
126
  try {
126
127
  await ctx.scheduler.runAfter(0, (internal.scheduler as any).reconcileWorkerPoolFromEnqueue, {
127
128
  workspaceId: "default",
129
+ providerConfig: args.providerConfig,
128
130
  });
129
131
  } catch (error) {
130
132
  console.warn(
@@ -99,7 +99,7 @@ async function runReconcileWorkerPool(
99
99
  ) {
100
100
  const nowMs = args.nowMs ?? Date.now();
101
101
  const scaling = args.scalingPolicy ?? DEFAULT_CONFIG.scaling;
102
- const providerConfig = args.providerConfig ?? DEFAULT_CONFIG.provider;
102
+ const providerConfig = ensureProviderConfig(args.providerConfig ?? DEFAULT_CONFIG.provider);
103
103
  const flyApiToken =
104
104
  args.flyApiToken ??
105
105
  (await ctx.runQuery(internal.queue.getActiveSecretPlaintext, {
@@ -304,7 +304,7 @@ async function runEnforceIdleShutdowns(
304
304
  },
305
305
  ) {
306
306
  const nowMs = args.nowMs ?? Date.now();
307
- const providerConfig = args.providerConfig ?? DEFAULT_CONFIG.provider;
307
+ const providerConfig = ensureProviderConfig(args.providerConfig ?? DEFAULT_CONFIG.provider);
308
308
  const flyApiToken =
309
309
  args.flyApiToken ??
310
310
  (await ctx.runQuery(internal.queue.getActiveSecretPlaintext, {
@@ -431,6 +431,27 @@ function resolveProvider(kind: string, flyApiToken: string): WorkerProvider {
431
431
  }
432
432
  }
433
433
 
434
+ function ensureProviderConfig(providerConfig: typeof DEFAULT_CONFIG.provider) {
435
+ const requiredFields: Array<
436
+ "appName" | "organizationSlug" | "image" | "region" | "volumeName" | "volumePath"
437
+ > = [
438
+ "appName",
439
+ "organizationSlug",
440
+ "image",
441
+ "region",
442
+ "volumeName",
443
+ "volumePath",
444
+ ];
445
+ for (const field of requiredFields) {
446
+ if (!providerConfig[field]?.trim()) {
447
+ throw new Error(
448
+ `Missing providerConfig.${field}. Pass providerConfig explicitly when starting/reconciling workers.`,
449
+ );
450
+ }
451
+ }
452
+ return providerConfig;
453
+ }
454
+
434
455
  function clamp(value: number, min: number, max: number): number {
435
456
  return Math.max(min, Math.min(max, value));
436
457
  }