@vertesia/tools-sdk 1.0.0-dev.20260227.112605Z → 1.0.0-dev.20260331.091034Z

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 (68) hide show
  1. package/lib/cjs/ActivityCollection.js +93 -0
  2. package/lib/cjs/ActivityCollection.js.map +1 -0
  3. package/lib/cjs/auth.js +3 -3
  4. package/lib/cjs/auth.js.map +1 -1
  5. package/lib/cjs/index.js +1 -0
  6. package/lib/cjs/index.js.map +1 -1
  7. package/lib/cjs/server/activities.js +103 -0
  8. package/lib/cjs/server/activities.js.map +1 -0
  9. package/lib/cjs/server/app-package.js +14 -1
  10. package/lib/cjs/server/app-package.js.map +1 -1
  11. package/lib/cjs/server/interactions.js +21 -9
  12. package/lib/cjs/server/interactions.js.map +1 -1
  13. package/lib/cjs/server/site.js +7 -1
  14. package/lib/cjs/server/site.js.map +1 -1
  15. package/lib/cjs/server/skills.js +10 -0
  16. package/lib/cjs/server/skills.js.map +1 -1
  17. package/lib/cjs/server.js +4 -1
  18. package/lib/cjs/server.js.map +1 -1
  19. package/lib/cjs/site/templates.js +138 -3
  20. package/lib/cjs/site/templates.js.map +1 -1
  21. package/lib/esm/ActivityCollection.js +89 -0
  22. package/lib/esm/ActivityCollection.js.map +1 -0
  23. package/lib/esm/auth.js +3 -3
  24. package/lib/esm/auth.js.map +1 -1
  25. package/lib/esm/index.js +1 -0
  26. package/lib/esm/index.js.map +1 -1
  27. package/lib/esm/server/activities.js +100 -0
  28. package/lib/esm/server/activities.js.map +1 -0
  29. package/lib/esm/server/app-package.js +14 -1
  30. package/lib/esm/server/app-package.js.map +1 -1
  31. package/lib/esm/server/interactions.js +21 -9
  32. package/lib/esm/server/interactions.js.map +1 -1
  33. package/lib/esm/server/site.js +8 -2
  34. package/lib/esm/server/site.js.map +1 -1
  35. package/lib/esm/server/skills.js +10 -0
  36. package/lib/esm/server/skills.js.map +1 -1
  37. package/lib/esm/server.js +4 -1
  38. package/lib/esm/server.js.map +1 -1
  39. package/lib/esm/site/templates.js +136 -3
  40. package/lib/esm/site/templates.js.map +1 -1
  41. package/lib/types/ActivityCollection.d.ts +55 -0
  42. package/lib/types/ActivityCollection.d.ts.map +1 -0
  43. package/lib/types/index.d.ts +1 -0
  44. package/lib/types/index.d.ts.map +1 -1
  45. package/lib/types/server/activities.d.ts +4 -0
  46. package/lib/types/server/activities.d.ts.map +1 -0
  47. package/lib/types/server/app-package.d.ts.map +1 -1
  48. package/lib/types/server/interactions.d.ts.map +1 -1
  49. package/lib/types/server/site.d.ts.map +1 -1
  50. package/lib/types/server/types.d.ts +5 -0
  51. package/lib/types/server/types.d.ts.map +1 -1
  52. package/lib/types/server.d.ts.map +1 -1
  53. package/lib/types/site/templates.d.ts +10 -0
  54. package/lib/types/site/templates.d.ts.map +1 -1
  55. package/package.json +5 -5
  56. package/src/ActivityCollection.test.ts +161 -0
  57. package/src/ActivityCollection.ts +136 -0
  58. package/src/auth.ts +3 -3
  59. package/src/index.ts +1 -0
  60. package/src/server/activities.test.ts +165 -0
  61. package/src/server/activities.ts +114 -0
  62. package/src/server/app-package.ts +15 -2
  63. package/src/server/interactions.ts +22 -11
  64. package/src/server/site.ts +9 -0
  65. package/src/server/skills.ts +10 -0
  66. package/src/server/types.ts +5 -0
  67. package/src/server.ts +4 -0
  68. package/src/site/templates.ts +143 -2
@@ -0,0 +1,161 @@
1
+ import { RemoteActivityExecutionPayload } from "@vertesia/common";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { ActivityCollection, ActivityDefinition } from "./ActivityCollection.js";
4
+
5
+ vi.mock("./auth.js", () => ({
6
+ authorize: vi.fn().mockResolvedValue({ token: "test-token" }),
7
+ }));
8
+
9
+ const mockActivity: ActivityDefinition = {
10
+ name: "analyze_sentiment",
11
+ title: "Analyze Sentiment",
12
+ description: "Analyzes text sentiment",
13
+ input_schema: {
14
+ type: "object",
15
+ properties: { text: { type: "string" } },
16
+ required: ["text"],
17
+ },
18
+ output_schema: {
19
+ type: "object",
20
+ properties: { score: { type: "number" } },
21
+ },
22
+ run: vi.fn().mockResolvedValue({ score: 0.95 }),
23
+ };
24
+
25
+ const mockFailingActivity: ActivityDefinition = {
26
+ name: "fail_always",
27
+ description: "Always fails",
28
+ run: vi.fn().mockRejectedValue(new Error("Intentional failure")),
29
+ };
30
+
31
+ function createCollection(activities: ActivityDefinition[] = [mockActivity]) {
32
+ return new ActivityCollection({
33
+ name: "test-collection",
34
+ title: "Test Collection",
35
+ description: "A test collection",
36
+ activities,
37
+ });
38
+ }
39
+
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ describe("ActivityCollection", () => {
45
+ describe("getActivityDefinitions", () => {
46
+ it("returns correct metadata for all registered activities", () => {
47
+ const coll = createCollection([mockActivity, mockFailingActivity]);
48
+ const defs = coll.getActivityDefinitions();
49
+
50
+ expect(defs).toHaveLength(2);
51
+ expect(defs[0]).toEqual({
52
+ name: "analyze_sentiment",
53
+ title: "Analyze Sentiment",
54
+ description: "Analyzes text sentiment",
55
+ input_schema: mockActivity.input_schema,
56
+ output_schema: mockActivity.output_schema,
57
+ });
58
+ expect(defs[1]).toEqual({
59
+ name: "fail_always",
60
+ title: undefined,
61
+ description: "Always fails",
62
+ input_schema: undefined,
63
+ output_schema: undefined,
64
+ });
65
+ });
66
+
67
+ it("returns empty array when no activities registered", () => {
68
+ const coll = createCollection([]);
69
+ expect(coll.getActivityDefinitions()).toEqual([]);
70
+ });
71
+ });
72
+
73
+ describe("getActivity", () => {
74
+ it("returns the activity by name", () => {
75
+ const coll = createCollection();
76
+ const activity = coll.getActivity("analyze_sentiment");
77
+ expect(activity.name).toBe("analyze_sentiment");
78
+ expect(activity.run).toBeDefined();
79
+ });
80
+
81
+ it("throws 404 for unknown activity name", () => {
82
+ const coll = createCollection();
83
+ expect(() => coll.getActivity("nonexistent")).toThrow();
84
+ });
85
+ });
86
+
87
+ describe("iterator", () => {
88
+ it("iterates over all activities", () => {
89
+ const coll = createCollection([mockActivity, mockFailingActivity]);
90
+ const names = [...coll].map(a => a.name);
91
+ expect(names).toEqual(["analyze_sentiment", "fail_always"]);
92
+ });
93
+ });
94
+
95
+ describe("execute", () => {
96
+ const createMockContext = () => {
97
+ const jsonFn = vi.fn().mockReturnThis();
98
+ return {
99
+ json: jsonFn,
100
+ req: {
101
+ header: vi.fn().mockReturnValue("Bearer test-token"),
102
+ url: "http://localhost/api/activities",
103
+ },
104
+ set: vi.fn(),
105
+ } as any;
106
+ };
107
+
108
+ const payload: RemoteActivityExecutionPayload = {
109
+ activity_name: "analyze_sentiment",
110
+ params: { text: "Hello world" },
111
+ metadata: {
112
+ app_install_id: "install-1",
113
+ },
114
+ };
115
+
116
+ it("calls the correct activity handler and returns result", async () => {
117
+ const coll = createCollection();
118
+ const ctx = createMockContext();
119
+
120
+ await coll.execute(ctx, payload);
121
+
122
+ expect(mockActivity.run).toHaveBeenCalledWith(
123
+ payload,
124
+ expect.objectContaining({ token: "test-token" }),
125
+ );
126
+ expect(ctx.json).toHaveBeenCalledWith({
127
+ result: { score: 0.95 },
128
+ is_error: false,
129
+ });
130
+ });
131
+
132
+ it("returns is_error with message when activity throws", async () => {
133
+ const coll = createCollection([mockFailingActivity]);
134
+ const ctx = createMockContext();
135
+
136
+ const failPayload: RemoteActivityExecutionPayload = {
137
+ ...payload,
138
+ activity_name: "fail_always",
139
+ };
140
+
141
+ await coll.execute(ctx, failPayload);
142
+
143
+ expect(ctx.json).toHaveBeenCalledWith(
144
+ { result: null, is_error: true, error: "Intentional failure" },
145
+ 500,
146
+ );
147
+ });
148
+
149
+ it("throws 404 when activity_name is not found", async () => {
150
+ const coll = createCollection();
151
+ const ctx = createMockContext();
152
+
153
+ const unknownPayload: RemoteActivityExecutionPayload = {
154
+ ...payload,
155
+ activity_name: "nonexistent",
156
+ };
157
+
158
+ await expect(coll.execute(ctx, unknownPayload)).rejects.toThrow();
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,136 @@
1
+ import { RemoteActivityDefinition, RemoteActivityExecutionPayload, RemoteActivityExecutionResponse } from "@vertesia/common";
2
+ import { Context } from "hono";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { authorize } from "./auth.js";
5
+ import { CollectionProperties, ICollection, ToolExecutionContext } from "./types.js";
6
+ import { kebabCaseToTitle } from "./utils.js";
7
+
8
+ /**
9
+ * Context provided to activity handlers during execution.
10
+ * Same interface as ToolExecutionContext: includes token, decoded payload, and getClient().
11
+ */
12
+ export type ActivityExecutionContext = ToolExecutionContext;
13
+
14
+ /**
15
+ * Function signature for a remote activity handler.
16
+ */
17
+ export type ActivityFn<ParamsT extends Record<string, any> = Record<string, any>> = (
18
+ payload: RemoteActivityExecutionPayload<ParamsT>,
19
+ context: ActivityExecutionContext
20
+ ) => Promise<any>;
21
+
22
+ /**
23
+ * An activity definition within an ActivityCollection.
24
+ */
25
+ export interface ActivityDefinition<ParamsT extends Record<string, any> = Record<string, any>> {
26
+ /** Activity name (snake_case) */
27
+ name: string;
28
+ /** Display title */
29
+ title?: string;
30
+ /** Description of what the activity does */
31
+ description?: string;
32
+ /** JSON Schema for the activity input parameters */
33
+ input_schema?: Record<string, any>;
34
+ /** JSON Schema for the activity output */
35
+ output_schema?: Record<string, any>;
36
+ /** The activity handler function */
37
+ run: ActivityFn<ParamsT>;
38
+ }
39
+
40
+ export interface ActivityCollectionProperties extends CollectionProperties {
41
+ activities: ActivityDefinition[];
42
+ }
43
+
44
+ /**
45
+ * A collection of remote activities exposed by a tool server for DSL workflows.
46
+ * Follows the same collection pattern as ToolCollection and SkillCollection.
47
+ */
48
+ export class ActivityCollection implements ICollection<ActivityDefinition> {
49
+ name: string;
50
+ title?: string;
51
+ icon?: string;
52
+ description?: string;
53
+ private registry: Record<string, ActivityDefinition> = {};
54
+
55
+ constructor({ name, title, icon, description, activities }: ActivityCollectionProperties) {
56
+ this.name = name;
57
+ this.title = title || kebabCaseToTitle(name);
58
+ this.icon = icon;
59
+ this.description = description;
60
+ for (const activity of activities) {
61
+ this.registry[activity.name] = activity;
62
+ }
63
+ }
64
+
65
+ [Symbol.iterator](): Iterator<ActivityDefinition> {
66
+ let index = 0;
67
+ const activities = Object.values(this.registry);
68
+ return {
69
+ next(): IteratorResult<ActivityDefinition> {
70
+ if (index < activities.length) {
71
+ return { value: activities[index++], done: false };
72
+ }
73
+ return { done: true, value: undefined };
74
+ }
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Get activity definitions for discovery (metadata only, no run function).
80
+ */
81
+ getActivityDefinitions(): RemoteActivityDefinition[] {
82
+ return Object.values(this.registry).map(activity => ({
83
+ name: activity.name,
84
+ title: activity.title,
85
+ description: activity.description,
86
+ input_schema: activity.input_schema,
87
+ output_schema: activity.output_schema,
88
+ }));
89
+ }
90
+
91
+ getActivity(name: string): ActivityDefinition {
92
+ const activity = this.registry[name];
93
+ if (!activity) {
94
+ throw new HTTPException(404, {
95
+ message: `Activity not found: ${name}. Available: ${Object.keys(this.registry).join(', ')}`
96
+ });
97
+ }
98
+ return activity;
99
+ }
100
+
101
+ /**
102
+ * Execute an activity from an HTTP POST request.
103
+ */
104
+ async execute(ctx: Context, payload: RemoteActivityExecutionPayload): Promise<Response> {
105
+ const activityName = payload.activity_name;
106
+
107
+ console.debug(`[ActivityCollection] Activity call received: ${activityName}`, {
108
+ collection: this.name,
109
+ metadata: payload.metadata,
110
+ });
111
+
112
+ const activity = this.getActivity(activityName);
113
+
114
+ const context = await authorize(ctx, payload.metadata?.endpoints);
115
+
116
+ try {
117
+ const result = await activity.run(payload, context);
118
+ return ctx.json({
119
+ result,
120
+ is_error: false,
121
+ } satisfies RemoteActivityExecutionResponse);
122
+ } catch (err: unknown) {
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ console.error(`[ActivityCollection] Activity execution failed: ${activityName}`, {
125
+ collection: this.name,
126
+ metadata: payload.metadata,
127
+ error: message,
128
+ });
129
+ return ctx.json({
130
+ result: null,
131
+ is_error: true,
132
+ error: message,
133
+ } satisfies RemoteActivityExecutionResponse, 500);
134
+ }
135
+ }
136
+ }
package/src/auth.ts CHANGED
@@ -7,7 +7,7 @@ import { ToolExecutionContext } from "./types.js";
7
7
  const cache: Record<string, JWTVerifyGetKey> = {};
8
8
 
9
9
  export async function getJwks(url: string) {
10
- if (!cache.url) {
10
+ if (!cache[url]) {
11
11
  console.log('JWKS cache miss for: ', url);
12
12
  const jwks = await fetch(url).then(r => {
13
13
  if (r.ok) {
@@ -17,9 +17,9 @@ export async function getJwks(url: string) {
17
17
  }).catch(err => {
18
18
  throw new Error("Failed to fetch jwks: " + err.message);
19
19
  })
20
- cache.url = createLocalJWKSet(jwks);
20
+ cache[url] = createLocalJWKSet(jwks);
21
21
  }
22
- return cache.url;
22
+ return cache[url];
23
23
  }
24
24
 
25
25
  export async function verifyToken(token: string) {
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./ActivityCollection.js";
1
2
  export { authorize, AuthSession } from "./auth.js";
2
3
  export * from "./ContentTypesCollection.js";
3
4
  export * from "./RenderingTemplateCollection.js";
@@ -0,0 +1,165 @@
1
+ import { Hono } from "hono";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ActivityCollection, ActivityDefinition } from "../ActivityCollection.js";
4
+ import { createActivitiesRoute } from "./activities.js";
5
+ import { ToolServerConfig } from "./types.js";
6
+
7
+ // Mock authorize to avoid JWT verification
8
+ vi.mock("../auth.js", () => ({
9
+ authorize: vi.fn().mockResolvedValue({ token: "test-token" }),
10
+ AuthSession: vi.fn(),
11
+ }));
12
+
13
+ const mockActivity: ActivityDefinition = {
14
+ name: "analyze_sentiment",
15
+ title: "Analyze Sentiment",
16
+ description: "Analyzes text sentiment",
17
+ input_schema: { type: "object", properties: { text: { type: "string" } } },
18
+ run: vi.fn().mockResolvedValue({ score: 0.95 }),
19
+ };
20
+
21
+ const mockActivity2: ActivityDefinition = {
22
+ name: "extract_entities",
23
+ description: "Extracts entities",
24
+ run: vi.fn().mockResolvedValue({ entities: ["foo"] }),
25
+ };
26
+
27
+ function createApp(activities: ActivityCollection[] = []) {
28
+ const app = new Hono();
29
+ const config = { activities } as ToolServerConfig;
30
+ createActivitiesRoute(app, "/api/activities", config);
31
+ return app;
32
+ }
33
+
34
+ function createTestCollections() {
35
+ const coll1 = new ActivityCollection({
36
+ name: "nlp",
37
+ description: "NLP activities",
38
+ activities: [mockActivity],
39
+ });
40
+ const coll2 = new ActivityCollection({
41
+ name: "extraction",
42
+ description: "Extraction activities",
43
+ activities: [mockActivity2],
44
+ });
45
+ return [coll1, coll2];
46
+ }
47
+
48
+ describe("Activities server routes", () => {
49
+ describe("GET /api/activities", () => {
50
+ it("returns all activity definitions across collections", async () => {
51
+ const app = createApp(createTestCollections());
52
+ const res = await app.request("/api/activities");
53
+ const body = await res.json() as any;
54
+
55
+ expect(res.status).toBe(200);
56
+ expect(body.activities).toHaveLength(2);
57
+ expect(body.activities[0].name).toBe("analyze_sentiment");
58
+ expect(body.activities[1].name).toBe("extract_entities");
59
+ expect(body.collections).toHaveLength(2);
60
+ });
61
+
62
+ it("returns empty when no collections configured", async () => {
63
+ const app = createApp([]);
64
+ const res = await app.request("/api/activities");
65
+ const body = await res.json() as any;
66
+
67
+ expect(res.status).toBe(200);
68
+ expect(body.activities).toHaveLength(0);
69
+ });
70
+ });
71
+
72
+ describe("GET /api/activities/{collection}", () => {
73
+ it("returns activities for a specific collection", async () => {
74
+ const app = createApp(createTestCollections());
75
+ const res = await app.request("/api/activities/nlp");
76
+ const body = await res.json() as any;
77
+
78
+ expect(res.status).toBe(200);
79
+ expect(body.name).toBe("nlp");
80
+ expect(body.activities).toHaveLength(1);
81
+ expect(body.activities[0].name).toBe("analyze_sentiment");
82
+ });
83
+ });
84
+
85
+ describe("POST /api/activities", () => {
86
+ it("executes activity by name", async () => {
87
+ const app = createApp(createTestCollections());
88
+ const res = await app.request("/api/activities", {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ "Authorization": "Bearer test-token",
93
+ },
94
+ body: JSON.stringify({
95
+ activity_name: "analyze_sentiment",
96
+ params: { text: "hello" },
97
+ metadata: {
98
+ workflow_name: "wf",
99
+ account_id: "acc",
100
+ project_id: "proj",
101
+ },
102
+ }),
103
+ });
104
+
105
+ expect(res.status).toBe(200);
106
+ const body = await res.json() as any;
107
+ expect(body.result).toEqual({ score: 0.95 });
108
+ expect(body.is_error).toBe(false);
109
+ });
110
+
111
+ it("returns 404 for unknown activity name", async () => {
112
+ const app = createApp(createTestCollections());
113
+ const res = await app.request("/api/activities", {
114
+ method: "POST",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ "Authorization": "Bearer test-token",
118
+ },
119
+ body: JSON.stringify({
120
+ activity_name: "nonexistent",
121
+ params: {},
122
+ metadata: {},
123
+ }),
124
+ });
125
+
126
+ expect(res.status).toBe(404);
127
+ });
128
+
129
+ it("returns 400 for missing activity_name", async () => {
130
+ const app = createApp(createTestCollections());
131
+ const res = await app.request("/api/activities", {
132
+ method: "POST",
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ "Authorization": "Bearer test-token",
136
+ },
137
+ body: JSON.stringify({ params: {} }),
138
+ });
139
+
140
+ expect(res.status).toBe(400);
141
+ });
142
+ });
143
+
144
+ describe("POST /api/activities/{collection}", () => {
145
+ it("routes to correct collection", async () => {
146
+ const app = createApp(createTestCollections());
147
+ const res = await app.request("/api/activities/extraction", {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ "Authorization": "Bearer test-token",
152
+ },
153
+ body: JSON.stringify({
154
+ activity_name: "extract_entities",
155
+ params: {},
156
+ metadata: {},
157
+ }),
158
+ });
159
+
160
+ expect(res.status).toBe(200);
161
+ const body = await res.json() as any;
162
+ expect(body.result).toEqual({ entities: ["foo"] });
163
+ });
164
+ });
165
+ });
@@ -0,0 +1,114 @@
1
+ import { RemoteActivityDefinition, RemoteActivityExecutionPayload } from "@vertesia/common";
2
+ import { Context, Hono } from "hono";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { ActivityCollection } from "../ActivityCollection.js";
5
+ import { ToolServerConfig } from "./types.js";
6
+
7
+ /**
8
+ * Safely parse JSON from a request body. Throws HTTPException(400) on invalid JSON.
9
+ */
10
+ async function safeParseJson(c: Context): Promise<unknown> {
11
+ try {
12
+ return await c.req.json();
13
+ } catch {
14
+ throw new HTTPException(400, {
15
+ message: 'Invalid JSON in request body.'
16
+ });
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Validates the structure of a RemoteActivityExecutionPayload.
22
+ * Returns the parsed payload or throws HTTPException(400).
23
+ */
24
+ function parseActivityPayload(body: unknown): RemoteActivityExecutionPayload {
25
+ if (!body || typeof body !== 'object') {
26
+ throw new HTTPException(400, {
27
+ message: 'Invalid or missing activity execution payload.'
28
+ });
29
+ }
30
+ const obj = body as Record<string, any>;
31
+ if (typeof obj.activity_name !== 'string' || !obj.activity_name) {
32
+ throw new HTTPException(400, {
33
+ message: 'Missing required field: activity_name'
34
+ });
35
+ }
36
+ return {
37
+ activity_name: obj.activity_name,
38
+ params: obj.params || {},
39
+ metadata: obj.metadata || {},
40
+ };
41
+ }
42
+
43
+ export function createActivitiesRoute(app: Hono, basePath: string, config: ToolServerConfig) {
44
+ const { activities = [] } = config;
45
+
46
+ // Build a map of activity name -> collection for routing
47
+ const activityToCollection = new Map<string, ActivityCollection>();
48
+ for (const coll of activities) {
49
+ for (const def of coll.getActivityDefinitions()) {
50
+ activityToCollection.set(def.name, coll);
51
+ }
52
+ }
53
+
54
+ // GET /api/activities - List all activities across all collections
55
+ app.get(basePath, (c) => {
56
+ const allActivities: RemoteActivityDefinition[] = [];
57
+ for (const coll of activities) {
58
+ allActivities.push(...coll.getActivityDefinitions());
59
+ }
60
+ return c.json({
61
+ title: 'All Activities',
62
+ description: 'All available remote activities across all collections',
63
+ activities: allActivities,
64
+ collections: activities.map(a => ({
65
+ name: a.name,
66
+ title: a.title,
67
+ description: a.description,
68
+ })),
69
+ });
70
+ });
71
+
72
+ // POST /api/activities - Execute an activity by name (routes to correct collection)
73
+ app.post(basePath, async (c) => {
74
+ const body = await safeParseJson(c);
75
+ const payload = parseActivityPayload(body);
76
+
77
+ const collection = activityToCollection.get(payload.activity_name);
78
+ if (!collection) {
79
+ throw new HTTPException(404, {
80
+ message: `Activity not found: ${payload.activity_name}. Available: ${Array.from(activityToCollection.keys()).join(', ')}`
81
+ });
82
+ }
83
+
84
+ return collection.execute(c, payload);
85
+ });
86
+
87
+ // Per-collection endpoints
88
+ for (const coll of activities) {
89
+ app.route(`${basePath}/${coll.name}`, createActivityEndpoints(coll));
90
+ }
91
+ }
92
+
93
+ function createActivityEndpoints(coll: ActivityCollection): Hono {
94
+ const endpoint = new Hono();
95
+
96
+ // GET /api/activities/{collection} - List activities in this collection
97
+ endpoint.get('/', (c) => {
98
+ return c.json({
99
+ name: coll.name,
100
+ title: coll.title,
101
+ description: coll.description,
102
+ activities: coll.getActivityDefinitions(),
103
+ });
104
+ });
105
+
106
+ // POST /api/activities/{collection} - Execute activity in this collection
107
+ endpoint.post('/', async (c: Context) => {
108
+ const body = await safeParseJson(c);
109
+ const payload = parseActivityPayload(body);
110
+ return coll.execute(c, payload);
111
+ });
112
+
113
+ return endpoint;
114
+ }
@@ -1,4 +1,4 @@
1
- import { AppPackage, AppPackageScope, AppWidgetInfo, CatalogInteractionRef, InCodeTypeDefinition } from "@vertesia/common";
1
+ import { AppPackage, AppPackageScope, AppWidgetInfo, CatalogInteractionRef, InCodeTypeDefinition, RemoteActivityDefinition } from "@vertesia/common";
2
2
  import { Context, Hono } from "hono";
3
3
  import { ToolUseContext } from "../types.js";
4
4
  import { ToolServerConfig } from "./types.js";
@@ -103,6 +103,15 @@ const builders: Record<Exclude<AppPackageScope, 'all'>, (pkg: AppPackage, config
103
103
  pkg.settings_schema = { ...config.settings };
104
104
  }
105
105
  },
106
+ async activities(pkg: AppPackage, config: ToolServerConfig) {
107
+ const allActivities: RemoteActivityDefinition[] = [];
108
+ for (const coll of config.activities || []) {
109
+ for (const def of coll.getActivityDefinitions()) {
110
+ allActivities.push({ ...def, collection: coll.name });
111
+ }
112
+ }
113
+ pkg.activities = allActivities;
114
+ },
106
115
  }
107
116
 
108
117
 
@@ -120,6 +129,7 @@ async function handlePackageRequest(c: Context, config: ToolServerConfig) {
120
129
  await builders.widgets(pkg, config, c);
121
130
  await builders.ui(pkg, config, c);
122
131
  await builders.settings(pkg, config, c);
132
+ await builders.activities(pkg, config, c);
123
133
  } else {
124
134
  if (scopes.has('tools')) {
125
135
  await builders.tools(pkg, config, c);
@@ -140,7 +150,10 @@ async function handlePackageRequest(c: Context, config: ToolServerConfig) {
140
150
  await builders.ui(pkg, config, c);
141
151
  }
142
152
  if (scopes.has('settings')) {
143
- builders.settings(pkg, config, c);
153
+ await builders.settings(pkg, config, c);
154
+ }
155
+ if (scopes.has('activities')) {
156
+ await builders.activities(pkg, config, c);
144
157
  }
145
158
  }
146
159