devchain-cli 0.3.1 → 0.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.
Files changed (158) hide show
  1. package/README.md +14 -0
  2. package/dist/server/app.module.js +2 -0
  3. package/dist/server/app.module.js.map +1 -1
  4. package/dist/server/common/constants/terminal.d.ts +3 -0
  5. package/dist/server/common/constants/terminal.js +7 -0
  6. package/dist/server/common/constants/terminal.js.map +1 -0
  7. package/dist/server/common/validation/template-validation.d.ts +8 -0
  8. package/dist/server/common/validation/template-validation.js +18 -0
  9. package/dist/server/common/validation/template-validation.js.map +1 -0
  10. package/dist/server/modules/chat/dtos/chat.dto.js +1 -1
  11. package/dist/server/modules/chat/dtos/chat.dto.js.map +1 -1
  12. package/dist/server/modules/epics/services/epics.service.d.ts +2 -0
  13. package/dist/server/modules/epics/services/epics.service.js +194 -32
  14. package/dist/server/modules/epics/services/epics.service.js.map +1 -1
  15. package/dist/server/modules/events/catalog/epic.assigned.d.ts +9 -13
  16. package/dist/server/modules/events/catalog/epic.assigned.js +5 -5
  17. package/dist/server/modules/events/catalog/epic.assigned.js.map +1 -1
  18. package/dist/server/modules/events/catalog/epic.created.d.ts +39 -0
  19. package/dist/server/modules/events/catalog/epic.created.js +20 -0
  20. package/dist/server/modules/events/catalog/epic.created.js.map +1 -0
  21. package/dist/server/modules/events/catalog/epic.updated.d.ts +178 -0
  22. package/dist/server/modules/events/catalog/epic.updated.js +43 -0
  23. package/dist/server/modules/events/catalog/epic.updated.js.map +1 -0
  24. package/dist/server/modules/events/catalog/index.d.ts +221 -12
  25. package/dist/server/modules/events/catalog/index.js +9 -1
  26. package/dist/server/modules/events/catalog/index.js.map +1 -1
  27. package/dist/server/modules/events/catalog/settings.terminal.changed.d.ts +12 -0
  28. package/dist/server/modules/events/catalog/settings.terminal.changed.js +11 -0
  29. package/dist/server/modules/events/catalog/settings.terminal.changed.js.map +1 -0
  30. package/dist/server/modules/events/services/events.service.js +0 -7
  31. package/dist/server/modules/events/services/events.service.js.map +1 -1
  32. package/dist/server/modules/events/subscribers/chat-message-delivery.subscriber.d.ts +5 -2
  33. package/dist/server/modules/events/subscribers/chat-message-delivery.subscriber.js +44 -24
  34. package/dist/server/modules/events/subscribers/chat-message-delivery.subscriber.js.map +1 -1
  35. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.d.ts +5 -8
  36. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.js +37 -56
  37. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.js.map +1 -1
  38. package/dist/server/modules/mcp/constants.d.ts +2 -0
  39. package/dist/server/modules/mcp/constants.js +6 -1
  40. package/dist/server/modules/mcp/constants.js.map +1 -1
  41. package/dist/server/modules/mcp/controllers/mcp-http.controller.js +2 -6
  42. package/dist/server/modules/mcp/controllers/mcp-http.controller.js.map +1 -1
  43. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +8 -6
  44. package/dist/server/modules/mcp/dtos/mcp.dto.js.map +1 -1
  45. package/dist/server/modules/mcp/mcp.module.js +2 -0
  46. package/dist/server/modules/mcp/mcp.module.js.map +1 -1
  47. package/dist/server/modules/mcp/services/mcp.service.d.ts +5 -1
  48. package/dist/server/modules/mcp/services/mcp.service.js +65 -33
  49. package/dist/server/modules/mcp/services/mcp.service.js.map +1 -1
  50. package/dist/server/modules/projects/controllers/projects.controller.d.ts +142 -6
  51. package/dist/server/modules/projects/controllers/projects.controller.js +91 -31
  52. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  53. package/dist/server/modules/projects/dtos/export.dto.d.ts +83 -0
  54. package/dist/server/modules/projects/dtos/export.dto.js +34 -0
  55. package/dist/server/modules/projects/dtos/export.dto.js.map +1 -0
  56. package/dist/server/modules/projects/projects.module.js +1 -0
  57. package/dist/server/modules/projects/projects.module.js.map +1 -1
  58. package/dist/server/modules/projects/services/projects.service.d.ts +39 -3
  59. package/dist/server/modules/projects/services/projects.service.js +358 -434
  60. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  61. package/dist/server/modules/registry/controllers/registry.controller.d.ts +98 -0
  62. package/dist/server/modules/registry/controllers/registry.controller.js +352 -0
  63. package/dist/server/modules/registry/controllers/registry.controller.js.map +1 -0
  64. package/dist/server/modules/registry/controllers/templates.controller.d.ts +35 -0
  65. package/dist/server/modules/registry/controllers/templates.controller.js +157 -0
  66. package/dist/server/modules/registry/controllers/templates.controller.js.map +1 -0
  67. package/dist/server/modules/registry/dtos/registry-error.d.ts +11 -0
  68. package/dist/server/modules/registry/dtos/registry-error.js +30 -0
  69. package/dist/server/modules/registry/dtos/registry-error.js.map +1 -0
  70. package/dist/server/modules/registry/dtos/registry.dto.d.ts +7 -0
  71. package/dist/server/modules/registry/dtos/registry.dto.js +50 -0
  72. package/dist/server/modules/registry/dtos/registry.dto.js.map +1 -0
  73. package/dist/server/modules/registry/index.d.ts +7 -0
  74. package/dist/server/modules/registry/index.js +24 -0
  75. package/dist/server/modules/registry/index.js.map +1 -0
  76. package/dist/server/modules/registry/interfaces/registry.interface.d.ts +68 -0
  77. package/dist/server/modules/registry/interfaces/registry.interface.js +3 -0
  78. package/dist/server/modules/registry/interfaces/registry.interface.js.map +1 -0
  79. package/dist/server/modules/registry/registry.module.d.ts +2 -0
  80. package/dist/server/modules/registry/registry.module.js +45 -0
  81. package/dist/server/modules/registry/registry.module.js.map +1 -0
  82. package/dist/server/modules/registry/services/registry-client.service.d.ts +14 -0
  83. package/dist/server/modules/registry/services/registry-client.service.js +226 -0
  84. package/dist/server/modules/registry/services/registry-client.service.js.map +1 -0
  85. package/dist/server/modules/registry/services/registry-orchestration.service.d.ts +45 -0
  86. package/dist/server/modules/registry/services/registry-orchestration.service.js +165 -0
  87. package/dist/server/modules/registry/services/registry-orchestration.service.js.map +1 -0
  88. package/dist/server/modules/registry/services/template-cache.service.d.ts +66 -0
  89. package/dist/server/modules/registry/services/template-cache.service.js +310 -0
  90. package/dist/server/modules/registry/services/template-cache.service.js.map +1 -0
  91. package/dist/server/modules/registry/services/template-upgrade.service.d.ts +41 -0
  92. package/dist/server/modules/registry/services/template-upgrade.service.js +212 -0
  93. package/dist/server/modules/registry/services/template-upgrade.service.js.map +1 -0
  94. package/dist/server/modules/registry/services/unified-template.service.d.ts +35 -0
  95. package/dist/server/modules/registry/services/unified-template.service.js +228 -0
  96. package/dist/server/modules/registry/services/unified-template.service.js.map +1 -0
  97. package/dist/server/modules/sessions/controllers/sessions.controller.d.ts +32 -1
  98. package/dist/server/modules/sessions/controllers/sessions.controller.js +150 -8
  99. package/dist/server/modules/sessions/controllers/sessions.controller.js.map +1 -1
  100. package/dist/server/modules/sessions/services/message-activity-stream.service.d.ts +12 -0
  101. package/dist/server/modules/sessions/services/message-activity-stream.service.js +51 -0
  102. package/dist/server/modules/sessions/services/message-activity-stream.service.js.map +1 -0
  103. package/dist/server/modules/sessions/services/sessions-message-pool.service.d.ts +125 -0
  104. package/dist/server/modules/sessions/services/sessions-message-pool.service.js +599 -0
  105. package/dist/server/modules/sessions/services/sessions-message-pool.service.js.map +1 -0
  106. package/dist/server/modules/sessions/services/sessions.service.d.ts +2 -9
  107. package/dist/server/modules/sessions/services/sessions.service.js +32 -27
  108. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  109. package/dist/server/modules/sessions/sessions.module.js +16 -2
  110. package/dist/server/modules/sessions/sessions.module.js.map +1 -1
  111. package/dist/server/modules/settings/dtos/settings.dto.d.ts +231 -0
  112. package/dist/server/modules/settings/dtos/settings.dto.js +40 -1
  113. package/dist/server/modules/settings/dtos/settings.dto.js.map +1 -1
  114. package/dist/server/modules/settings/services/settings.service.d.ts +56 -15
  115. package/dist/server/modules/settings/services/settings.service.js +295 -10
  116. package/dist/server/modules/settings/services/settings.service.js.map +1 -1
  117. package/dist/server/modules/subscribers/actions/action.interface.d.ts +2 -0
  118. package/dist/server/modules/subscribers/actions/send-message.action.js +34 -11
  119. package/dist/server/modules/subscribers/actions/send-message.action.js.map +1 -1
  120. package/dist/server/modules/subscribers/events/event-fields-catalog.js +109 -6
  121. package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
  122. package/dist/server/modules/subscribers/services/subscriber-executor.service.d.ts +3 -1
  123. package/dist/server/modules/subscribers/services/subscriber-executor.service.js +13 -1
  124. package/dist/server/modules/subscribers/services/subscriber-executor.service.js.map +1 -1
  125. package/dist/server/modules/terminal/dtos/ws-envelope.dto.d.ts +20 -0
  126. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js +8 -1
  127. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js.map +1 -1
  128. package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +1 -3
  129. package/dist/server/modules/terminal/gateways/terminal.gateway.js +50 -11
  130. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  131. package/dist/server/modules/terminal/services/pty.service.d.ts +1 -3
  132. package/dist/server/modules/terminal/services/pty.service.js +2 -14
  133. package/dist/server/modules/terminal/services/pty.service.js.map +1 -1
  134. package/dist/server/modules/terminal/services/terminal-seed.service.d.ts +6 -4
  135. package/dist/server/modules/terminal/services/terminal-seed.service.js +74 -67
  136. package/dist/server/modules/terminal/services/terminal-seed.service.js.map +1 -1
  137. package/dist/server/modules/terminal/services/tmux.service.js +3 -2
  138. package/dist/server/modules/terminal/services/tmux.service.js.map +1 -1
  139. package/dist/server/modules/terminal/terminal.module.js +0 -3
  140. package/dist/server/modules/terminal/terminal.module.js.map +1 -1
  141. package/dist/server/templates/claude-codex-advanced.json +78 -17
  142. package/dist/server/templates/claude-opus.json +251 -37
  143. package/dist/server/templates/simple-codex.json +141 -38
  144. package/dist/server/test-setup.js +19 -0
  145. package/dist/server/test-setup.js.map +1 -1
  146. package/dist/server/tsconfig.tsbuildinfo +1 -1
  147. package/dist/server/ui/assets/index-4Q4XuabC.js +733 -0
  148. package/dist/server/ui/assets/index-BkGGbapJ.css +32 -0
  149. package/dist/server/ui/index.html +2 -2
  150. package/dist/templates/claude-codex-advanced.json +78 -17
  151. package/dist/templates/claude-opus.json +251 -37
  152. package/dist/templates/simple-codex.json +141 -38
  153. package/package.json +15 -2
  154. package/dist/server/modules/terminal/services/terminal-emulator.service.d.ts +0 -40
  155. package/dist/server/modules/terminal/services/terminal-emulator.service.js +0 -356
  156. package/dist/server/modules/terminal/services/terminal-emulator.service.js.map +0 -1
  157. package/dist/server/ui/assets/index-C9GXCjnF.js +0 -700
  158. package/dist/server/ui/assets/index-o0FbZg-1.css +0 -32
@@ -24,124 +24,17 @@ const error_types_1 = require("../../../common/errors/error-types");
24
24
  const path_1 = require("path");
25
25
  const fs_1 = require("fs");
26
26
  const env_config_1 = require("../../../common/config/env.config");
27
- const zod_1 = require("zod");
27
+ const shared_1 = require("@devchain/shared");
28
+ const unified_template_service_1 = require("../../registry/services/unified-template.service");
28
29
  const logger = (0, logger_1.createLogger)('ProjectsService');
29
- const ExportSchema = zod_1.z
30
- .object({
31
- version: zod_1.z.number().optional().default(1),
32
- exportedAt: zod_1.z.string().optional(),
33
- prompts: zod_1.z
34
- .array(zod_1.z.object({
35
- id: zod_1.z.string().uuid().optional(),
36
- title: zod_1.z.string().min(1),
37
- content: zod_1.z.string().default(''),
38
- version: zod_1.z.number().optional(),
39
- tags: zod_1.z.array(zod_1.z.string()).optional().default([]),
40
- }))
41
- .optional()
42
- .default([]),
43
- profiles: zod_1.z
44
- .array(zod_1.z.object({
45
- id: zod_1.z.string().uuid().optional(),
46
- name: zod_1.z.string().min(1),
47
- provider: zod_1.z.object({ id: zod_1.z.string().optional(), name: zod_1.z.string().min(1) }),
48
- options: zod_1.z.unknown().nullable().optional(),
49
- instructions: zod_1.z.string().nullable().optional(),
50
- temperature: zod_1.z.number().nullable().optional(),
51
- maxTokens: zod_1.z.number().int().nullable().optional(),
52
- }))
53
- .optional()
54
- .default([]),
55
- agents: zod_1.z
56
- .array(zod_1.z.object({
57
- id: zod_1.z.string().uuid().optional(),
58
- name: zod_1.z.string().min(1),
59
- profileId: zod_1.z.string().uuid().optional(),
60
- description: zod_1.z.string().nullable().optional(),
61
- }))
62
- .optional()
63
- .default([]),
64
- statuses: zod_1.z
65
- .array(zod_1.z.object({
66
- id: zod_1.z.string().uuid().optional(),
67
- label: zod_1.z.string().min(1),
68
- color: zod_1.z.string().min(1),
69
- position: zod_1.z.number().int(),
70
- mcpHidden: zod_1.z.boolean().optional().default(false),
71
- }))
72
- .optional()
73
- .default([]),
74
- initialPrompt: zod_1.z
75
- .object({ promptId: zod_1.z.string().uuid().optional(), title: zod_1.z.string().optional() })
76
- .nullable()
77
- .optional(),
78
- projectSettings: zod_1.z
79
- .object({
80
- initialPromptTitle: zod_1.z.string().optional(),
81
- autoCleanStatusLabels: zod_1.z.array(zod_1.z.string()).optional(),
82
- epicAssignedTemplate: zod_1.z.string().optional(),
83
- })
84
- .optional(),
85
- watchers: zod_1.z
86
- .array(zod_1.z.object({
87
- id: zod_1.z.string().uuid().optional(),
88
- name: zod_1.z.string().min(1),
89
- description: zod_1.z.string().nullable().optional(),
90
- enabled: zod_1.z.boolean(),
91
- scope: zod_1.z.enum(['all', 'agent', 'profile', 'provider']),
92
- scopeFilterName: zod_1.z.string().nullable().optional(),
93
- pollIntervalMs: zod_1.z.number().int(),
94
- viewportLines: zod_1.z.number().int(),
95
- condition: zod_1.z.object({
96
- type: zod_1.z.enum(['contains', 'regex', 'not_contains']),
97
- pattern: zod_1.z.string(),
98
- flags: zod_1.z.string().optional(),
99
- }),
100
- cooldownMs: zod_1.z.number().int(),
101
- cooldownMode: zod_1.z.enum(['time', 'until_clear']),
102
- eventName: zod_1.z.string(),
103
- }))
104
- .optional()
105
- .default([]),
106
- subscribers: zod_1.z
107
- .array(zod_1.z.object({
108
- id: zod_1.z.string().uuid().optional(),
109
- name: zod_1.z.string().min(1),
110
- description: zod_1.z.string().nullable().optional(),
111
- enabled: zod_1.z.boolean(),
112
- eventName: zod_1.z.string(),
113
- eventFilter: zod_1.z
114
- .object({
115
- field: zod_1.z.string(),
116
- operator: zod_1.z.enum(['equals', 'contains', 'regex']),
117
- value: zod_1.z.string(),
118
- })
119
- .nullable()
120
- .optional(),
121
- actionType: zod_1.z.string(),
122
- actionInputs: zod_1.z.record(zod_1.z.string(), zod_1.z.object({
123
- source: zod_1.z.enum(['event_field', 'custom']),
124
- eventField: zod_1.z.string().optional(),
125
- customValue: zod_1.z.string().optional(),
126
- })),
127
- delayMs: zod_1.z.number().int(),
128
- cooldownMs: zod_1.z.number().int(),
129
- retryOnError: zod_1.z.boolean(),
130
- groupName: zod_1.z.string().nullable().optional(),
131
- position: zod_1.z.number().int().optional().default(0),
132
- priority: zod_1.z.number().int().optional().default(0),
133
- }))
134
- .optional()
135
- .default([]),
136
- })
137
- .strict();
138
30
  let ProjectsService = class ProjectsService {
139
- constructor(storage, sessions, settings, watchersService, watcherRunner) {
31
+ constructor(storage, sessions, settings, watchersService, watcherRunner, unifiedTemplateService) {
140
32
  this.storage = storage;
141
33
  this.sessions = sessions;
142
34
  this.settings = settings;
143
35
  this.watchersService = watchersService;
144
36
  this.watcherRunner = watcherRunner;
37
+ this.unifiedTemplateService = unifiedTemplateService;
145
38
  }
146
39
  findTemplatesDirectory() {
147
40
  const env = (0, env_config_1.getEnvConfig)();
@@ -169,8 +62,7 @@ let ProjectsService = class ProjectsService {
169
62
  const templatesDir = this.findTemplatesDirectory();
170
63
  if (!templatesDir) {
171
64
  logger.error('Templates directory not found');
172
- throw new common_1.InternalServerErrorException({
173
- message: 'Templates directory not found',
65
+ throw new error_types_1.StorageError('Templates directory not found', {
174
66
  hint: 'Templates directory is not available in this deployment',
175
67
  });
176
68
  }
@@ -186,8 +78,7 @@ let ProjectsService = class ProjectsService {
186
78
  }
187
79
  catch (error) {
188
80
  logger.error({ error, templatesDir }, 'Failed to read templates directory');
189
- throw new common_1.InternalServerErrorException({
190
- message: 'Failed to read templates directory',
81
+ throw new error_types_1.StorageError('Failed to read templates directory', {
191
82
  hint: 'Error accessing templates',
192
83
  });
193
84
  }
@@ -196,8 +87,7 @@ let ProjectsService = class ProjectsService {
196
87
  logger.info({ templateId }, 'getTemplateContent');
197
88
  const templatesDir = this.findTemplatesDirectory();
198
89
  if (!templatesDir) {
199
- throw new common_1.InternalServerErrorException({
200
- message: 'Templates directory not found',
90
+ throw new error_types_1.StorageError('Templates directory not found', {
201
91
  hint: 'Templates directory is not available in this deployment',
202
92
  });
203
93
  }
@@ -220,68 +110,28 @@ let ProjectsService = class ProjectsService {
220
110
  }
221
111
  catch (error) {
222
112
  logger.error({ error, templatePath }, 'Failed to read template file');
223
- throw new common_1.InternalServerErrorException({
224
- message: 'Failed to read template file',
113
+ throw new error_types_1.StorageError('Failed to read template file', {
225
114
  hint: 'Template file exists but cannot be read or parsed',
226
115
  });
227
116
  }
228
117
  }
229
118
  async createFromTemplate(input) {
230
119
  logger.info({ input }, 'createFromTemplate');
231
- const templatesDir = this.findTemplatesDirectory();
232
- if (!templatesDir) {
233
- throw new common_1.InternalServerErrorException({
234
- message: 'Templates directory not found',
235
- hint: 'Templates directory is not available in this deployment',
236
- });
237
- }
238
- const TEMPLATE_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
239
- if (!TEMPLATE_ID_REGEX.test(input.templateId)) {
240
- throw new error_types_1.ValidationError('Invalid template ID: must contain only alphanumeric characters, hyphens, and underscores', { templateId: input.templateId });
241
- }
242
- const resolvedTemplatesDir = (0, path_1.resolve)(templatesDir);
243
- const templatePath = (0, path_1.resolve)(templatesDir, `${input.templateId}.json`);
244
- if (!templatePath.startsWith(resolvedTemplatesDir + path_1.sep)) {
245
- logger.warn({ templateId: input.templateId, templatePath, templatesDir: resolvedTemplatesDir }, 'Path traversal attempt detected');
246
- throw new error_types_1.ValidationError('Invalid template ID: path traversal not allowed', {
247
- templateId: input.templateId,
248
- });
249
- }
250
- if (!(0, fs_1.existsSync)(templatePath)) {
251
- throw new error_types_1.NotFoundError('Template', input.templateId);
252
- }
253
- let templateContent;
254
- try {
255
- templateContent = (0, fs_1.readFileSync)(templatePath, 'utf-8');
256
- }
257
- catch (error) {
258
- logger.error({ error, templatePath }, 'Failed to read template file');
259
- throw new common_1.InternalServerErrorException({
260
- message: 'Failed to read template file',
261
- hint: 'Template file exists but cannot be read',
262
- });
263
- }
120
+ const templateResult = await this.unifiedTemplateService.getTemplate(input.slug, input.version ?? undefined);
264
121
  let payload;
265
122
  try {
266
- payload = ExportSchema.parse(JSON.parse(templateContent));
123
+ payload = shared_1.ExportSchema.parse(templateResult.content);
267
124
  }
268
125
  catch (error) {
269
- logger.error({ error, templateId: input.templateId }, 'Invalid template format');
270
- throw new common_1.BadRequestException({
271
- message: 'Invalid template format',
126
+ logger.error({ error, slug: input.slug, version: input.version }, 'Invalid template format');
127
+ throw new error_types_1.ValidationError('Invalid template format', {
272
128
  hint: 'Template file does not match expected export schema',
273
129
  });
274
130
  }
275
131
  const providerNames = new Set((payload.profiles ?? []).map((p) => p.provider.name.trim().toLowerCase()));
276
- const providers = await this.storage.listProviders();
277
- const available = new Map();
278
- for (const prov of providers.items) {
279
- available.set(prov.name.trim().toLowerCase(), prov.id);
280
- }
281
- const missingProviders = Array.from(providerNames).filter((n) => !available.has(n));
132
+ const { available, missing: missingProviders } = await this.resolveProviders(providerNames);
282
133
  if (missingProviders.length > 0) {
283
- throw new common_1.BadRequestException({
284
- message: 'Import aborted: missing providers',
134
+ throw new error_types_1.ValidationError('Import aborted: missing providers', {
285
135
  missingProviders,
286
136
  hint: 'Install/configure providers by name before creating project from template.',
287
137
  });
@@ -297,25 +147,13 @@ let ProjectsService = class ProjectsService {
297
147
  profiles: payload.profiles.map((prof) => {
298
148
  const providerId = available.get(prof.provider.name.trim().toLowerCase());
299
149
  if (!providerId) {
300
- throw new common_1.BadRequestException({ message: `Provider not found: ${prof.provider.name}` });
301
- }
302
- let optionsStr = null;
303
- if (typeof prof.options === 'string') {
304
- optionsStr = prof.options;
305
- }
306
- else if (prof.options && typeof prof.options === 'object') {
307
- try {
308
- optionsStr = JSON.stringify(prof.options);
309
- }
310
- catch {
311
- optionsStr = null;
312
- }
150
+ throw new error_types_1.NotFoundError('Provider', prof.provider.name);
313
151
  }
314
152
  return {
315
153
  id: prof.id,
316
154
  name: prof.name,
317
155
  providerId,
318
- options: optionsStr,
156
+ options: this.normalizeProfileOptions(prof.options),
319
157
  instructions: prof.instructions ?? null,
320
158
  temperature: prof.temperature ?? null,
321
159
  maxTokens: prof.maxTokens ?? null,
@@ -342,115 +180,81 @@ let ProjectsService = class ProjectsService {
342
180
  rootPath: input.rootPath,
343
181
  isTemplate: false,
344
182
  }, templatePayload);
345
- let initialPromptSet = false;
346
- if (payload.initialPrompt) {
347
- let targetPromptId = null;
348
- if (payload.initialPrompt.title) {
349
- const oldId = Object.keys(result.mappings.promptIdMap).find((oldId) => payload.prompts.some((p) => p.id === oldId &&
350
- p.title.trim().toLowerCase() === payload.initialPrompt.title.trim().toLowerCase()));
351
- if (oldId) {
352
- targetPromptId = result.mappings.promptIdMap[oldId];
353
- }
354
- }
355
- if (!targetPromptId && payload.initialPrompt.promptId) {
356
- const mapped = result.mappings.promptIdMap[payload.initialPrompt.promptId];
357
- if (mapped)
358
- targetPromptId = mapped;
359
- }
360
- await this.settings.updateSettings({
361
- projectId: result.project.id,
362
- initialSessionPromptId: targetPromptId ?? null,
363
- });
364
- initialPromptSet = Boolean(targetPromptId);
365
- }
366
- if (payload.projectSettings) {
367
- const ps = payload.projectSettings;
368
- if (ps.initialPromptTitle) {
369
- const promptByTitle = payload.prompts.find((p) => p.title.toLowerCase() === ps.initialPromptTitle.toLowerCase());
370
- if (promptByTitle?.id) {
371
- const mappedPromptId = result.mappings.promptIdMap[promptByTitle.id];
372
- if (mappedPromptId) {
373
- await this.settings.updateSettings({
374
- projectId: result.project.id,
375
- initialSessionPromptId: mappedPromptId,
376
- });
377
- initialPromptSet = true;
378
- logger.info({ projectId: result.project.id, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
379
- }
380
- }
381
- }
382
- if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
383
- const statusLabelMap = new Map(templatePayload.statuses.map((s) => [s.label.toLowerCase(), s.id]));
384
- const autoCleanStatusIds = ps.autoCleanStatusLabels
385
- .map((label) => {
386
- const templateId = statusLabelMap.get(label.toLowerCase());
387
- return templateId ? result.mappings.statusIdMap[templateId] : undefined;
388
- })
389
- .filter((id) => !!id);
390
- if (autoCleanStatusIds.length > 0) {
391
- const currentSettings = this.settings.getSettings();
392
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
393
- await this.settings.updateSettings({
394
- autoClean: {
395
- statusIds: { ...existingAutoClean, [result.project.id]: autoCleanStatusIds },
396
- },
397
- });
398
- logger.info({ projectId: result.project.id, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
399
- }
400
- }
401
- else {
402
- const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
403
- if (archiveTemplateStatus?.id) {
404
- const archiveNewId = result.mappings.statusIdMap[archiveTemplateStatus.id];
405
- if (archiveNewId) {
406
- const currentSettings = this.settings.getSettings();
407
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
408
- await this.settings.updateSettings({
409
- autoClean: {
410
- statusIds: { ...existingAutoClean, [result.project.id]: [archiveNewId] },
411
- },
412
- });
413
- logger.info({ projectId: result.project.id, archiveStatusId: archiveNewId }, 'Auto-configured Archive status for auto-clean (fallback)');
414
- }
415
- }
416
- }
417
- if (ps.epicAssignedTemplate) {
418
- await this.settings.updateSettings({
419
- events: {
420
- epicAssigned: { template: ps.epicAssignedTemplate },
421
- },
422
- });
423
- logger.info({ projectId: result.project.id }, 'Applied epicAssigned template from projectSettings');
424
- }
425
- }
426
- else {
427
- const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
428
- if (archiveTemplateStatus?.id) {
429
- const archiveNewId = result.mappings.statusIdMap[archiveTemplateStatus.id];
430
- if (archiveNewId) {
431
- const currentSettings = this.settings.getSettings();
432
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
433
- await this.settings.updateSettings({
434
- autoClean: {
435
- statusIds: { ...existingAutoClean, [result.project.id]: [archiveNewId] },
436
- },
437
- });
438
- logger.info({ projectId: result.project.id, archiveStatusId: archiveNewId }, 'Auto-configured Archive status for auto-clean');
439
- }
440
- }
441
- }
183
+ const { agentNameToId: agentNameToNewId, profileNameToId: profileNameToNewId } = this.buildNameToIdMaps(templatePayload, result.mappings);
184
+ let mergedInitialPromptTitle;
185
+ if (payload.initialPrompt?.title) {
186
+ mergedInitialPromptTitle = payload.initialPrompt.title;
187
+ }
188
+ else if (payload.initialPrompt?.promptId) {
189
+ const matchingPrompt = payload.prompts.find((p) => p.id === payload.initialPrompt.promptId);
190
+ if (matchingPrompt) {
191
+ mergedInitialPromptTitle = matchingPrompt.title;
192
+ }
193
+ }
194
+ else if (payload.projectSettings?.initialPromptTitle) {
195
+ mergedInitialPromptTitle = payload.projectSettings.initialPromptTitle;
196
+ }
197
+ const mergedSettings = payload.projectSettings
198
+ ? { ...payload.projectSettings, initialPromptTitle: mergedInitialPromptTitle }
199
+ : mergedInitialPromptTitle
200
+ ? { initialPromptTitle: mergedInitialPromptTitle }
201
+ : undefined;
202
+ const promptTitleToId = new Map();
203
+ for (const p of payload.prompts) {
204
+ if (p.id && result.mappings.promptIdMap[p.id]) {
205
+ promptTitleToId.set(p.title.toLowerCase(), result.mappings.promptIdMap[p.id]);
206
+ }
207
+ }
208
+ const statusLabelToId = new Map();
209
+ for (const s of templatePayload.statuses) {
210
+ if (s.id && result.mappings.statusIdMap[s.id]) {
211
+ statusLabelToId.set(s.label.toLowerCase(), result.mappings.statusIdMap[s.id]);
212
+ }
213
+ }
214
+ const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
215
+ const archiveStatusId = archiveTemplateStatus?.id
216
+ ? (result.mappings.statusIdMap[archiveTemplateStatus.id] ?? null)
217
+ : null;
218
+ const settingsResult = await this.applyProjectSettings(result.project.id, mergedSettings, { promptTitleToId, statusLabelToId }, archiveStatusId);
219
+ const initialPromptSet = settingsResult.initialPromptSet;
220
+ const { created: watchersCreated } = await this.createWatchersFromPayload(result.project.id, payload.watchers, {
221
+ agentNameToId: agentNameToNewId,
222
+ profileNameToId: profileNameToNewId,
223
+ providerNameToId: available,
224
+ });
225
+ const { created: subscribersCreated } = await this.createSubscribersFromPayload(result.project.id, payload.subscribers);
226
+ const registryConfig = this.settings.getRegistryConfig();
227
+ await this.settings.setProjectTemplateMetadata(result.project.id, {
228
+ templateSlug: input.slug,
229
+ source: templateResult.source,
230
+ installedVersion: templateResult.version,
231
+ registryUrl: templateResult.source === 'registry' ? registryConfig.url : null,
232
+ installedAt: new Date().toISOString(),
233
+ });
234
+ logger.info({
235
+ projectId: result.project.id,
236
+ slug: input.slug,
237
+ source: templateResult.source,
238
+ version: templateResult.version,
239
+ }, 'Template metadata set for project');
442
240
  return {
443
241
  success: true,
444
242
  project: result.project,
445
- imported: result.imported,
243
+ imported: {
244
+ ...result.imported,
245
+ watchers: watchersCreated,
246
+ subscribers: subscribersCreated,
247
+ },
446
248
  mappings: result.mappings,
447
249
  initialPromptSet,
448
250
  message: 'Project created from template successfully.',
449
251
  };
450
252
  }
451
- async exportProject(projectId) {
253
+ async exportProject(projectId, opts) {
452
254
  logger.info({ projectId }, 'exportProject');
453
- const [promptsRes, profilesRes, agentsRes, statusesRes, initialPrompt, settings, watchersRes, subscribersRes,] = await Promise.all([
255
+ const { manifestOverrides } = opts ?? {};
256
+ const [project, promptsRes, profilesRes, agentsRes, statusesRes, initialPrompt, settings, watchersRes, subscribersRes,] = await Promise.all([
257
+ this.storage.getProject(projectId),
454
258
  this.storage.listPrompts({ projectId, limit: 1000, offset: 0 }),
455
259
  this.storage.listAgentProfiles({ projectId, limit: 1000, offset: 0 }),
456
260
  this.storage.listAgents(projectId, { limit: 1000, offset: 0 }),
@@ -557,6 +361,10 @@ let ProjectsService = class ProjectsService {
557
361
  if (epicAssignedTemplate) {
558
362
  projectSettings.epicAssignedTemplate = epicAssignedTemplate;
559
363
  }
364
+ const poolSettings = settings.messagePool?.projects?.[projectId];
365
+ if (poolSettings && Object.keys(poolSettings).length > 0) {
366
+ projectSettings.messagePoolSettings = poolSettings;
367
+ }
560
368
  const watchers = await Promise.all(watchersRes.map(async (w) => {
561
369
  let scopeFilterName = null;
562
370
  if (w.scopeFilterId) {
@@ -614,7 +422,17 @@ let ProjectsService = class ProjectsService {
614
422
  position: s.position,
615
423
  priority: s.priority,
616
424
  }));
425
+ const existingMetadata = this.settings.getProjectTemplateMetadata(projectId);
426
+ const manifest = {
427
+ slug: existingMetadata?.templateSlug || this.slugify(project.name),
428
+ name: project.name,
429
+ description: project.description || null,
430
+ version: existingMetadata?.installedVersion || '1.0.0',
431
+ ...manifestOverrides,
432
+ publishedAt: new Date().toISOString(),
433
+ };
617
434
  const exportPayload = {
435
+ _manifest: manifest,
618
436
  version: 1,
619
437
  exportedAt: new Date().toISOString(),
620
438
  prompts,
@@ -633,14 +451,9 @@ let ProjectsService = class ProjectsService {
633
451
  async importProject(input) {
634
452
  logger.info({ projectId: input.projectId, dryRun: input.dryRun }, 'importProject');
635
453
  const isDryRun = input.dryRun ?? false;
636
- const payload = ExportSchema.parse(input.payload ?? {});
454
+ const payload = shared_1.ExportSchema.parse(input.payload ?? {});
637
455
  const providerNames = new Set((payload.profiles ?? []).map((p) => p.provider.name.trim().toLowerCase()));
638
- const providers = await this.storage.listProviders();
639
- const available = new Map();
640
- for (const prov of providers.items) {
641
- available.set(prov.name.trim().toLowerCase(), prov.id);
642
- }
643
- const missingProviders = Array.from(providerNames).filter((n) => !available.has(n));
456
+ const { available, missing: missingProviders } = await this.resolveProviders(providerNames);
644
457
  const [existingPrompts, existingProfiles, existingAgents, existingStatuses, existingWatchers, existingSubscribers,] = await Promise.all([
645
458
  this.storage.listPrompts({ projectId: input.projectId, limit: 10000, offset: 0 }),
646
459
  this.storage.listAgentProfiles({ projectId: input.projectId, limit: 10000, offset: 0 }),
@@ -695,16 +508,14 @@ let ProjectsService = class ProjectsService {
695
508
  };
696
509
  }
697
510
  if (missingProviders.length > 0) {
698
- throw new common_1.BadRequestException({
699
- message: 'Import aborted: missing providers',
511
+ throw new error_types_1.ValidationError('Import aborted: missing providers', {
700
512
  missingProviders,
701
513
  hint: 'Install/configure providers by name before importing profiles.',
702
514
  });
703
515
  }
704
516
  const activeSessions = this.sessions.getActiveSessionsForProject(input.projectId);
705
517
  if (activeSessions.length > 0) {
706
- throw new common_1.BadRequestException({
707
- message: 'Import aborted: active agent sessions detected',
518
+ throw new error_types_1.ConflictError('Import aborted: active agent sessions detected', {
708
519
  activeSessions: activeSessions.map((s) => ({ id: s.id, agentId: s.agentId })),
709
520
  hint: 'Terminate all running sessions for this project before importing.',
710
521
  });
@@ -804,45 +615,29 @@ let ProjectsService = class ProjectsService {
804
615
  promptIdMap[p.id] = created.id;
805
616
  createdPrompts.push({ id: created.id, title: created.title });
806
617
  }
807
- const profileNameToNewId = new Map();
808
618
  for (const prof of payload.profiles) {
809
619
  const providerId = available.get(prof.provider.name.trim().toLowerCase());
810
620
  if (!providerId) {
811
- throw new common_1.BadRequestException({ message: `Provider not found: ${prof.provider.name}` });
812
- }
813
- let optionsStr = null;
814
- if (typeof prof.options === 'string') {
815
- optionsStr = prof.options;
816
- }
817
- else if (prof.options && typeof prof.options === 'object') {
818
- try {
819
- optionsStr = JSON.stringify(prof.options);
820
- }
821
- catch {
822
- optionsStr = null;
823
- }
621
+ throw new error_types_1.NotFoundError('Provider', prof.provider.name);
824
622
  }
825
623
  const created = await this.storage.createAgentProfile({
826
624
  projectId: input.projectId,
827
625
  name: prof.name,
828
626
  providerId,
829
- options: optionsStr ?? null,
627
+ options: this.normalizeProfileOptions(prof.options),
830
628
  systemPrompt: null,
831
629
  instructions: prof.instructions ?? null,
832
630
  temperature: prof.temperature ?? null,
833
631
  maxTokens: prof.maxTokens ?? null,
834
632
  });
835
- if (prof.id)
836
- profileIdMap[prof.id] = created.id;
837
- profileNameToNewId.set(prof.name.trim().toLowerCase(), created.id);
633
+ const profKey = prof.id || `name:${prof.name.trim().toLowerCase()}`;
634
+ profileIdMap[profKey] = created.id;
838
635
  }
839
- const agentNameToNewId = new Map();
840
636
  for (const a of payload.agents) {
841
637
  const oldProfileId = a.profileId ?? '';
842
638
  const newProfileId = oldProfileId && profileIdMap[oldProfileId] ? profileIdMap[oldProfileId] : undefined;
843
639
  if (!newProfileId) {
844
- throw new common_1.BadRequestException({
845
- message: `Profile mapping missing for agent ${a.name}`,
640
+ throw new error_types_1.ValidationError(`Profile mapping missing for agent ${a.name}`, {
846
641
  profileId: oldProfileId || null,
847
642
  });
848
643
  }
@@ -852,75 +647,26 @@ let ProjectsService = class ProjectsService {
852
647
  profileId: newProfileId,
853
648
  description: a.description ?? null,
854
649
  });
855
- if (a.id)
856
- agentIdMap[a.id] = created.id;
857
- agentNameToNewId.set(a.name.trim().toLowerCase(), created.id);
858
- }
859
- const watcherIdMap = {};
860
- for (const w of payload.watchers) {
861
- let scopeFilterId = null;
862
- if (w.scopeFilterName && w.scope !== 'all') {
863
- const scopeFilterNameLower = w.scopeFilterName.trim().toLowerCase();
864
- switch (w.scope) {
865
- case 'agent': {
866
- scopeFilterId = agentNameToNewId.get(scopeFilterNameLower) ?? null;
867
- break;
868
- }
869
- case 'profile': {
870
- scopeFilterId = profileNameToNewId.get(scopeFilterNameLower) ?? null;
871
- break;
872
- }
873
- case 'provider': {
874
- const provider = providers.items.find((p) => p.name.trim().toLowerCase() === scopeFilterNameLower);
875
- scopeFilterId = provider?.id ?? null;
876
- break;
877
- }
878
- }
879
- if (!scopeFilterId) {
880
- logger.warn({ watcherName: w.name, scope: w.scope, scopeFilterName: w.scopeFilterName }, 'Could not resolve scope filter, setting scope to "all"');
881
- }
882
- }
883
- const created = await this.storage.createWatcher({
884
- projectId: input.projectId,
885
- name: w.name,
886
- description: w.description ?? null,
887
- enabled: w.enabled,
888
- scope: scopeFilterId ? w.scope : 'all',
889
- scopeFilterId,
890
- pollIntervalMs: w.pollIntervalMs,
891
- viewportLines: w.viewportLines,
892
- condition: w.condition,
893
- cooldownMs: w.cooldownMs,
894
- cooldownMode: w.cooldownMode,
895
- eventName: w.eventName,
896
- });
897
- if (w.id)
898
- watcherIdMap[w.id] = created.id;
899
- if (created.enabled) {
900
- await this.watcherRunner.startWatcher(created);
901
- }
902
- }
903
- const subscriberIdMap = {};
904
- for (const s of payload.subscribers) {
905
- const created = await this.storage.createSubscriber({
906
- projectId: input.projectId,
907
- name: s.name,
908
- description: s.description ?? null,
909
- enabled: s.enabled,
910
- eventName: s.eventName,
911
- eventFilter: s.eventFilter ?? null,
912
- actionType: s.actionType,
913
- actionInputs: s.actionInputs,
914
- delayMs: s.delayMs,
915
- cooldownMs: s.cooldownMs,
916
- retryOnError: s.retryOnError,
917
- groupName: s.groupName ?? null,
918
- position: s.position ?? 0,
919
- priority: s.priority ?? 0,
920
- });
921
- if (s.id)
922
- subscriberIdMap[s.id] = created.id;
650
+ const agentKey = a.id || `name:${a.name.trim().toLowerCase()}`;
651
+ agentIdMap[agentKey] = created.id;
923
652
  }
653
+ const augmentedPayload = {
654
+ agents: payload.agents.map((a) => ({
655
+ ...a,
656
+ id: a.id || `name:${a.name.trim().toLowerCase()}`,
657
+ })),
658
+ profiles: payload.profiles.map((p) => ({
659
+ ...p,
660
+ id: p.id || `name:${p.name.trim().toLowerCase()}`,
661
+ })),
662
+ };
663
+ const { agentNameToId: agentNameToNewId, profileNameToId: profileNameToNewId } = this.buildNameToIdMaps(augmentedPayload, { agentIdMap, profileIdMap });
664
+ const { watcherIdMap } = await this.createWatchersFromPayload(input.projectId, payload.watchers, {
665
+ agentNameToId: agentNameToNewId,
666
+ profileNameToId: profileNameToNewId,
667
+ providerNameToId: available,
668
+ });
669
+ const { subscriberIdMap } = await this.createSubscribersFromPayload(input.projectId, payload.subscribers);
924
670
  logger.info({
925
671
  watchersCreated: payload.watchers.length,
926
672
  subscribersCreated: payload.subscribers.length,
@@ -952,62 +698,31 @@ let ProjectsService = class ProjectsService {
952
698
  }
953
699
  }
954
700
  logger.info({ epicsRemapped, epicsCleared }, 'Epic agent references updated after import');
955
- let initialPromptSet = false;
956
- if (payload.initialPrompt) {
957
- let targetPromptId = null;
958
- if (payload.initialPrompt.title) {
959
- const match = createdPrompts.find((cp) => cp.title.trim().toLowerCase() === payload.initialPrompt.title.trim().toLowerCase());
960
- if (match)
961
- targetPromptId = match.id;
962
- }
963
- if (!targetPromptId && payload.initialPrompt.promptId) {
964
- const mapped = promptIdMap[payload.initialPrompt.promptId];
965
- if (mapped)
966
- targetPromptId = mapped;
967
- }
968
- await this.settings.updateSettings({
969
- projectId: input.projectId,
970
- initialSessionPromptId: targetPromptId ?? null,
971
- });
972
- initialPromptSet = Boolean(targetPromptId);
973
- }
974
- if (payload.projectSettings) {
975
- const ps = payload.projectSettings;
976
- if (ps.initialPromptTitle) {
977
- const matchByTitle = createdPrompts.find((cp) => cp.title.toLowerCase() === ps.initialPromptTitle.toLowerCase());
978
- if (matchByTitle) {
979
- await this.settings.updateSettings({
980
- projectId: input.projectId,
981
- initialSessionPromptId: matchByTitle.id,
982
- });
983
- initialPromptSet = true;
984
- logger.info({ projectId: input.projectId, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
985
- }
986
- }
987
- if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
988
- const autoCleanStatusIds = ps.autoCleanStatusLabels
989
- .map((label) => templateLabelToStatusId.get(label.toLowerCase()))
990
- .filter((id) => !!id);
991
- if (autoCleanStatusIds.length > 0) {
992
- const currentSettings = this.settings.getSettings();
993
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
994
- await this.settings.updateSettings({
995
- autoClean: {
996
- statusIds: { ...existingAutoClean, [input.projectId]: autoCleanStatusIds },
997
- },
998
- });
999
- logger.info({ projectId: input.projectId, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
1000
- }
1001
- }
1002
- if (ps.epicAssignedTemplate) {
1003
- await this.settings.updateSettings({
1004
- events: {
1005
- epicAssigned: { template: ps.epicAssignedTemplate },
1006
- },
1007
- });
1008
- logger.info({ projectId: input.projectId }, 'Applied epicAssigned template from projectSettings');
701
+ let mergedInitialPromptTitle;
702
+ if (payload.initialPrompt?.title) {
703
+ mergedInitialPromptTitle = payload.initialPrompt.title;
704
+ }
705
+ else if (payload.initialPrompt?.promptId) {
706
+ const matchingPrompt = payload.prompts.find((p) => p.id === payload.initialPrompt.promptId);
707
+ if (matchingPrompt) {
708
+ mergedInitialPromptTitle = matchingPrompt.title;
1009
709
  }
1010
710
  }
711
+ else if (payload.projectSettings?.initialPromptTitle) {
712
+ mergedInitialPromptTitle = payload.projectSettings.initialPromptTitle;
713
+ }
714
+ const mergedSettings = payload.projectSettings
715
+ ? { ...payload.projectSettings, initialPromptTitle: mergedInitialPromptTitle }
716
+ : mergedInitialPromptTitle
717
+ ? { initialPromptTitle: mergedInitialPromptTitle }
718
+ : undefined;
719
+ const promptTitleToId = new Map();
720
+ for (const cp of createdPrompts) {
721
+ promptTitleToId.set(cp.title.toLowerCase(), cp.id);
722
+ }
723
+ const archiveStatusId = templateLabelToStatusId.get('archive') ?? null;
724
+ const settingsResult = await this.applyProjectSettings(input.projectId, mergedSettings, { promptTitleToId, statusLabelToId: templateLabelToStatusId }, archiveStatusId);
725
+ const initialPromptSet = settingsResult.initialPromptSet;
1011
726
  return {
1012
727
  success: true,
1013
728
  mode: 'replace',
@@ -1051,8 +766,210 @@ let ProjectsService = class ProjectsService {
1051
766
  catch (error) {
1052
767
  logger.error({ error, projectId: input.projectId }, 'Import failed');
1053
768
  const message = this.getImportErrorMessage(error);
1054
- throw new common_1.BadRequestException({ message });
769
+ throw new error_types_1.StorageError(message);
770
+ }
771
+ }
772
+ async resolveProviders(providerNames) {
773
+ const providers = await this.storage.listProviders();
774
+ const available = new Map();
775
+ for (const prov of providers.items) {
776
+ available.set(prov.name.trim().toLowerCase(), prov.id);
777
+ }
778
+ const missing = Array.from(providerNames).filter((n) => !available.has(n));
779
+ return { available, missing };
780
+ }
781
+ buildNameToIdMaps(payload, mappings) {
782
+ const agentNameToId = new Map();
783
+ for (const a of payload.agents) {
784
+ if (a.id && mappings.agentIdMap[a.id]) {
785
+ const nameLower = a.name.trim().toLowerCase();
786
+ if (agentNameToId.has(nameLower)) {
787
+ logger.warn({
788
+ name: a.name,
789
+ existingId: agentNameToId.get(nameLower),
790
+ newId: mappings.agentIdMap[a.id],
791
+ }, 'Duplicate agent name detected, using last occurrence');
792
+ }
793
+ agentNameToId.set(nameLower, mappings.agentIdMap[a.id]);
794
+ }
795
+ }
796
+ const profileNameToId = new Map();
797
+ for (const prof of payload.profiles) {
798
+ if (prof.id && mappings.profileIdMap[prof.id]) {
799
+ const nameLower = prof.name.trim().toLowerCase();
800
+ if (profileNameToId.has(nameLower)) {
801
+ logger.warn({
802
+ name: prof.name,
803
+ existingId: profileNameToId.get(nameLower),
804
+ newId: mappings.profileIdMap[prof.id],
805
+ }, 'Duplicate profile name detected, using last occurrence');
806
+ }
807
+ profileNameToId.set(nameLower, mappings.profileIdMap[prof.id]);
808
+ }
1055
809
  }
810
+ return { agentNameToId, profileNameToId };
811
+ }
812
+ async createWatchersFromPayload(projectId, watchers, maps) {
813
+ const watcherIdMap = {};
814
+ let created = 0;
815
+ for (const w of watchers) {
816
+ let scopeFilterId = null;
817
+ if (w.scopeFilterName && w.scope !== 'all') {
818
+ const scopeFilterNameLower = w.scopeFilterName.trim().toLowerCase();
819
+ switch (w.scope) {
820
+ case 'agent': {
821
+ scopeFilterId = maps.agentNameToId.get(scopeFilterNameLower) ?? null;
822
+ break;
823
+ }
824
+ case 'profile': {
825
+ scopeFilterId = maps.profileNameToId.get(scopeFilterNameLower) ?? null;
826
+ break;
827
+ }
828
+ case 'provider': {
829
+ scopeFilterId = maps.providerNameToId.get(scopeFilterNameLower) ?? null;
830
+ break;
831
+ }
832
+ }
833
+ if (!scopeFilterId) {
834
+ logger.warn({ projectId, watcherName: w.name, scope: w.scope, scopeFilterName: w.scopeFilterName }, 'Could not resolve scope filter, setting scope to "all"');
835
+ }
836
+ }
837
+ try {
838
+ const createdWatcher = await this.watchersService.createWatcher({
839
+ projectId,
840
+ name: w.name,
841
+ description: w.description ?? null,
842
+ enabled: w.enabled,
843
+ scope: scopeFilterId ? w.scope : 'all',
844
+ scopeFilterId,
845
+ pollIntervalMs: w.pollIntervalMs,
846
+ viewportLines: w.viewportLines,
847
+ condition: w.condition,
848
+ cooldownMs: w.cooldownMs,
849
+ cooldownMode: w.cooldownMode,
850
+ eventName: w.eventName,
851
+ });
852
+ if (w.id)
853
+ watcherIdMap[w.id] = createdWatcher.id;
854
+ created++;
855
+ }
856
+ catch (error) {
857
+ if (error instanceof Error &&
858
+ error.message.includes('UNIQUE constraint failed') &&
859
+ error.message.includes('event_name')) {
860
+ throw new error_types_1.ConflictError(`Duplicate watcher eventName: "${w.eventName}"`, {
861
+ hint: 'Each watcher must have a unique eventName within the project.',
862
+ });
863
+ }
864
+ throw error;
865
+ }
866
+ }
867
+ return { created, watcherIdMap };
868
+ }
869
+ async createSubscribersFromPayload(projectId, subscribers) {
870
+ const subscriberIdMap = {};
871
+ let created = 0;
872
+ for (const s of subscribers) {
873
+ const createdSubscriber = await this.storage.createSubscriber({
874
+ projectId,
875
+ name: s.name,
876
+ description: s.description ?? null,
877
+ enabled: s.enabled,
878
+ eventName: s.eventName,
879
+ eventFilter: s.eventFilter ?? null,
880
+ actionType: s.actionType,
881
+ actionInputs: s.actionInputs,
882
+ delayMs: s.delayMs,
883
+ cooldownMs: s.cooldownMs,
884
+ retryOnError: s.retryOnError,
885
+ groupName: s.groupName ?? null,
886
+ position: s.position ?? 0,
887
+ priority: s.priority ?? 0,
888
+ });
889
+ if (s.id)
890
+ subscriberIdMap[s.id] = createdSubscriber.id;
891
+ created++;
892
+ }
893
+ return { created, subscriberIdMap };
894
+ }
895
+ async applyProjectSettings(projectId, projectSettings, maps, archiveStatusId) {
896
+ let initialPromptSet = false;
897
+ if (projectSettings) {
898
+ const ps = projectSettings;
899
+ if (ps.initialPromptTitle) {
900
+ const promptId = maps.promptTitleToId.get(ps.initialPromptTitle.toLowerCase());
901
+ if (promptId) {
902
+ await this.settings.updateSettings({
903
+ projectId,
904
+ initialSessionPromptId: promptId,
905
+ });
906
+ initialPromptSet = true;
907
+ logger.info({ projectId, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
908
+ }
909
+ }
910
+ if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
911
+ const autoCleanStatusIds = ps.autoCleanStatusLabels
912
+ .map((label) => maps.statusLabelToId.get(label.toLowerCase()))
913
+ .filter((id) => !!id);
914
+ if (autoCleanStatusIds.length > 0) {
915
+ const currentSettings = this.settings.getSettings();
916
+ const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
917
+ await this.settings.updateSettings({
918
+ autoClean: {
919
+ statusIds: { ...existingAutoClean, [projectId]: autoCleanStatusIds },
920
+ },
921
+ });
922
+ logger.info({ projectId, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
923
+ }
924
+ }
925
+ else if (archiveStatusId) {
926
+ const currentSettings = this.settings.getSettings();
927
+ const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
928
+ await this.settings.updateSettings({
929
+ autoClean: {
930
+ statusIds: { ...existingAutoClean, [projectId]: [archiveStatusId] },
931
+ },
932
+ });
933
+ logger.info({ projectId, archiveStatusId }, 'Auto-configured Archive status for auto-clean (fallback)');
934
+ }
935
+ if (ps.epicAssignedTemplate) {
936
+ await this.settings.updateSettings({
937
+ events: {
938
+ epicAssigned: { template: ps.epicAssignedTemplate },
939
+ },
940
+ });
941
+ logger.info({ projectId }, 'Applied epicAssigned template from projectSettings');
942
+ }
943
+ if (ps.messagePoolSettings) {
944
+ await this.settings.setProjectPoolSettings(projectId, ps.messagePoolSettings);
945
+ logger.info({ projectId, poolSettings: ps.messagePoolSettings }, 'Applied message pool settings from projectSettings');
946
+ }
947
+ }
948
+ else if (archiveStatusId) {
949
+ const currentSettings = this.settings.getSettings();
950
+ const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
951
+ await this.settings.updateSettings({
952
+ autoClean: {
953
+ statusIds: { ...existingAutoClean, [projectId]: [archiveStatusId] },
954
+ },
955
+ });
956
+ logger.info({ projectId, archiveStatusId }, 'Auto-configured Archive status for auto-clean');
957
+ }
958
+ return { initialPromptSet };
959
+ }
960
+ normalizeProfileOptions(options) {
961
+ if (typeof options === 'string') {
962
+ return options;
963
+ }
964
+ if (options && typeof options === 'object') {
965
+ try {
966
+ return JSON.stringify(options);
967
+ }
968
+ catch {
969
+ return null;
970
+ }
971
+ }
972
+ return null;
1056
973
  }
1057
974
  getImportErrorMessage(error) {
1058
975
  const errorCode = error?.code;
@@ -1076,6 +993,12 @@ let ProjectsService = class ProjectsService {
1076
993
  }
1077
994
  return 'Import failed: An unexpected error occurred.';
1078
995
  }
996
+ slugify(name) {
997
+ return name
998
+ .toLowerCase()
999
+ .replace(/[^a-z0-9]+/g, '-')
1000
+ .replace(/(^-|-$)/g, '');
1001
+ }
1079
1002
  };
1080
1003
  exports.ProjectsService = ProjectsService;
1081
1004
  exports.ProjectsService = ProjectsService = __decorate([
@@ -1084,6 +1007,7 @@ exports.ProjectsService = ProjectsService = __decorate([
1084
1007
  __metadata("design:paramtypes", [Object, sessions_service_1.SessionsService,
1085
1008
  settings_service_1.SettingsService,
1086
1009
  watchers_service_1.WatchersService,
1087
- watcher_runner_service_1.WatcherRunnerService])
1010
+ watcher_runner_service_1.WatcherRunnerService,
1011
+ unified_template_service_1.UnifiedTemplateService])
1088
1012
  ], ProjectsService);
1089
1013
  //# sourceMappingURL=projects.service.js.map