@alfe.ai/integration-manifest 0.0.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/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @alfe.ai/integration-manifest
2
+
3
+ Type definitions, Zod validation schemas, and YAML parsing for Alfe integration manifests.
4
+
5
+ ## What It Does
6
+
7
+ Defines the schema and types for integration manifests (`alfe-integration.yaml`) — the standard format describing what an integration provides (capabilities, config fields, install targets, hooks, marketplace metadata, etc.).
8
+
9
+ Used by `services/integrations` to validate and parse integration configurations, by frontend apps to render integration setup UIs, and by the CLI for manifest validation.
10
+
11
+ ## Key Files
12
+
13
+ ```
14
+ src/
15
+ ├── index.ts # Public re-exports
16
+ ├── types.ts # Manifest type definitions (IntegrationManifest, state, health reports)
17
+ ├── schema.ts # Zod validation schemas + buildConfigValidationSchema()
18
+ ├── parser.ts # YAML parsing and validation (parseManifestString, parseManifestFile)
19
+ ├── schema.test.ts # Schema tests
20
+ └── parser.test.ts # Parser tests
21
+ ```
22
+
23
+ ## Example Manifest
24
+
25
+ ```yaml
26
+ # alfe-integration.yaml
27
+ id: discord
28
+ name: Alfe Discord
29
+ version: 1.0.0
30
+ description: Discord bot integration for Alfe agents
31
+ author: alfe
32
+ license: MIT
33
+ depends_on: []
34
+ min_gateway_version: 1.0.0
35
+
36
+ installs:
37
+ skills:
38
+ - path: skills/discord-notify
39
+ plugins:
40
+ - package: "@alfe.ai/openclaw-discord"
41
+
42
+ config_schema:
43
+ - key: bot_token
44
+ type: secret
45
+ label: "Discord Bot Token"
46
+ description: "Bot token from Discord Developer Portal"
47
+ required: true
48
+ - key: guild_id
49
+ type: string
50
+ label: "Guild ID"
51
+ required: false
52
+
53
+ capabilities:
54
+ - discord.send_message
55
+ - discord.receive_message
56
+
57
+ hooks:
58
+ post_install: scripts/post-install.sh
59
+ health_check: scripts/health.sh
60
+ ```
61
+
62
+ Only `id`, `name`, `version`, `description`, and `author` are required — everything else has sensible defaults.
63
+
64
+ ## Usage
65
+
66
+ ```typescript
67
+ import {
68
+ type IntegrationManifest,
69
+ parseManifestFile,
70
+ parseManifestString,
71
+ IntegrationManifestSchema,
72
+ buildConfigValidationSchema,
73
+ } from '@alfe.ai/integration-manifest';
74
+
75
+ // Parse and validate a manifest file
76
+ const manifest = parseManifestFile('./alfe-integration.yaml');
77
+
78
+ // Or parse a raw YAML string
79
+ const manifest = parseManifestString(yamlString);
80
+
81
+ // Build a Zod schema from a manifest's config_schema to validate user config
82
+ const configSchema = buildConfigValidationSchema(manifest.config_schema);
83
+ const result = configSchema.safeParse(userProvidedConfig);
84
+ ```
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ pnpm install
90
+ pnpm --filter @alfe.ai/integration-manifest build
91
+ pnpm --filter @alfe.ai/integration-manifest test
92
+ ```
93
+
94
+ ## Dependencies
95
+
96
+ - `yaml` — YAML parsing
97
+ - `zod` — Schema validation
@@ -0,0 +1,453 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * TypeScript types for the Alfe integration manifest (`alfe-integration.yaml`).
7
+ *
8
+ * These are the canonical types used by all packages that interact with
9
+ * integration manifests -- registry, lifecycle manager, CLI, etc.
10
+ */
11
+ type ConfigFieldType = 'secret' | 'string' | 'number' | 'boolean' | 'enum' | 'select' | 'multi_select' | 'oauth_connect' | 'phone_number_picker';
12
+ interface SelectOption {
13
+ value: string;
14
+ label: string;
15
+ }
16
+ interface ConfigSchemaField {
17
+ key: string;
18
+ type: ConfigFieldType;
19
+ label: string;
20
+ description?: string;
21
+ required: boolean;
22
+ default?: string | number | boolean;
23
+ /** Who can mutate this field at runtime. Default: 'admin' */
24
+ editable: 'admin' | 'agent';
25
+ /** Only used when type === 'enum' */
26
+ options?: string[];
27
+ /** Structured options for select/multi_select fields */
28
+ select_options?: SelectOption[];
29
+ /** OAuth provider identifier for oauth_connect fields */
30
+ oauth_provider?: string;
31
+ /** Only show this field if another field has a truthy value (string) or matches a specific value (object) */
32
+ depends_on_field?: string | {
33
+ key: string;
34
+ value: string | number | boolean;
35
+ };
36
+ /** Only show this field if a specific integration is installed */
37
+ depends_on_integration?: string;
38
+ }
39
+ interface SkillInstall {
40
+ /** Relative path within the integration repo to the skill directory */
41
+ path: string;
42
+ }
43
+ interface PluginInstall {
44
+ /** npm package name to install */
45
+ package: string;
46
+ }
47
+ type AgentRuntime = 'openclaw' | 'nanoclaw' | (string & {});
48
+ interface RuntimeInstall {
49
+ plugins?: PluginInstall[];
50
+ skills?: SkillInstall[];
51
+ /** Deep-merged into the runtime's agent config on activation */
52
+ config?: Record<string, unknown>;
53
+ }
54
+ interface InstallTargets {
55
+ /** Universal skills — applied to all runtimes */
56
+ skills?: SkillInstall[];
57
+ /** Universal plugins — applied to all runtimes */
58
+ plugins?: PluginInstall[];
59
+ /** Per-runtime installs */
60
+ runtimes?: Record<AgentRuntime, RuntimeInstall>;
61
+ }
62
+ interface IntegrationHooks {
63
+ pre_install?: string;
64
+ post_install?: string;
65
+ pre_uninstall?: string;
66
+ post_uninstall?: string;
67
+ health_check?: string;
68
+ }
69
+ interface IntegrationPricingPlan {
70
+ name: string;
71
+ price: number;
72
+ currency?: string;
73
+ interval?: 'month' | 'year';
74
+ }
75
+ interface IntegrationPricing {
76
+ type: 'free' | 'paid';
77
+ /** Single price (shorthand for integrations with one plan) */
78
+ price?: number;
79
+ currency?: string;
80
+ interval?: 'month' | 'year';
81
+ /** Multiple plans/tiers (e.g., starter, growth, scale) */
82
+ plans?: Record<string, IntegrationPricingPlan>;
83
+ }
84
+ interface IntegrationAuthor {
85
+ name: string;
86
+ url?: string;
87
+ }
88
+ interface IntegrationManifest {
89
+ id: string;
90
+ name: string;
91
+ version: string;
92
+ description: string;
93
+ /** Simple author string (legacy) or structured author object */
94
+ author: string | IntegrationAuthor;
95
+ license: string;
96
+ depends_on: string[];
97
+ min_gateway_version: string;
98
+ installs: InstallTargets;
99
+ config_schema: ConfigSchemaField[];
100
+ capabilities: string[];
101
+ hooks: IntegrationHooks;
102
+ /**
103
+ * Agent runtimes this integration supports.
104
+ * If omitted or empty, the integration is considered universal (all runtimes).
105
+ * Example: ['openclaw'] means this integration only works with OpenClaw.
106
+ */
107
+ supported_agents?: AgentRuntime[];
108
+ /** Marketplace metadata */
109
+ publisherId?: string;
110
+ icon?: string;
111
+ pricing?: IntegrationPricing;
112
+ preview_images?: string[];
113
+ /** Long-form features list for the detail view */
114
+ features?: string[];
115
+ }
116
+ interface PublishedVersion {
117
+ version: string;
118
+ commit_hash: string;
119
+ changelog?: string;
120
+ published_at: string;
121
+ }
122
+ interface PublishedIntegration {
123
+ manifest: IntegrationManifest;
124
+ versions: PublishedVersion[];
125
+ /** Resolved asset URLs baked at publish time */
126
+ resolved_assets: {
127
+ icon?: string;
128
+ preview_images?: string[];
129
+ };
130
+ }
131
+ type IntegrationStatus = 'installing' | 'installed' | 'configured' | 'active' | 'error';
132
+ interface IntegrationStateEntry {
133
+ status: IntegrationStatus;
134
+ version: string;
135
+ installedAt: string;
136
+ config: Record<string, unknown>;
137
+ error?: string;
138
+ }
139
+ interface IntegrationsStateFile {
140
+ version: number;
141
+ integrations: Record<string, IntegrationStateEntry>;
142
+ }
143
+ interface HealthCheckResult {
144
+ id: string;
145
+ name?: string;
146
+ healthy: boolean;
147
+ status: IntegrationStatus;
148
+ version?: string;
149
+ message?: string;
150
+ stdout?: string;
151
+ stderr?: string;
152
+ }
153
+ interface HealthReport {
154
+ overall: boolean;
155
+ results: HealthCheckResult[];
156
+ checkedAt: string;
157
+ }
158
+ //#endregion
159
+ //#region src/schema.d.ts
160
+ declare const ConfigFieldTypeSchema: z.ZodEnum<{
161
+ string: "string";
162
+ number: "number";
163
+ boolean: "boolean";
164
+ secret: "secret";
165
+ enum: "enum";
166
+ select: "select";
167
+ multi_select: "multi_select";
168
+ oauth_connect: "oauth_connect";
169
+ phone_number_picker: "phone_number_picker";
170
+ }>;
171
+ /** Structured option for select/multi_select fields */
172
+ declare const SelectOptionSchema: z.ZodObject<{
173
+ value: z.ZodString;
174
+ label: z.ZodString;
175
+ }, z.core.$strip>;
176
+ declare const ConfigSchemaFieldSchema: z.ZodObject<{
177
+ key: z.ZodString;
178
+ type: z.ZodEnum<{
179
+ string: "string";
180
+ number: "number";
181
+ boolean: "boolean";
182
+ secret: "secret";
183
+ enum: "enum";
184
+ select: "select";
185
+ multi_select: "multi_select";
186
+ oauth_connect: "oauth_connect";
187
+ phone_number_picker: "phone_number_picker";
188
+ }>;
189
+ label: z.ZodString;
190
+ description: z.ZodOptional<z.ZodString>;
191
+ required: z.ZodDefault<z.ZodBoolean>;
192
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
193
+ editable: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
194
+ admin: "admin";
195
+ agent: "agent";
196
+ }>>>;
197
+ options: z.ZodOptional<z.ZodArray<z.ZodString>>;
198
+ select_options: z.ZodOptional<z.ZodArray<z.ZodObject<{
199
+ value: z.ZodString;
200
+ label: z.ZodString;
201
+ }, z.core.$strip>>>;
202
+ oauth_provider: z.ZodOptional<z.ZodString>;
203
+ depends_on_field: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
204
+ key: z.ZodString;
205
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
206
+ }, z.core.$strip>]>>;
207
+ depends_on_integration: z.ZodOptional<z.ZodString>;
208
+ }, z.core.$strip>;
209
+ declare const SkillInstallSchema: z.ZodObject<{
210
+ path: z.ZodString;
211
+ }, z.core.$strip>;
212
+ declare const PluginInstallSchema: z.ZodObject<{
213
+ package: z.ZodString;
214
+ }, z.core.$strip>;
215
+ /** Per-runtime install overrides */
216
+ declare const RuntimeInstallSchema: z.ZodObject<{
217
+ skills: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
218
+ path: z.ZodString;
219
+ }, z.core.$strip>>>>;
220
+ plugins: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
221
+ package: z.ZodString;
222
+ }, z.core.$strip>>>>;
223
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
224
+ }, z.core.$strip>;
225
+ declare const InstallTargetsSchema: z.ZodObject<{
226
+ skills: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
227
+ path: z.ZodString;
228
+ }, z.core.$strip>>>>;
229
+ plugins: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
230
+ package: z.ZodString;
231
+ }, z.core.$strip>>>>;
232
+ runtimes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
233
+ skills: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
234
+ path: z.ZodString;
235
+ }, z.core.$strip>>>>;
236
+ plugins: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
237
+ package: z.ZodString;
238
+ }, z.core.$strip>>>>;
239
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
240
+ }, z.core.$strip>>>;
241
+ }, z.core.$strip>;
242
+ declare const IntegrationHooksSchema: z.ZodObject<{
243
+ pre_install: z.ZodOptional<z.ZodString>;
244
+ post_install: z.ZodOptional<z.ZodString>;
245
+ pre_uninstall: z.ZodOptional<z.ZodString>;
246
+ post_uninstall: z.ZodOptional<z.ZodString>;
247
+ health_check: z.ZodOptional<z.ZodString>;
248
+ }, z.core.$strip>;
249
+ declare const IntegrationManifestSchema: z.ZodObject<{
250
+ id: z.ZodString;
251
+ name: z.ZodString;
252
+ version: z.ZodString;
253
+ description: z.ZodString;
254
+ author: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
255
+ name: z.ZodString;
256
+ url: z.ZodOptional<z.ZodURL>;
257
+ }, z.core.$strip>]>;
258
+ license: z.ZodDefault<z.ZodString>;
259
+ depends_on: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
260
+ min_gateway_version: z.ZodDefault<z.ZodOptional<z.ZodString>>;
261
+ installs: z.ZodDefault<z.ZodOptional<z.ZodObject<{
262
+ skills: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
263
+ path: z.ZodString;
264
+ }, z.core.$strip>>>>;
265
+ plugins: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
266
+ package: z.ZodString;
267
+ }, z.core.$strip>>>>;
268
+ runtimes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
269
+ skills: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
270
+ path: z.ZodString;
271
+ }, z.core.$strip>>>>;
272
+ plugins: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
273
+ package: z.ZodString;
274
+ }, z.core.$strip>>>>;
275
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
276
+ }, z.core.$strip>>>;
277
+ }, z.core.$strip>>>;
278
+ config_schema: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
279
+ key: z.ZodString;
280
+ type: z.ZodEnum<{
281
+ string: "string";
282
+ number: "number";
283
+ boolean: "boolean";
284
+ secret: "secret";
285
+ enum: "enum";
286
+ select: "select";
287
+ multi_select: "multi_select";
288
+ oauth_connect: "oauth_connect";
289
+ phone_number_picker: "phone_number_picker";
290
+ }>;
291
+ label: z.ZodString;
292
+ description: z.ZodOptional<z.ZodString>;
293
+ required: z.ZodDefault<z.ZodBoolean>;
294
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
295
+ editable: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
296
+ admin: "admin";
297
+ agent: "agent";
298
+ }>>>;
299
+ options: z.ZodOptional<z.ZodArray<z.ZodString>>;
300
+ select_options: z.ZodOptional<z.ZodArray<z.ZodObject<{
301
+ value: z.ZodString;
302
+ label: z.ZodString;
303
+ }, z.core.$strip>>>;
304
+ oauth_provider: z.ZodOptional<z.ZodString>;
305
+ depends_on_field: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
306
+ key: z.ZodString;
307
+ value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
308
+ }, z.core.$strip>]>>;
309
+ depends_on_integration: z.ZodOptional<z.ZodString>;
310
+ }, z.core.$strip>>>>;
311
+ capabilities: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
312
+ hooks: z.ZodDefault<z.ZodOptional<z.ZodObject<{
313
+ pre_install: z.ZodOptional<z.ZodString>;
314
+ post_install: z.ZodOptional<z.ZodString>;
315
+ pre_uninstall: z.ZodOptional<z.ZodString>;
316
+ post_uninstall: z.ZodOptional<z.ZodString>;
317
+ health_check: z.ZodOptional<z.ZodString>;
318
+ }, z.core.$strip>>>;
319
+ supported_agents: z.ZodOptional<z.ZodArray<z.ZodString>>;
320
+ publisherId: z.ZodOptional<z.ZodString>;
321
+ icon: z.ZodOptional<z.ZodString>;
322
+ pricing: z.ZodOptional<z.ZodObject<{
323
+ type: z.ZodEnum<{
324
+ free: "free";
325
+ paid: "paid";
326
+ }>;
327
+ price: z.ZodOptional<z.ZodNumber>;
328
+ currency: z.ZodDefault<z.ZodOptional<z.ZodString>>;
329
+ interval: z.ZodOptional<z.ZodEnum<{
330
+ month: "month";
331
+ year: "year";
332
+ }>>;
333
+ plans: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
334
+ name: z.ZodString;
335
+ price: z.ZodNumber;
336
+ currency: z.ZodDefault<z.ZodOptional<z.ZodString>>;
337
+ interval: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
338
+ month: "month";
339
+ year: "year";
340
+ }>>>;
341
+ }, z.core.$strip>>>;
342
+ }, z.core.$strip>>;
343
+ preview_images: z.ZodOptional<z.ZodArray<z.ZodString>>;
344
+ features: z.ZodOptional<z.ZodArray<z.ZodString>>;
345
+ }, z.core.$strip>;
346
+ /**
347
+ * Validate user-provided config values against a manifest's config_schema.
348
+ * Returns a Zod schema dynamically built from the manifest's config_schema.
349
+ */
350
+ declare function buildConfigValidationSchema(configSchema: z.infer<typeof ConfigSchemaFieldSchema>[]): z.ZodObject<Record<string, z.ZodType>>;
351
+ type ManifestSchemaType = z.infer<typeof IntegrationManifestSchema>;
352
+ //#endregion
353
+ //#region src/parser.d.ts
354
+ declare class ManifestParseError extends Error {
355
+ readonly issues?: {
356
+ path: string;
357
+ message: string;
358
+ }[] | undefined;
359
+ constructor(message: string, issues?: {
360
+ path: string;
361
+ message: string;
362
+ }[] | undefined);
363
+ }
364
+ /**
365
+ * Parse a raw YAML string into a validated IntegrationManifest.
366
+ */
367
+ declare function parseManifestString(yaml: string): IntegrationManifest;
368
+ /**
369
+ * Read and parse an alfe-integration.yaml file from disk.
370
+ */
371
+ declare function parseManifestFile(filePath: string): IntegrationManifest;
372
+ //#endregion
373
+ //#region src/assets.d.ts
374
+ /**
375
+ * Asset URL resolution for integration manifests.
376
+ *
377
+ * At publish time, relative asset paths (icons, preview images) are resolved
378
+ * to absolute raw GitHub content URLs. Already-absolute URLs pass through unchanged.
379
+ */
380
+ /**
381
+ * Check whether a path is relative (not a fully qualified URL).
382
+ */
383
+ declare function isRelativePath(path: string): boolean;
384
+ /**
385
+ * Resolve a relative asset path to a raw GitHub content URL.
386
+ * Absolute URLs are returned unchanged.
387
+ *
388
+ * @param assetPath - The path from the manifest (e.g. "./assets/icon.png")
389
+ * @param repoUrl - GitHub repo URL (e.g. "https://github.com/org/repo")
390
+ * @param commitHash - The commit hash to pin the URL to
391
+ * @param subdir - Subdirectory within the repo where the manifest lives (e.g. "integrations/voice")
392
+ */
393
+ declare function resolveAssetUrl(assetPath: string, repoUrl: string, commitHash: string, subdir: string): string;
394
+ //#endregion
395
+ //#region src/interpolation.d.ts
396
+ /**
397
+ * Variable interpolation engine for integration configs.
398
+ *
399
+ * Supports templates like `{{integration.slack.oauth_token}}` that reference
400
+ * config values from other installed integrations.
401
+ *
402
+ * - Single-pass replacement (no recursive resolution — prevents circular refs)
403
+ * - Unresolved templates are left as-is (graceful degradation)
404
+ */
405
+ interface InterpolationContext {
406
+ /** Map of integrationId → config key-value pairs */
407
+ integrations: Partial<Record<string, Record<string, unknown>>>;
408
+ }
409
+ /**
410
+ * Interpolate template variables in a config object.
411
+ * Only string values are interpolated; other types pass through unchanged.
412
+ */
413
+ declare function interpolateConfig(config: Record<string, unknown>, ctx: InterpolationContext): Record<string, unknown>;
414
+ /**
415
+ * Extract all template references from a config object.
416
+ * Useful for dependency analysis and validation.
417
+ */
418
+ declare function extractTemplateReferences(config: Record<string, unknown>): {
419
+ integrationId: string;
420
+ field: string;
421
+ }[];
422
+ //#endregion
423
+ //#region src/migration.d.ts
424
+ interface ConfigSchemaDiff {
425
+ added: ConfigSchemaField[];
426
+ removed: ConfigSchemaField[];
427
+ changed: {
428
+ field: string;
429
+ oldSchema: ConfigSchemaField;
430
+ newSchema: ConfigSchemaField;
431
+ /** True when the type changed (existing value likely invalid) */
432
+ breaking: boolean;
433
+ }[];
434
+ unchanged: ConfigSchemaField[];
435
+ }
436
+ /**
437
+ * Diff two config schemas to determine what changed between versions.
438
+ */
439
+ declare function diffConfigSchemas(oldSchema: ConfigSchemaField[], newSchema: ConfigSchemaField[]): ConfigSchemaDiff;
440
+ /**
441
+ * Migrate existing config values forward based on a schema diff.
442
+ *
443
+ * - Carries forward values for unchanged and non-breaking changed fields
444
+ * - Applies defaults for new fields when available
445
+ * - Drops removed fields
446
+ * - Generates warnings for new required fields without defaults and breaking changes
447
+ */
448
+ declare function migrateConfig(currentConfig: Record<string, unknown>, diff: ConfigSchemaDiff): {
449
+ config: Record<string, unknown>;
450
+ warnings: string[];
451
+ };
452
+ //#endregion
453
+ export { type AgentRuntime, type ConfigFieldType, ConfigFieldTypeSchema, type ConfigSchemaDiff, type ConfigSchemaField, ConfigSchemaFieldSchema, type HealthCheckResult, type HealthReport, type InstallTargets, InstallTargetsSchema, type IntegrationHooks, IntegrationHooksSchema, type IntegrationManifest, IntegrationManifestSchema, type IntegrationPricing, type IntegrationPricingPlan, type IntegrationStateEntry, type IntegrationStatus, type IntegrationsStateFile, type InterpolationContext, ManifestParseError, type ManifestSchemaType, type PluginInstall, PluginInstallSchema, type PublishedIntegration, type PublishedVersion, type RuntimeInstall, RuntimeInstallSchema, type SelectOption, SelectOptionSchema, type SkillInstall, SkillInstallSchema, buildConfigValidationSchema, diffConfigSchemas, extractTemplateReferences, interpolateConfig, isRelativePath, migrateConfig, parseManifestFile, parseManifestString, resolveAssetUrl };
package/dist/index.js ADDED
@@ -0,0 +1,366 @@
1
+ import { z } from "zod";
2
+ import { readFileSync } from "node:fs";
3
+ import { parse } from "yaml";
4
+ //#region src/schema.ts
5
+ /**
6
+ * Zod validation schema for the Alfe integration manifest.
7
+ *
8
+ * Validates the parsed YAML structure against the expected format.
9
+ * Used by the parser and by the lifecycle manager to ensure manifest
10
+ * correctness before installation.
11
+ */
12
+ const ConfigFieldTypeSchema = z.enum([
13
+ "secret",
14
+ "string",
15
+ "number",
16
+ "boolean",
17
+ "enum",
18
+ "select",
19
+ "multi_select",
20
+ "oauth_connect",
21
+ "phone_number_picker"
22
+ ]);
23
+ /** Structured option for select/multi_select fields */
24
+ const SelectOptionSchema = z.object({
25
+ value: z.string().min(1),
26
+ label: z.string().min(1)
27
+ });
28
+ const ConfigSchemaFieldSchema = z.object({
29
+ key: z.string().min(1, "Config key must not be empty"),
30
+ type: ConfigFieldTypeSchema,
31
+ label: z.string().min(1, "Config label must not be empty"),
32
+ description: z.string().optional(),
33
+ required: z.boolean().default(false),
34
+ default: z.union([
35
+ z.string(),
36
+ z.number(),
37
+ z.boolean()
38
+ ]).optional(),
39
+ editable: z.enum(["admin", "agent"]).optional().default("admin"),
40
+ options: z.array(z.string()).optional(),
41
+ select_options: z.array(SelectOptionSchema).optional(),
42
+ oauth_provider: z.string().optional(),
43
+ depends_on_field: z.union([z.string(), z.object({
44
+ key: z.string().min(1),
45
+ value: z.union([
46
+ z.string(),
47
+ z.number(),
48
+ z.boolean()
49
+ ])
50
+ })]).optional(),
51
+ depends_on_integration: z.string().optional()
52
+ }).refine((field) => {
53
+ if (field.type === "enum") return field.options !== void 0 && field.options.length > 0;
54
+ return true;
55
+ }, { message: "Enum fields must have at least one option" }).refine((field) => {
56
+ if (field.type === "select" || field.type === "multi_select") return field.select_options !== void 0 && field.select_options.length > 0;
57
+ return true;
58
+ }, { message: "Select/multi_select fields must have at least one option in select_options" }).refine((field) => {
59
+ if (field.type === "oauth_connect") return field.oauth_provider !== void 0 && field.oauth_provider.length > 0;
60
+ return true;
61
+ }, { message: "oauth_connect fields must specify an oauth_provider" });
62
+ const SkillInstallSchema = z.object({ path: z.string().min(1, "Skill path must not be empty") });
63
+ const PluginInstallSchema = z.object({ package: z.string().min(1, "Plugin package name must not be empty") });
64
+ /** Per-runtime install overrides */
65
+ const RuntimeInstallSchema = z.object({
66
+ skills: z.array(SkillInstallSchema).optional().default([]),
67
+ plugins: z.array(PluginInstallSchema).optional().default([]),
68
+ config: z.record(z.string(), z.unknown()).optional()
69
+ });
70
+ /** Runtime key: lowercase alphanumeric + hyphens */
71
+ const RuntimeKeySchema = z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Runtime key must be lowercase alphanumeric with hyphens");
72
+ const InstallTargetsSchema = z.object({
73
+ skills: z.array(SkillInstallSchema).optional().default([]),
74
+ plugins: z.array(PluginInstallSchema).optional().default([]),
75
+ runtimes: z.record(RuntimeKeySchema, RuntimeInstallSchema).optional()
76
+ });
77
+ const IntegrationHooksSchema = z.object({
78
+ pre_install: z.string().optional(),
79
+ post_install: z.string().optional(),
80
+ pre_uninstall: z.string().optional(),
81
+ post_uninstall: z.string().optional(),
82
+ health_check: z.string().optional()
83
+ });
84
+ const IntegrationPricingPlanSchema = z.object({
85
+ name: z.string().min(1),
86
+ price: z.number().positive(),
87
+ currency: z.string().length(3).optional().default("USD"),
88
+ interval: z.enum(["month", "year"]).optional().default("month")
89
+ });
90
+ const IntegrationPricingSchema = z.object({
91
+ type: z.enum(["free", "paid"]),
92
+ price: z.number().positive().optional(),
93
+ currency: z.string().length(3).optional().default("USD"),
94
+ interval: z.enum(["month", "year"]).optional(),
95
+ plans: z.record(z.string(), IntegrationPricingPlanSchema).optional()
96
+ }).refine((pricing) => {
97
+ if (pricing.type === "paid") {
98
+ const hasSinglePrice = pricing.price !== void 0 && pricing.price > 0;
99
+ const hasPlans = pricing.plans !== void 0 && Object.keys(pricing.plans).length > 0;
100
+ return hasSinglePrice || hasPlans;
101
+ }
102
+ return true;
103
+ }, { message: "Paid integrations must have a positive price or at least one plan" });
104
+ const IntegrationAuthorSchema = z.union([z.string().min(1, "Author must not be empty"), z.object({
105
+ name: z.string().min(1, "Author name must not be empty"),
106
+ url: z.url().optional()
107
+ })]);
108
+ const SemverSchema = z.string().regex(/^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/, "Must be a valid semver version (e.g. 1.0.0)");
109
+ const IntegrationManifestSchema = z.object({
110
+ id: z.string().min(1, "Integration id must not be empty").regex(/^[a-z0-9][a-z0-9-]*$/, "id must be lowercase alphanumeric with hyphens"),
111
+ name: z.string().min(1, "Integration name must not be empty"),
112
+ version: SemverSchema,
113
+ description: z.string().min(1, "Description must not be empty"),
114
+ author: IntegrationAuthorSchema,
115
+ license: z.string().default("MIT"),
116
+ depends_on: z.array(z.string()).optional().default([]),
117
+ min_gateway_version: SemverSchema.optional().default("0.1.0"),
118
+ installs: InstallTargetsSchema.optional().default({
119
+ skills: [],
120
+ plugins: []
121
+ }),
122
+ config_schema: z.array(ConfigSchemaFieldSchema).optional().default([]),
123
+ capabilities: z.array(z.string()).optional().default([]),
124
+ hooks: IntegrationHooksSchema.optional().default({}),
125
+ supported_agents: z.array(RuntimeKeySchema).optional(),
126
+ publisherId: z.string().optional(),
127
+ icon: z.string().optional(),
128
+ pricing: IntegrationPricingSchema.optional(),
129
+ preview_images: z.array(z.string()).optional(),
130
+ features: z.array(z.string()).optional()
131
+ });
132
+ /**
133
+ * Validate user-provided config values against a manifest's config_schema.
134
+ * Returns a Zod schema dynamically built from the manifest's config_schema.
135
+ */
136
+ function buildConfigValidationSchema(configSchema) {
137
+ const shape = {};
138
+ for (const field of configSchema) {
139
+ let fieldSchema;
140
+ switch (field.type) {
141
+ case "secret":
142
+ case "string":
143
+ fieldSchema = z.string();
144
+ break;
145
+ case "enum":
146
+ fieldSchema = z.string();
147
+ if (field.options) fieldSchema = z.enum(field.options);
148
+ break;
149
+ case "select":
150
+ fieldSchema = z.string();
151
+ if (field.select_options) {
152
+ const values = field.select_options.map((o) => o.value);
153
+ fieldSchema = z.enum(values);
154
+ }
155
+ break;
156
+ case "multi_select":
157
+ if (field.select_options) {
158
+ const values = field.select_options.map((o) => o.value);
159
+ fieldSchema = z.array(z.enum(values));
160
+ } else fieldSchema = z.array(z.string());
161
+ break;
162
+ case "oauth_connect":
163
+ fieldSchema = z.string();
164
+ break;
165
+ case "phone_number_picker":
166
+ fieldSchema = z.string();
167
+ break;
168
+ case "number":
169
+ fieldSchema = z.number();
170
+ break;
171
+ case "boolean":
172
+ fieldSchema = z.boolean();
173
+ break;
174
+ default: fieldSchema = z.unknown();
175
+ }
176
+ if (!field.required) fieldSchema = fieldSchema.optional();
177
+ shape[field.key] = fieldSchema;
178
+ }
179
+ return z.object(shape);
180
+ }
181
+ //#endregion
182
+ //#region src/parser.ts
183
+ /**
184
+ * YAML parser for Alfe integration manifests.
185
+ *
186
+ * Reads an alfe-integration.yaml file or raw YAML string, parses it,
187
+ * and validates against the Zod schema. Returns a fully typed
188
+ * IntegrationManifest or throws a descriptive error.
189
+ */
190
+ var ManifestParseError = class extends Error {
191
+ constructor(message, issues) {
192
+ super(message);
193
+ this.issues = issues;
194
+ this.name = "ManifestParseError";
195
+ }
196
+ };
197
+ /**
198
+ * Parse a raw YAML string into a validated IntegrationManifest.
199
+ */
200
+ function parseManifestString(yaml) {
201
+ let raw;
202
+ try {
203
+ raw = parse(yaml);
204
+ } catch (err) {
205
+ throw new ManifestParseError(`Invalid YAML: ${err instanceof Error ? err.message : String(err)}`);
206
+ }
207
+ if (typeof raw !== "object" || raw === null) throw new ManifestParseError("Manifest must be a YAML object");
208
+ const result = IntegrationManifestSchema.safeParse(raw);
209
+ if (!result.success) {
210
+ const issues = result.error.issues.map((i) => ({
211
+ path: i.path.join("."),
212
+ message: i.message
213
+ }));
214
+ throw new ManifestParseError(`Invalid integration manifest:\n${issues.map((i) => ` ${i.path ? `${i.path}: ` : ""}${i.message}`).join("\n")}`, issues);
215
+ }
216
+ return result.data;
217
+ }
218
+ /**
219
+ * Read and parse an alfe-integration.yaml file from disk.
220
+ */
221
+ function parseManifestFile(filePath) {
222
+ let content;
223
+ try {
224
+ content = readFileSync(filePath, "utf-8");
225
+ } catch (err) {
226
+ throw new ManifestParseError(`Failed to read manifest file: ${err instanceof Error ? err.message : String(err)}`);
227
+ }
228
+ return parseManifestString(content);
229
+ }
230
+ //#endregion
231
+ //#region src/assets.ts
232
+ /**
233
+ * Asset URL resolution for integration manifests.
234
+ *
235
+ * At publish time, relative asset paths (icons, preview images) are resolved
236
+ * to absolute raw GitHub content URLs. Already-absolute URLs pass through unchanged.
237
+ */
238
+ /**
239
+ * Check whether a path is relative (not a fully qualified URL).
240
+ */
241
+ function isRelativePath(path) {
242
+ try {
243
+ new URL(path);
244
+ return false;
245
+ } catch {
246
+ return true;
247
+ }
248
+ }
249
+ /**
250
+ * Resolve a relative asset path to a raw GitHub content URL.
251
+ * Absolute URLs are returned unchanged.
252
+ *
253
+ * @param assetPath - The path from the manifest (e.g. "./assets/icon.png")
254
+ * @param repoUrl - GitHub repo URL (e.g. "https://github.com/org/repo")
255
+ * @param commitHash - The commit hash to pin the URL to
256
+ * @param subdir - Subdirectory within the repo where the manifest lives (e.g. "integrations/voice")
257
+ */
258
+ function resolveAssetUrl(assetPath, repoUrl, commitHash, subdir) {
259
+ if (!isRelativePath(assetPath)) return assetPath;
260
+ const repoMatch = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)/.exec(repoUrl);
261
+ if (!repoMatch) return assetPath;
262
+ const [, owner, repo] = repoMatch;
263
+ const normalizedPath = assetPath.replace(/^\.\//, "");
264
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${commitHash}/${subdir ? `${subdir.replace(/\/$/, "")}/${normalizedPath}` : normalizedPath}`;
265
+ }
266
+ //#endregion
267
+ //#region src/interpolation.ts
268
+ /**
269
+ * Variable interpolation engine for integration configs.
270
+ *
271
+ * Supports templates like `{{integration.slack.oauth_token}}` that reference
272
+ * config values from other installed integrations.
273
+ *
274
+ * - Single-pass replacement (no recursive resolution — prevents circular refs)
275
+ * - Unresolved templates are left as-is (graceful degradation)
276
+ */
277
+ const TEMPLATE_RE = /\{\{integration\.([a-z0-9-]+)\.([a-z0-9_]+)\}\}/g;
278
+ /**
279
+ * Interpolate template variables in a config object.
280
+ * Only string values are interpolated; other types pass through unchanged.
281
+ */
282
+ function interpolateConfig(config, ctx) {
283
+ const result = {};
284
+ for (const [key, value] of Object.entries(config)) if (typeof value === "string") result[key] = value.replace(TEMPLATE_RE, (_match, integrationId, field) => {
285
+ const integrationConfig = ctx.integrations[integrationId];
286
+ if (!integrationConfig) return _match;
287
+ const resolved = integrationConfig[field];
288
+ if (resolved == null) return _match;
289
+ return typeof resolved === "string" ? resolved : JSON.stringify(resolved);
290
+ });
291
+ else result[key] = value;
292
+ return result;
293
+ }
294
+ /**
295
+ * Extract all template references from a config object.
296
+ * Useful for dependency analysis and validation.
297
+ */
298
+ function extractTemplateReferences(config) {
299
+ const refs = [];
300
+ for (const value of Object.values(config)) {
301
+ if (typeof value !== "string") continue;
302
+ let match;
303
+ const re = new RegExp(TEMPLATE_RE.source, TEMPLATE_RE.flags);
304
+ while ((match = re.exec(value)) !== null) refs.push({
305
+ integrationId: match[1],
306
+ field: match[2]
307
+ });
308
+ }
309
+ return refs;
310
+ }
311
+ //#endregion
312
+ //#region src/migration.ts
313
+ /**
314
+ * Diff two config schemas to determine what changed between versions.
315
+ */
316
+ function diffConfigSchemas(oldSchema, newSchema) {
317
+ const oldMap = new Map(oldSchema.map((f) => [f.key, f]));
318
+ const newMap = new Map(newSchema.map((f) => [f.key, f]));
319
+ const added = [];
320
+ const removed = [];
321
+ const changed = [];
322
+ const unchanged = [];
323
+ for (const [key, newField] of newMap) {
324
+ const oldField = oldMap.get(key);
325
+ if (!oldField) added.push(newField);
326
+ else if (JSON.stringify(oldField) === JSON.stringify(newField)) unchanged.push(newField);
327
+ else changed.push({
328
+ field: key,
329
+ oldSchema: oldField,
330
+ newSchema: newField,
331
+ breaking: oldField.type !== newField.type
332
+ });
333
+ }
334
+ for (const [key, oldField] of oldMap) if (!newMap.has(key)) removed.push(oldField);
335
+ return {
336
+ added,
337
+ removed,
338
+ changed,
339
+ unchanged
340
+ };
341
+ }
342
+ /**
343
+ * Migrate existing config values forward based on a schema diff.
344
+ *
345
+ * - Carries forward values for unchanged and non-breaking changed fields
346
+ * - Applies defaults for new fields when available
347
+ * - Drops removed fields
348
+ * - Generates warnings for new required fields without defaults and breaking changes
349
+ */
350
+ function migrateConfig(currentConfig, diff) {
351
+ const config = {};
352
+ const warnings = [];
353
+ for (const field of diff.unchanged) if (field.key in currentConfig) config[field.key] = currentConfig[field.key];
354
+ for (const change of diff.changed) if (change.breaking) {
355
+ if (change.newSchema.default !== void 0) config[change.field] = change.newSchema.default;
356
+ warnings.push(`Field "${change.field}" type changed from "${change.oldSchema.type}" to "${change.newSchema.type}" — value reset`);
357
+ } else if (change.field in currentConfig) config[change.field] = currentConfig[change.field];
358
+ for (const field of diff.added) if (field.default !== void 0) config[field.key] = field.default;
359
+ else if (field.required) warnings.push(`New required field "${field.key}" has no default — user input needed`);
360
+ return {
361
+ config,
362
+ warnings
363
+ };
364
+ }
365
+ //#endregion
366
+ export { ConfigFieldTypeSchema, ConfigSchemaFieldSchema, InstallTargetsSchema, IntegrationHooksSchema, IntegrationManifestSchema, ManifestParseError, PluginInstallSchema, RuntimeInstallSchema, SelectOptionSchema, SkillInstallSchema, buildConfigValidationSchema, diffConfigSchemas, extractTemplateReferences, interpolateConfig, isRelativePath, migrateConfig, parseManifestFile, parseManifestString, resolveAssetUrl };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@alfe.ai/integration-manifest",
3
+ "version": "0.0.1",
4
+ "description": "Integration manifest schema, types, and parser for Alfe integration platform",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "yaml": "^2.7.0",
16
+ "zod": "^4.0.5"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "license": "UNLICENSED",
22
+ "scripts": {
23
+ "build": "tsdown",
24
+ "dev": "tsdown --watch",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:coverage": "vitest run --coverage",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "eslint ."
30
+ }
31
+ }