@vertesia/tools-sdk 1.4.0-dev.20260615.051508Z → 1.4.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.
@@ -1,5 +1,4 @@
1
1
  import type {
2
- AppDashboardDefinition,
3
2
  AppPackage,
4
3
  AppPackageScope,
5
4
  AppWidgetInfo,
@@ -16,19 +15,14 @@ function getRequestPayload<T>(c: Context): Promise<T | undefined> {
16
15
  return c.req.method === 'POST' ? c.req.json<T>() : Promise.resolve(undefined);
17
16
  }
18
17
 
19
- export interface BuildAppPackageOptions {
20
- scope?: AppPackageScope | AppPackageScope[] | string;
21
- origin?: string;
22
- toolUseContext?: ToolUseContext;
23
- }
24
-
25
- type AppPackageBuilder = (pkg: AppPackage, config: ToolServerConfig, options: BuildAppPackageOptions) => Promise<void>;
26
-
27
- const builders: Record<Exclude<AppPackageScope, 'all'>, AppPackageBuilder> = {
28
- async tools(pkg: AppPackage, config: ToolServerConfig, options: BuildAppPackageOptions) {
18
+ const builders: Record<
19
+ Exclude<AppPackageScope, 'all'>,
20
+ (pkg: AppPackage, config: ToolServerConfig, c: Context) => Promise<void>
21
+ > = {
22
+ async tools(pkg: AppPackage, config: ToolServerConfig, c: Context) {
29
23
  const { tools: toolCollections = [], skills: skillCollections = [] } = config;
30
24
 
31
- const filterContext = options.toolUseContext;
25
+ const filterContext = await getRequestPayload<ToolUseContext>(c);
32
26
 
33
27
  // Aggregate all tools from all collections
34
28
  const allTools = toolCollections.flatMap((collection) => collection.getToolDefinitions(filterContext));
@@ -65,23 +59,13 @@ const builders: Record<Exclude<AppPackageScope, 'all'>, AppPackageBuilder> = {
65
59
  pkg.interactions = allInteractions;
66
60
  },
67
61
  async types(pkg: AppPackage, config: ToolServerConfig) {
68
- // A type's public id is its BARE name: stored objects reference it as the portable
69
- // `app:<app>:<type>` string, so the in-code collection (a code-organization grouping,
70
- // unlike tool/skill collections which are semantic) must NOT leak into the id.
71
- // Names must therefore be unique across collections — fail the package build otherwise.
72
62
  const allTypes: InCodeTypeDefinition[] = [];
73
- const seen = new Map<string, string>();
74
63
  for (const coll of config.types || []) {
75
64
  for (const type of coll.types) {
76
- const existing = seen.get(type.name);
77
- if (existing) {
78
- throw new Error(
79
- `Duplicate content type name '${type.name}' (collections '${existing}' and '${coll.name}'). ` +
80
- `Type names must be unique across collections so the portable 'app:<app>:${type.name}' ref resolves.`,
81
- );
82
- }
83
- seen.set(type.name, coll.name);
84
- allTypes.push({ ...type, id: type.name });
65
+ allTypes.push({
66
+ ...type,
67
+ id: `${coll.name}:${type.name}`,
68
+ });
85
69
  }
86
70
  }
87
71
  pkg.types = allTypes;
@@ -102,19 +86,6 @@ const builders: Record<Exclude<AppPackageScope, 'all'>, AppPackageBuilder> = {
102
86
  })),
103
87
  );
104
88
  },
105
- async dashboards(pkg: AppPackage, config: ToolServerConfig) {
106
- const seen = new Set<string>();
107
- const dashboards: AppDashboardDefinition[] = [];
108
- for (const dashboard of config.dashboards || []) {
109
- if (seen.has(dashboard.id)) {
110
- console.warn(`[app-package] Duplicate dashboard id "${dashboard.id}", skipping`);
111
- continue;
112
- }
113
- seen.add(dashboard.id);
114
- dashboards.push(dashboard);
115
- }
116
- pkg.dashboards = dashboards;
117
- },
118
89
  async widgets(pkg: AppPackage, config: ToolServerConfig) {
119
90
  const { skills: skillCollections = [] } = config;
120
91
  const widgets: Record<string, AppWidgetInfo> = {};
@@ -131,10 +102,10 @@ const builders: Record<Exclude<AppPackageScope, 'all'>, AppPackageBuilder> = {
131
102
  }
132
103
  pkg.widgets = widgets;
133
104
  },
134
- async ui(pkg: AppPackage, config: ToolServerConfig, options: BuildAppPackageOptions) {
105
+ async ui(pkg: AppPackage, config: ToolServerConfig, c: Context) {
135
106
  if (config.uiConfig) {
136
107
  pkg.ui = { ...config.uiConfig };
137
- const origin = options.origin || 'http://localhost';
108
+ const origin = new URL(c.req.url).origin;
138
109
  pkg.ui.src = new URL(pkg.ui.src, origin).toString();
139
110
  if (!pkg.ui.isolation) {
140
111
  pkg.ui.isolation = 'shadow';
@@ -157,71 +128,52 @@ const builders: Record<Exclude<AppPackageScope, 'all'>, AppPackageBuilder> = {
157
128
  },
158
129
  };
159
130
 
160
- function normalizeScopes(scope: BuildAppPackageOptions['scope']): Set<AppPackageScope> {
161
- const values = Array.isArray(scope) ? scope : typeof scope === 'string' ? scope.split(',') : [scope || 'all'];
162
- return new Set(values.filter(Boolean) as AppPackageScope[]);
163
- }
164
-
165
- export async function buildAppPackage(
166
- config: ToolServerConfig,
167
- options: BuildAppPackageOptions = {},
168
- ): Promise<AppPackage> {
131
+ async function handlePackageRequest(c: Context, config: ToolServerConfig) {
132
+ const scope = c.req.query('scope') || 'all';
169
133
  const pkg: AppPackage = {};
170
134
 
171
- const scopes = normalizeScopes(options.scope);
135
+ const scopes = new Set<AppPackageScope>(scope.split(',') as AppPackageScope[]);
136
+ // TODO build pkg based on the query param scope
172
137
  if (scopes.has('all')) {
173
- await builders.tools(pkg, config, options);
174
- await builders.interactions(pkg, config, options);
175
- await builders.types(pkg, config, options);
176
- await builders.processes(pkg, config, options);
177
- await builders.templates(pkg, config, options);
178
- await builders.dashboards(pkg, config, options);
179
- await builders.widgets(pkg, config, options);
180
- await builders.ui(pkg, config, options);
181
- await builders.settings(pkg, config, options);
182
- await builders.activities(pkg, config, options);
138
+ await builders.tools(pkg, config, c);
139
+ await builders.interactions(pkg, config, c);
140
+ await builders.types(pkg, config, c);
141
+ await builders.processes(pkg, config, c);
142
+ await builders.templates(pkg, config, c);
143
+ await builders.widgets(pkg, config, c);
144
+ await builders.ui(pkg, config, c);
145
+ await builders.settings(pkg, config, c);
146
+ await builders.activities(pkg, config, c);
183
147
  } else {
184
148
  if (scopes.has('tools')) {
185
- await builders.tools(pkg, config, options);
149
+ await builders.tools(pkg, config, c);
186
150
  }
187
151
  if (scopes.has('interactions')) {
188
- await builders.interactions(pkg, config, options);
152
+ await builders.interactions(pkg, config, c);
189
153
  }
190
154
  if (scopes.has('types')) {
191
- await builders.types(pkg, config, options);
155
+ await builders.types(pkg, config, c);
192
156
  }
193
157
  if (scopes.has('processes')) {
194
- await builders.processes(pkg, config, options);
158
+ await builders.processes(pkg, config, c);
195
159
  }
196
160
  if (scopes.has('templates')) {
197
- await builders.templates(pkg, config, options);
198
- }
199
- if (scopes.has('dashboards')) {
200
- await builders.dashboards(pkg, config, options);
161
+ await builders.templates(pkg, config, c);
201
162
  }
202
163
  if (scopes.has('widgets')) {
203
- await builders.widgets(pkg, config, options);
164
+ await builders.widgets(pkg, config, c);
204
165
  }
205
166
  if (scopes.has('ui')) {
206
- await builders.ui(pkg, config, options);
167
+ await builders.ui(pkg, config, c);
207
168
  }
208
169
  if (scopes.has('settings')) {
209
- await builders.settings(pkg, config, options);
170
+ await builders.settings(pkg, config, c);
210
171
  }
211
172
  if (scopes.has('activities')) {
212
- await builders.activities(pkg, config, options);
173
+ await builders.activities(pkg, config, c);
213
174
  }
214
175
  }
215
176
 
216
- return pkg;
217
- }
218
-
219
- async function handlePackageRequest(c: Context, config: ToolServerConfig) {
220
- const pkg = await buildAppPackage(config, {
221
- scope: c.req.query('scope') || 'all',
222
- origin: new URL(c.req.url).origin,
223
- toolUseContext: await getRequestPayload<ToolUseContext>(c),
224
- });
225
177
  return c.json(pkg);
226
178
  }
227
179
 
@@ -10,15 +10,13 @@ import type { ToolServerConfig } from './types.js';
10
10
  export function createContentTypesRoute(app: Hono, basePath: string, config: ToolServerConfig) {
11
11
  const { types = [] } = config;
12
12
 
13
- // GET /api/types - Returns all content types from all collections.
14
- // A type's public id is its declared BARE name, verbatim (`app:<app>:<type>` once
15
- // app-prefixed): collections organize code, they are not part of the type identity.
13
+ // GET /api/types - Returns all interactions from all collections
16
14
  app.get(basePath, (c) => {
17
15
  const allTypes: InCodeTypeDefinition[] = [];
18
16
 
19
17
  for (const coll of types) {
20
18
  for (const type of coll.types) {
21
- allTypes.push({ ...type, id: type.name });
19
+ allTypes.push({ ...type, id: `${coll.name}:${toPathName(type.name)}` });
22
20
  }
23
21
  }
24
22
 
@@ -39,51 +37,20 @@ export function createContentTypesRoute(app: Hono, basePath: string, config: Too
39
37
  app.route(`${basePath}/${coll.name}`, createContentTypeEndpoints(coll));
40
38
  }
41
39
 
42
- // GET /api/types/:name - Direct access to content type.
43
- // Canonical form is the BARE type name (`<type>`), matching the portable
44
- // `app:<app>:<type>` ref convention used by objects.create/search — the
45
- // collection is an in-code grouping, not part of the type's public id.
46
- // `<collection>:<type>` is also accepted as a backward-compatible alias.
40
+ // GET /api/types/:name - Direct access to content type
47
41
  app.get(`${basePath}/:name`, async (c) => {
48
42
  const name = c.req.param('name');
49
43
  const parts = name.split(':');
50
- if (parts.length === 1) {
51
- // Match the declared name verbatim; fall back to the toPathName variant for
52
- // leniency with legacy refs that used the path-mangled form.
53
- const matches = types.flatMap((coll) =>
54
- coll.types
55
- .filter((t) => t.name === name || toPathName(t.name) === name)
56
- .map((t) => ({ coll, type: t })),
57
- );
58
- if (matches.length === 1) {
59
- return c.json({ ...matches[0].type, id: matches[0].type.name });
60
- }
61
- if (matches.length > 1) {
62
- const colls = matches.map((m) => m.coll.name).join(', ');
63
- throw new HTTPException(409, {
64
- message:
65
- `Ambiguous content type name '${name}': defined in collections ${colls}. ` +
66
- 'Type names must be unique across collections so the portable ' +
67
- `'app:<app>:${name}' ref resolves; rename one of them, or address it as '<collection>:<type>'.`,
68
- });
69
- }
70
- throw new HTTPException(404, {
71
- message: `No content type found with name: ${name}`,
72
- });
73
- }
74
44
  if (parts.length !== 2) {
75
45
  throw new HTTPException(400, {
76
- message: "Invalid content type name. Expected '<type>' or 'collection:type'",
46
+ message: "Invalid content type name. Expected format 'collection:type'",
77
47
  });
78
48
  }
79
49
  const collName = parts[0];
80
- const typeName = parts[1];
81
- const ctype = types
82
- .find((t) => t.name === collName)
83
- ?.types.find((t) => t.name === typeName || toPathName(t.name) === toPathName(typeName));
50
+ const typeName = toPathName(parts[1]);
51
+ const ctype = types.find((t) => t.name === collName)?.getTypeByName(typeName);
84
52
  if (ctype) {
85
- // Alias lookup still returns the canonical bare id.
86
- return c.json({ ...ctype, id: ctype.name });
53
+ return c.json({ ...ctype, id: `${collName}:${typeName}` });
87
54
  }
88
55
 
89
56
  throw new HTTPException(404, {
@@ -96,12 +63,12 @@ function createContentTypeEndpoints(coll: ContentTypesCollection): Hono {
96
63
  const endpoint = new Hono();
97
64
 
98
65
  endpoint.get('/', (c: Context) => {
99
- return c.json(coll.types.map((t) => ({ ...t, id: t.name })));
66
+ return c.json(coll.types.map((t) => ({ ...t, id: `${coll.name}:${toPathName(t.name)}` })));
100
67
  });
101
68
 
102
69
  endpoint.get('/:name', (c: Context) => {
103
70
  const name = c.req.param('name');
104
- const ctype = coll.types.find((t) => t.name === name || toPathName(t.name) === name);
71
+ const ctype = coll.types.find((t) => toPathName(t.name) === name);
105
72
  if (!ctype) {
106
73
  throw new HTTPException(404, {
107
74
  message: `No content type found with name: ${name}`,
@@ -109,7 +76,7 @@ function createContentTypeEndpoints(coll: ContentTypesCollection): Hono {
109
76
  }
110
77
  return c.json({
111
78
  ...ctype,
112
- id: ctype.name,
79
+ id: `${coll.name}:${toPathName(ctype.name)}`,
113
80
  });
114
81
  });
115
82
 
@@ -1,10 +1,5 @@
1
1
  import type { JSONSchema } from '@llumiverse/common';
2
- import type {
3
- AppDashboardDefinition,
4
- AppUIConfig,
5
- InCodeProcessDefinition,
6
- ProjectConfiguration,
7
- } from '@vertesia/common';
2
+ import type { AppUIConfig, InCodeProcessDefinition, ProjectConfiguration } from '@vertesia/common';
8
3
  import type { Context } from 'hono';
9
4
  import type { ActivityCollection } from '../ActivityCollection.js';
10
5
  import type { AuthSession } from '../auth.js';
@@ -68,10 +63,6 @@ export interface ToolServerConfig {
68
63
  * Process definitions to expose as app-contributed processes.
69
64
  */
70
65
  processes?: InCodeProcessDefinition[];
71
- /**
72
- * Dashboard definitions to expose as app-contributed dashboards.
73
- */
74
- dashboards?: AppDashboardDefinition[];
75
66
  /**
76
67
  * Skill collections to expose
77
68
  */
package/src/server.ts CHANGED
@@ -5,7 +5,6 @@ import { z } from 'zod';
5
5
  import { createActivitiesRoute } from './server/activities.js';
6
6
  import { createPackageRoute } from './server/app-package.js';
7
7
  import { createContentTypesRoute } from './server/content-types.js';
8
- import { createDashboardsRoute } from './server/dashboards.js';
9
8
  import { createInteractionsRoute } from './server/interactions.js';
10
9
  import { createMcpRoute } from './server/mcp.js';
11
10
  import { createProcessesRoute } from './server/processes.js';
@@ -50,7 +49,6 @@ export function createToolServer(config: ToolServerConfig): Hono {
50
49
  skills = [],
51
50
  templates = [],
52
51
  activities = [],
53
- dashboards = [],
54
52
  mcpProviders = [],
55
53
  disableHtml = false,
56
54
  } = config;
@@ -102,7 +100,6 @@ export function createToolServer(config: ToolServerConfig): Hono {
102
100
  interactions: interactions.map((col) => `${prefix}/interactions/${col.name}`),
103
101
  templates: templates.map((col) => `${prefix}/templates/${col.name}`),
104
102
  processes: `${prefix}/processes`,
105
- dashboards: dashboards.length > 0 ? `${prefix}/dashboards` : undefined,
106
103
  activities: activities.map((col) => `${prefix}/activities/${col.name}`),
107
104
  mcp: mcpProviders.map((p) => `${prefix}/mcp/${p.name}`),
108
105
  },
@@ -118,7 +115,6 @@ export function createToolServer(config: ToolServerConfig): Hono {
118
115
  createTemplatesRoute(app, `${prefix}/templates`, config);
119
116
  createContentTypesRoute(app, `${prefix}/types`, config);
120
117
  createProcessesRoute(app, `${prefix}/processes`, config);
121
- createDashboardsRoute(app, `${prefix}/dashboards`, config);
122
118
  createMcpRoute(app, `${prefix}/mcp`, config);
123
119
 
124
120
  // Global error handler - returns ToolExecutionResponseError format
package/src/types.ts CHANGED
@@ -99,6 +99,16 @@ export type ToolFn<ParamsT extends object = object> = (
99
99
  context: ToolExecutionContext,
100
100
  ) => Promise<ToolExecutionResult>;
101
101
 
102
+ /**
103
+ * Approval behavior class for a tool.
104
+ *
105
+ * - `read_only`: reads or inspects state without changing Vertesia, external systems, or user-visible artifacts.
106
+ * - `side_effecting`: can create, update, delete, send, execute, schedule, or otherwise change state.
107
+ * - `control`: affects agent control flow or tool availability, not user data or external systems.
108
+ * - `requires_confirmation`: high-impact action that must ask the user even in interactive full-control mode.
109
+ */
110
+ export type ToolApprovalClassification = 'read_only' | 'side_effecting' | 'control' | 'requires_confirmation';
111
+
102
112
  export interface ToolUseContext {
103
113
  project_id?: string;
104
114
  account_id?: string;
@@ -124,12 +134,11 @@ export interface Tool<ParamsT extends object = object> extends ToolDefinition {
124
134
  annotations?: MCPToolAnnotations;
125
135
 
126
136
  /**
127
- * When true, agents must obtain explicit user confirmation via `ask_user`
128
- * (Yes/No) before invoking this tool. If the user answers No, the tool
129
- * must not run. Stronger than `annotations.destructiveHint` this is a
130
- * hard contract.
137
+ * Internal approval classification used by interactive agent approval modes.
138
+ * Use `requires_confirmation` for actions that must prompt even in full-control mode.
139
+ * Remote/MCP tools can omit this and rely on annotations.
131
140
  */
132
- requires_user_confirmation?: boolean;
141
+ approval_class?: ToolApprovalClassification;
133
142
 
134
143
  /**
135
144
  * Optional filter to check if the tool is enabled for the given project configuration.
@@ -1,4 +0,0 @@
1
- import type { Hono } from 'hono';
2
- import type { ToolServerConfig } from './types.js';
3
- export declare function createDashboardsRoute(app: Hono, basePath: string, config: ToolServerConfig): void;
4
- //# sourceMappingURL=dashboards.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dashboards.d.ts","sourceRoot":"","sources":["../../src/server/dashboards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,QAqB1F"}
@@ -1,25 +0,0 @@
1
- import { HTTPException } from 'hono/http-exception';
2
- export function createDashboardsRoute(app, basePath, config) {
3
- const { dashboards = [] } = config;
4
- app.get(basePath, (c) => {
5
- return c.json({
6
- title: 'All Dashboards',
7
- description: 'All available dashboard definitions',
8
- dashboards,
9
- });
10
- });
11
- app.get(`${basePath}/:id`, (c) => {
12
- const id = c.req.param('id');
13
- const dashboard = findDashboard(dashboards, id);
14
- if (!dashboard) {
15
- throw new HTTPException(404, {
16
- message: `No dashboard found with id: ${id}`,
17
- });
18
- }
19
- return c.json(dashboard);
20
- });
21
- }
22
- function findDashboard(dashboards, id) {
23
- return dashboards.find((dashboard) => dashboard.id === id || dashboard.name === id || dashboard.title === id);
24
- }
25
- //# sourceMappingURL=dashboards.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dashboards.js","sourceRoot":"","sources":["../../src/server/dashboards.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,UAAU,qBAAqB,CAAC,GAAS,EAAE,QAAgB,EAAE,MAAwB;IACvF,MAAM,EAAE,UAAU,GAAG,EAAE,EAAE,GAAG,MAAM,CAAC;IAEnC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QACpB,OAAO,CAAC,CAAC,IAAI,CAAC;YACV,KAAK,EAAE,gBAAgB;YACvB,WAAW,EAAE,qCAAqC;YAClD,UAAU;SACb,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE;gBACzB,OAAO,EAAE,+BAA+B,EAAE,EAAE;aAC/C,CAAC,CAAC;QACP,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,aAAa,CAAC,UAAoC,EAAE,EAAU;IACnE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,SAAS,CAAC,IAAI,KAAK,EAAE,IAAI,SAAS,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;AAClH,CAAC"}
@@ -1,140 +0,0 @@
1
- import { PromptRole } from '@llumiverse/common';
2
- import { PROCESS_DEFINITION_FORMAT_VERSION, TemplateType } from '@vertesia/common';
3
- import { describe, expect, it } from 'vitest';
4
- import { ContentTypesCollection } from '../ContentTypesCollection.js';
5
- import { InteractionCollection } from '../InteractionCollection.js';
6
- import { buildAppPackage } from './app-package.js';
7
- import type { ToolServerConfig } from './types.js';
8
-
9
- describe('buildAppPackage', () => {
10
- it('builds the same package artifact inventory used by the package route', async () => {
11
- const config = {
12
- interactions: [
13
- new InteractionCollection({
14
- name: 'claims',
15
- interactions: [
16
- {
17
- name: 'review',
18
- title: 'Review Claim',
19
- prompts: [
20
- {
21
- role: PromptRole.user,
22
- content: '{{user_prompt}}',
23
- content_type: TemplateType.handlebars,
24
- },
25
- ],
26
- },
27
- ],
28
- }),
29
- ],
30
- types: [
31
- new ContentTypesCollection({
32
- name: 'claims',
33
- types: [
34
- {
35
- name: 'claim',
36
- object_schema: {
37
- type: 'object',
38
- properties: {
39
- status: { type: 'string' },
40
- },
41
- },
42
- },
43
- ],
44
- }),
45
- ],
46
- processes: [
47
- {
48
- id: 'claims:intake',
49
- name: 'Claims Intake',
50
- definition: {
51
- format_version: PROCESS_DEFINITION_FORMAT_VERSION,
52
- process: 'claims_intake',
53
- initial: 'done',
54
- context: {
55
- schema: {
56
- type: 'object',
57
- additionalProperties: true,
58
- },
59
- initial: {},
60
- },
61
- nodes: {
62
- done: { type: 'final', title: 'Done' },
63
- },
64
- },
65
- },
66
- ],
67
- dashboards: [
68
- {
69
- id: 'claims:ops',
70
- title: 'Claims Ops',
71
- spec: {},
72
- },
73
- ],
74
- uiConfig: {
75
- src: '/lib/plugin.js',
76
- available_in: ['app_portal'],
77
- },
78
- } satisfies ToolServerConfig;
79
-
80
- const pkg = await buildAppPackage(config, {
81
- origin: 'https://apps.example.test',
82
- scope: 'all',
83
- });
84
-
85
- expect(pkg.interactions?.map((interaction) => interaction.id)).toEqual(['claims:review']);
86
- expect(pkg.types?.map((type) => type.id)).toEqual(['claim']);
87
- expect(pkg.processes?.map((process) => process.id)).toEqual(['claims:intake']);
88
- expect(pkg.dashboards?.map((dashboard) => dashboard.id)).toEqual(['claims:ops']);
89
- expect(pkg.ui?.src).toBe('https://apps.example.test/lib/plugin.js');
90
- });
91
-
92
- it('honors package scopes', async () => {
93
- const config = {
94
- interactions: [
95
- new InteractionCollection({
96
- name: 'claims',
97
- interactions: [{ name: 'review', prompts: [] }],
98
- }),
99
- ],
100
- types: [
101
- new ContentTypesCollection({
102
- name: 'claims',
103
- types: [{ name: 'claim' }],
104
- }),
105
- ],
106
- } satisfies ToolServerConfig;
107
-
108
- const pkg = await buildAppPackage(config, { scope: 'types' });
109
-
110
- expect(pkg.types?.map((type) => type.id)).toEqual(['claim']);
111
- expect(pkg.interactions).toBeUndefined();
112
- });
113
- });
114
-
115
- describe('buildAppPackage type identity', () => {
116
- it('exposes the bare type name as the public id (collections are code organization only)', async () => {
117
- const config = {
118
- types: [
119
- new ContentTypesCollection({ name: 'claims', types: [{ name: 'claim' }] }),
120
- new ContentTypesCollection({ name: 'audit', types: [{ name: 'audit_event' }] }),
121
- ],
122
- } satisfies ToolServerConfig;
123
-
124
- const pkg = await buildAppPackage(config, { scope: 'types' });
125
- expect(pkg.types?.map((type) => type.id)).toEqual(['claim', 'audit_event']);
126
- });
127
-
128
- it('fails the package build when a type name is duplicated across collections', async () => {
129
- const config = {
130
- types: [
131
- new ContentTypesCollection({ name: 'claims', types: [{ name: 'claim' }] }),
132
- new ContentTypesCollection({ name: 'legacy', types: [{ name: 'claim' }] }),
133
- ],
134
- } satisfies ToolServerConfig;
135
-
136
- await expect(buildAppPackage(config, { scope: 'types' })).rejects.toThrow(
137
- /Duplicate content type name 'claim'/,
138
- );
139
- });
140
- });
@@ -1,64 +0,0 @@
1
- import { Hono } from 'hono';
2
- import { describe, expect, it } from 'vitest';
3
- import { ContentTypesCollection } from '../ContentTypesCollection.js';
4
- import { createContentTypesRoute } from './content-types.js';
5
- import type { ToolServerConfig } from './types.js';
6
-
7
- function makeApp(config: ToolServerConfig): Hono {
8
- const app = new Hono();
9
- createContentTypesRoute(app, '/api/types', config);
10
- return app;
11
- }
12
-
13
- const config = {
14
- types: [
15
- new ContentTypesCollection({
16
- name: 'contracts',
17
- types: [{ name: 'contract' }, { name: 'review_note' }],
18
- }),
19
- new ContentTypesCollection({
20
- name: 'audit',
21
- types: [{ name: 'audit_event' }],
22
- }),
23
- ],
24
- } satisfies ToolServerConfig;
25
-
26
- describe('content types route', () => {
27
- it('lists all types with bare ids (collection is not part of the type identity)', async () => {
28
- const res = await makeApp(config).request('/api/types');
29
- expect(res.status).toBe(200);
30
- const body = (await res.json()) as { types: Array<{ id: string }> };
31
- expect(body.types.map((t) => t.id)).toEqual(['contract', 'review_note', 'audit_event']);
32
- });
33
-
34
- it('resolves the canonical bare type name (the app:<app>:<type> ref convention)', async () => {
35
- const res = await makeApp(config).request('/api/types/contract');
36
- expect(res.status).toBe(200);
37
- const body = (await res.json()) as { id: string; name: string };
38
- expect(body.id).toBe('contract');
39
- expect(body.name).toBe('contract');
40
- });
41
-
42
- it('still resolves the legacy collection:type alias, returning the bare id', async () => {
43
- const res = await makeApp(config).request('/api/types/contracts:contract');
44
- expect(res.status).toBe(200);
45
- const body = (await res.json()) as { id: string };
46
- expect(body.id).toBe('contract');
47
- });
48
-
49
- it('409s on an ambiguous bare name so the collision is loud, not silent', async () => {
50
- const ambiguous = {
51
- types: [
52
- new ContentTypesCollection({ name: 'a', types: [{ name: 'note' }] }),
53
- new ContentTypesCollection({ name: 'b', types: [{ name: 'note' }] }),
54
- ],
55
- } satisfies ToolServerConfig;
56
- const res = await makeApp(ambiguous).request('/api/types/note');
57
- expect(res.status).toBe(409);
58
- });
59
-
60
- it('404s for an unknown type name', async () => {
61
- const res = await makeApp(config).request('/api/types/nope');
62
- expect(res.status).toBe(404);
63
- });
64
- });
@@ -1,31 +0,0 @@
1
- import type { AppDashboardDefinition } from '@vertesia/common';
2
- import type { Hono } from 'hono';
3
- import { HTTPException } from 'hono/http-exception';
4
- import type { ToolServerConfig } from './types.js';
5
-
6
- export function createDashboardsRoute(app: Hono, basePath: string, config: ToolServerConfig) {
7
- const { dashboards = [] } = config;
8
-
9
- app.get(basePath, (c) => {
10
- return c.json({
11
- title: 'All Dashboards',
12
- description: 'All available dashboard definitions',
13
- dashboards,
14
- });
15
- });
16
-
17
- app.get(`${basePath}/:id`, (c) => {
18
- const id = c.req.param('id');
19
- const dashboard = findDashboard(dashboards, id);
20
- if (!dashboard) {
21
- throw new HTTPException(404, {
22
- message: `No dashboard found with id: ${id}`,
23
- });
24
- }
25
- return c.json(dashboard);
26
- });
27
- }
28
-
29
- function findDashboard(dashboards: AppDashboardDefinition[], id: string): AppDashboardDefinition | undefined {
30
- return dashboards.find((dashboard) => dashboard.id === id || dashboard.name === id || dashboard.title === id);
31
- }