devchain-cli 0.3.2 → 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 (153) 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/epics/services/epics.service.d.ts +2 -0
  11. package/dist/server/modules/epics/services/epics.service.js +194 -32
  12. package/dist/server/modules/epics/services/epics.service.js.map +1 -1
  13. package/dist/server/modules/events/catalog/epic.assigned.d.ts +9 -13
  14. package/dist/server/modules/events/catalog/epic.assigned.js +5 -5
  15. package/dist/server/modules/events/catalog/epic.assigned.js.map +1 -1
  16. package/dist/server/modules/events/catalog/epic.created.d.ts +39 -0
  17. package/dist/server/modules/events/catalog/epic.created.js +20 -0
  18. package/dist/server/modules/events/catalog/epic.created.js.map +1 -0
  19. package/dist/server/modules/events/catalog/epic.updated.d.ts +178 -0
  20. package/dist/server/modules/events/catalog/epic.updated.js +43 -0
  21. package/dist/server/modules/events/catalog/epic.updated.js.map +1 -0
  22. package/dist/server/modules/events/catalog/index.d.ts +221 -12
  23. package/dist/server/modules/events/catalog/index.js +9 -1
  24. package/dist/server/modules/events/catalog/index.js.map +1 -1
  25. package/dist/server/modules/events/catalog/settings.terminal.changed.d.ts +12 -0
  26. package/dist/server/modules/events/catalog/settings.terminal.changed.js +11 -0
  27. package/dist/server/modules/events/catalog/settings.terminal.changed.js.map +1 -0
  28. package/dist/server/modules/events/services/events.service.js +0 -7
  29. package/dist/server/modules/events/services/events.service.js.map +1 -1
  30. package/dist/server/modules/events/subscribers/chat-message-delivery.subscriber.d.ts +5 -2
  31. package/dist/server/modules/events/subscribers/chat-message-delivery.subscriber.js +44 -24
  32. package/dist/server/modules/events/subscribers/chat-message-delivery.subscriber.js.map +1 -1
  33. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.d.ts +5 -8
  34. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.js +37 -56
  35. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.js.map +1 -1
  36. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +8 -6
  37. package/dist/server/modules/mcp/dtos/mcp.dto.js.map +1 -1
  38. package/dist/server/modules/mcp/mcp.module.js +2 -0
  39. package/dist/server/modules/mcp/mcp.module.js.map +1 -1
  40. package/dist/server/modules/mcp/services/mcp.service.d.ts +5 -1
  41. package/dist/server/modules/mcp/services/mcp.service.js +65 -33
  42. package/dist/server/modules/mcp/services/mcp.service.js.map +1 -1
  43. package/dist/server/modules/projects/controllers/projects.controller.d.ts +140 -6
  44. package/dist/server/modules/projects/controllers/projects.controller.js +91 -31
  45. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  46. package/dist/server/modules/projects/dtos/export.dto.d.ts +83 -0
  47. package/dist/server/modules/projects/dtos/export.dto.js +34 -0
  48. package/dist/server/modules/projects/dtos/export.dto.js.map +1 -0
  49. package/dist/server/modules/projects/projects.module.js +1 -0
  50. package/dist/server/modules/projects/projects.module.js.map +1 -1
  51. package/dist/server/modules/projects/services/projects.service.d.ts +37 -3
  52. package/dist/server/modules/projects/services/projects.service.js +353 -526
  53. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  54. package/dist/server/modules/registry/controllers/registry.controller.d.ts +98 -0
  55. package/dist/server/modules/registry/controllers/registry.controller.js +352 -0
  56. package/dist/server/modules/registry/controllers/registry.controller.js.map +1 -0
  57. package/dist/server/modules/registry/controllers/templates.controller.d.ts +35 -0
  58. package/dist/server/modules/registry/controllers/templates.controller.js +157 -0
  59. package/dist/server/modules/registry/controllers/templates.controller.js.map +1 -0
  60. package/dist/server/modules/registry/dtos/registry-error.d.ts +11 -0
  61. package/dist/server/modules/registry/dtos/registry-error.js +30 -0
  62. package/dist/server/modules/registry/dtos/registry-error.js.map +1 -0
  63. package/dist/server/modules/registry/dtos/registry.dto.d.ts +7 -0
  64. package/dist/server/modules/registry/dtos/registry.dto.js +50 -0
  65. package/dist/server/modules/registry/dtos/registry.dto.js.map +1 -0
  66. package/dist/server/modules/registry/index.d.ts +7 -0
  67. package/dist/server/modules/registry/index.js +24 -0
  68. package/dist/server/modules/registry/index.js.map +1 -0
  69. package/dist/server/modules/registry/interfaces/registry.interface.d.ts +68 -0
  70. package/dist/server/modules/registry/interfaces/registry.interface.js +3 -0
  71. package/dist/server/modules/registry/interfaces/registry.interface.js.map +1 -0
  72. package/dist/server/modules/registry/registry.module.d.ts +2 -0
  73. package/dist/server/modules/registry/registry.module.js +45 -0
  74. package/dist/server/modules/registry/registry.module.js.map +1 -0
  75. package/dist/server/modules/registry/services/registry-client.service.d.ts +14 -0
  76. package/dist/server/modules/registry/services/registry-client.service.js +226 -0
  77. package/dist/server/modules/registry/services/registry-client.service.js.map +1 -0
  78. package/dist/server/modules/registry/services/registry-orchestration.service.d.ts +45 -0
  79. package/dist/server/modules/registry/services/registry-orchestration.service.js +165 -0
  80. package/dist/server/modules/registry/services/registry-orchestration.service.js.map +1 -0
  81. package/dist/server/modules/registry/services/template-cache.service.d.ts +66 -0
  82. package/dist/server/modules/registry/services/template-cache.service.js +310 -0
  83. package/dist/server/modules/registry/services/template-cache.service.js.map +1 -0
  84. package/dist/server/modules/registry/services/template-upgrade.service.d.ts +41 -0
  85. package/dist/server/modules/registry/services/template-upgrade.service.js +212 -0
  86. package/dist/server/modules/registry/services/template-upgrade.service.js.map +1 -0
  87. package/dist/server/modules/registry/services/unified-template.service.d.ts +35 -0
  88. package/dist/server/modules/registry/services/unified-template.service.js +228 -0
  89. package/dist/server/modules/registry/services/unified-template.service.js.map +1 -0
  90. package/dist/server/modules/sessions/controllers/sessions.controller.d.ts +32 -1
  91. package/dist/server/modules/sessions/controllers/sessions.controller.js +150 -8
  92. package/dist/server/modules/sessions/controllers/sessions.controller.js.map +1 -1
  93. package/dist/server/modules/sessions/services/message-activity-stream.service.d.ts +12 -0
  94. package/dist/server/modules/sessions/services/message-activity-stream.service.js +51 -0
  95. package/dist/server/modules/sessions/services/message-activity-stream.service.js.map +1 -0
  96. package/dist/server/modules/sessions/services/sessions-message-pool.service.d.ts +125 -0
  97. package/dist/server/modules/sessions/services/sessions-message-pool.service.js +599 -0
  98. package/dist/server/modules/sessions/services/sessions-message-pool.service.js.map +1 -0
  99. package/dist/server/modules/sessions/services/sessions.service.d.ts +2 -9
  100. package/dist/server/modules/sessions/services/sessions.service.js +32 -27
  101. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  102. package/dist/server/modules/sessions/sessions.module.js +16 -2
  103. package/dist/server/modules/sessions/sessions.module.js.map +1 -1
  104. package/dist/server/modules/settings/dtos/settings.dto.d.ts +231 -0
  105. package/dist/server/modules/settings/dtos/settings.dto.js +40 -1
  106. package/dist/server/modules/settings/dtos/settings.dto.js.map +1 -1
  107. package/dist/server/modules/settings/services/settings.service.d.ts +56 -15
  108. package/dist/server/modules/settings/services/settings.service.js +295 -10
  109. package/dist/server/modules/settings/services/settings.service.js.map +1 -1
  110. package/dist/server/modules/subscribers/actions/action.interface.d.ts +2 -0
  111. package/dist/server/modules/subscribers/actions/send-message.action.js +34 -11
  112. package/dist/server/modules/subscribers/actions/send-message.action.js.map +1 -1
  113. package/dist/server/modules/subscribers/events/event-fields-catalog.js +109 -6
  114. package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
  115. package/dist/server/modules/subscribers/services/subscriber-executor.service.d.ts +3 -1
  116. package/dist/server/modules/subscribers/services/subscriber-executor.service.js +13 -1
  117. package/dist/server/modules/subscribers/services/subscriber-executor.service.js.map +1 -1
  118. package/dist/server/modules/terminal/dtos/ws-envelope.dto.d.ts +20 -0
  119. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js +8 -1
  120. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js.map +1 -1
  121. package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +1 -3
  122. package/dist/server/modules/terminal/gateways/terminal.gateway.js +50 -11
  123. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  124. package/dist/server/modules/terminal/services/pty.service.d.ts +1 -3
  125. package/dist/server/modules/terminal/services/pty.service.js +2 -14
  126. package/dist/server/modules/terminal/services/pty.service.js.map +1 -1
  127. package/dist/server/modules/terminal/services/terminal-seed.service.d.ts +6 -4
  128. package/dist/server/modules/terminal/services/terminal-seed.service.js +74 -67
  129. package/dist/server/modules/terminal/services/terminal-seed.service.js.map +1 -1
  130. package/dist/server/modules/terminal/services/tmux.service.js +3 -2
  131. package/dist/server/modules/terminal/services/tmux.service.js.map +1 -1
  132. package/dist/server/modules/terminal/terminal.module.js +0 -3
  133. package/dist/server/modules/terminal/terminal.module.js.map +1 -1
  134. package/dist/server/templates/claude-codex-advanced.json +130 -69
  135. package/dist/server/templates/claude-opus.json +30 -19
  136. package/dist/server/templates/simple-codex.json +30 -19
  137. package/dist/server/test-setup.js +19 -0
  138. package/dist/server/test-setup.js.map +1 -1
  139. package/dist/server/tsconfig.tsbuildinfo +1 -1
  140. package/dist/server/ui/assets/index-4Q4XuabC.js +733 -0
  141. package/dist/server/ui/assets/index-BkGGbapJ.css +32 -0
  142. package/dist/server/ui/index.html +2 -2
  143. package/dist/templates/claude-codex-advanced.json +130 -69
  144. package/dist/templates/claude-opus.json +30 -19
  145. package/dist/templates/simple-codex.json +30 -19
  146. package/package.json +9 -2
  147. package/dist/server/modules/terminal/services/terminal-emulator.service.d.ts +0 -40
  148. package/dist/server/modules/terminal/services/terminal-emulator.service.js +0 -356
  149. package/dist/server/modules/terminal/services/terminal-emulator.service.js.map +0 -1
  150. package/dist/server/templates/claude-codex-advanced-swe.json +0 -130
  151. package/dist/server/ui/assets/index-C9GXCjnF.js +0 -700
  152. package/dist/server/ui/assets/index-o0FbZg-1.css +0 -32
  153. package/dist/templates/claude-codex-advanced-swe.json +0 -130
@@ -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,196 +180,63 @@ let ProjectsService = class ProjectsService {
342
180
  rootPath: input.rootPath,
343
181
  isTemplate: false,
344
182
  }, templatePayload);
345
- const agentNameToNewId = new Map();
346
- for (const a of templatePayload.agents) {
347
- if (a.id && result.mappings.agentIdMap[a.id]) {
348
- agentNameToNewId.set(a.name.trim().toLowerCase(), result.mappings.agentIdMap[a.id]);
349
- }
350
- }
351
- const profileNameToNewId = new Map();
352
- for (const prof of templatePayload.profiles) {
353
- if (prof.id && result.mappings.profileIdMap[prof.id]) {
354
- profileNameToNewId.set(prof.name.trim().toLowerCase(), result.mappings.profileIdMap[prof.id]);
355
- }
356
- }
357
- let initialPromptSet = false;
358
- if (payload.initialPrompt) {
359
- let targetPromptId = null;
360
- if (payload.initialPrompt.title) {
361
- const oldId = Object.keys(result.mappings.promptIdMap).find((oldId) => payload.prompts.some((p) => p.id === oldId &&
362
- p.title.trim().toLowerCase() === payload.initialPrompt.title.trim().toLowerCase()));
363
- if (oldId) {
364
- targetPromptId = result.mappings.promptIdMap[oldId];
365
- }
366
- }
367
- if (!targetPromptId && payload.initialPrompt.promptId) {
368
- const mapped = result.mappings.promptIdMap[payload.initialPrompt.promptId];
369
- if (mapped)
370
- targetPromptId = mapped;
371
- }
372
- await this.settings.updateSettings({
373
- projectId: result.project.id,
374
- initialSessionPromptId: targetPromptId ?? null,
375
- });
376
- initialPromptSet = Boolean(targetPromptId);
377
- }
378
- if (payload.projectSettings) {
379
- const ps = payload.projectSettings;
380
- if (ps.initialPromptTitle) {
381
- const promptByTitle = payload.prompts.find((p) => p.title.toLowerCase() === ps.initialPromptTitle.toLowerCase());
382
- if (promptByTitle?.id) {
383
- const mappedPromptId = result.mappings.promptIdMap[promptByTitle.id];
384
- if (mappedPromptId) {
385
- await this.settings.updateSettings({
386
- projectId: result.project.id,
387
- initialSessionPromptId: mappedPromptId,
388
- });
389
- initialPromptSet = true;
390
- logger.info({ projectId: result.project.id, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
391
- }
392
- }
393
- }
394
- if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
395
- const statusLabelMap = new Map(templatePayload.statuses.map((s) => [s.label.toLowerCase(), s.id]));
396
- const autoCleanStatusIds = ps.autoCleanStatusLabels
397
- .map((label) => {
398
- const templateId = statusLabelMap.get(label.toLowerCase());
399
- return templateId ? result.mappings.statusIdMap[templateId] : undefined;
400
- })
401
- .filter((id) => !!id);
402
- if (autoCleanStatusIds.length > 0) {
403
- const currentSettings = this.settings.getSettings();
404
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
405
- await this.settings.updateSettings({
406
- autoClean: {
407
- statusIds: { ...existingAutoClean, [result.project.id]: autoCleanStatusIds },
408
- },
409
- });
410
- logger.info({ projectId: result.project.id, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
411
- }
412
- }
413
- else {
414
- const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
415
- if (archiveTemplateStatus?.id) {
416
- const archiveNewId = result.mappings.statusIdMap[archiveTemplateStatus.id];
417
- if (archiveNewId) {
418
- const currentSettings = this.settings.getSettings();
419
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
420
- await this.settings.updateSettings({
421
- autoClean: {
422
- statusIds: { ...existingAutoClean, [result.project.id]: [archiveNewId] },
423
- },
424
- });
425
- logger.info({ projectId: result.project.id, archiveStatusId: archiveNewId }, 'Auto-configured Archive status for auto-clean (fallback)');
426
- }
427
- }
428
- }
429
- if (ps.epicAssignedTemplate) {
430
- await this.settings.updateSettings({
431
- events: {
432
- epicAssigned: { template: ps.epicAssignedTemplate },
433
- },
434
- });
435
- logger.info({ projectId: result.project.id }, 'Applied epicAssigned template from projectSettings');
436
- }
437
- }
438
- else {
439
- const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
440
- if (archiveTemplateStatus?.id) {
441
- const archiveNewId = result.mappings.statusIdMap[archiveTemplateStatus.id];
442
- if (archiveNewId) {
443
- const currentSettings = this.settings.getSettings();
444
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
445
- await this.settings.updateSettings({
446
- autoClean: {
447
- statusIds: { ...existingAutoClean, [result.project.id]: [archiveNewId] },
448
- },
449
- });
450
- logger.info({ projectId: result.project.id, archiveStatusId: archiveNewId }, 'Auto-configured Archive status for auto-clean');
451
- }
452
- }
453
- }
454
- let watchersCreated = 0;
455
- for (const w of payload.watchers) {
456
- let scopeFilterId = null;
457
- if (w.scopeFilterName && w.scope !== 'all') {
458
- const scopeFilterNameLower = w.scopeFilterName.trim().toLowerCase();
459
- switch (w.scope) {
460
- case 'agent': {
461
- scopeFilterId = agentNameToNewId.get(scopeFilterNameLower) ?? null;
462
- break;
463
- }
464
- case 'profile': {
465
- scopeFilterId = profileNameToNewId.get(scopeFilterNameLower) ?? null;
466
- break;
467
- }
468
- case 'provider': {
469
- const provider = providers.items.find((p) => p.name.trim().toLowerCase() === scopeFilterNameLower);
470
- scopeFilterId = provider?.id ?? null;
471
- break;
472
- }
473
- }
474
- if (!scopeFilterId) {
475
- logger.warn({
476
- projectId: result.project.id,
477
- watcherName: w.name,
478
- scope: w.scope,
479
- scopeFilterName: w.scopeFilterName,
480
- }, 'Could not resolve scope filter for template watcher, setting scope to "all"');
481
- }
482
- }
483
- try {
484
- const created = await this.storage.createWatcher({
485
- projectId: result.project.id,
486
- name: w.name,
487
- description: w.description ?? null,
488
- enabled: w.enabled,
489
- scope: scopeFilterId ? w.scope : 'all',
490
- scopeFilterId,
491
- pollIntervalMs: w.pollIntervalMs,
492
- viewportLines: w.viewportLines,
493
- condition: w.condition,
494
- cooldownMs: w.cooldownMs,
495
- cooldownMode: w.cooldownMode,
496
- eventName: w.eventName,
497
- });
498
- watchersCreated++;
499
- if (created.enabled) {
500
- await this.watcherRunner.startWatcher(created);
501
- }
502
- }
503
- catch (error) {
504
- if (error instanceof Error &&
505
- error.message.includes('UNIQUE constraint failed') &&
506
- error.message.includes('event_name')) {
507
- throw new common_1.BadRequestException({
508
- message: `Duplicate watcher eventName: "${w.eventName}"`,
509
- hint: 'Each watcher must have a unique eventName within the project.',
510
- });
511
- }
512
- throw error;
513
- }
514
- }
515
- let subscribersCreated = 0;
516
- for (const s of payload.subscribers) {
517
- await this.storage.createSubscriber({
518
- projectId: result.project.id,
519
- name: s.name,
520
- description: s.description ?? null,
521
- enabled: s.enabled,
522
- eventName: s.eventName,
523
- eventFilter: s.eventFilter ?? null,
524
- actionType: s.actionType,
525
- actionInputs: s.actionInputs,
526
- delayMs: s.delayMs,
527
- cooldownMs: s.cooldownMs,
528
- retryOnError: s.retryOnError,
529
- groupName: s.groupName ?? null,
530
- position: s.position ?? 0,
531
- priority: s.priority ?? 0,
532
- });
533
- subscribersCreated++;
534
- }
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');
535
240
  return {
536
241
  success: true,
537
242
  project: result.project,
@@ -545,9 +250,11 @@ let ProjectsService = class ProjectsService {
545
250
  message: 'Project created from template successfully.',
546
251
  };
547
252
  }
548
- async exportProject(projectId) {
253
+ async exportProject(projectId, opts) {
549
254
  logger.info({ projectId }, 'exportProject');
550
- 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),
551
258
  this.storage.listPrompts({ projectId, limit: 1000, offset: 0 }),
552
259
  this.storage.listAgentProfiles({ projectId, limit: 1000, offset: 0 }),
553
260
  this.storage.listAgents(projectId, { limit: 1000, offset: 0 }),
@@ -654,6 +361,10 @@ let ProjectsService = class ProjectsService {
654
361
  if (epicAssignedTemplate) {
655
362
  projectSettings.epicAssignedTemplate = epicAssignedTemplate;
656
363
  }
364
+ const poolSettings = settings.messagePool?.projects?.[projectId];
365
+ if (poolSettings && Object.keys(poolSettings).length > 0) {
366
+ projectSettings.messagePoolSettings = poolSettings;
367
+ }
657
368
  const watchers = await Promise.all(watchersRes.map(async (w) => {
658
369
  let scopeFilterName = null;
659
370
  if (w.scopeFilterId) {
@@ -711,7 +422,17 @@ let ProjectsService = class ProjectsService {
711
422
  position: s.position,
712
423
  priority: s.priority,
713
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
+ };
714
434
  const exportPayload = {
435
+ _manifest: manifest,
715
436
  version: 1,
716
437
  exportedAt: new Date().toISOString(),
717
438
  prompts,
@@ -730,14 +451,9 @@ let ProjectsService = class ProjectsService {
730
451
  async importProject(input) {
731
452
  logger.info({ projectId: input.projectId, dryRun: input.dryRun }, 'importProject');
732
453
  const isDryRun = input.dryRun ?? false;
733
- const payload = ExportSchema.parse(input.payload ?? {});
454
+ const payload = shared_1.ExportSchema.parse(input.payload ?? {});
734
455
  const providerNames = new Set((payload.profiles ?? []).map((p) => p.provider.name.trim().toLowerCase()));
735
- const providers = await this.storage.listProviders();
736
- const available = new Map();
737
- for (const prov of providers.items) {
738
- available.set(prov.name.trim().toLowerCase(), prov.id);
739
- }
740
- const missingProviders = Array.from(providerNames).filter((n) => !available.has(n));
456
+ const { available, missing: missingProviders } = await this.resolveProviders(providerNames);
741
457
  const [existingPrompts, existingProfiles, existingAgents, existingStatuses, existingWatchers, existingSubscribers,] = await Promise.all([
742
458
  this.storage.listPrompts({ projectId: input.projectId, limit: 10000, offset: 0 }),
743
459
  this.storage.listAgentProfiles({ projectId: input.projectId, limit: 10000, offset: 0 }),
@@ -792,16 +508,14 @@ let ProjectsService = class ProjectsService {
792
508
  };
793
509
  }
794
510
  if (missingProviders.length > 0) {
795
- throw new common_1.BadRequestException({
796
- message: 'Import aborted: missing providers',
511
+ throw new error_types_1.ValidationError('Import aborted: missing providers', {
797
512
  missingProviders,
798
513
  hint: 'Install/configure providers by name before importing profiles.',
799
514
  });
800
515
  }
801
516
  const activeSessions = this.sessions.getActiveSessionsForProject(input.projectId);
802
517
  if (activeSessions.length > 0) {
803
- throw new common_1.BadRequestException({
804
- message: 'Import aborted: active agent sessions detected',
518
+ throw new error_types_1.ConflictError('Import aborted: active agent sessions detected', {
805
519
  activeSessions: activeSessions.map((s) => ({ id: s.id, agentId: s.agentId })),
806
520
  hint: 'Terminate all running sessions for this project before importing.',
807
521
  });
@@ -901,45 +615,29 @@ let ProjectsService = class ProjectsService {
901
615
  promptIdMap[p.id] = created.id;
902
616
  createdPrompts.push({ id: created.id, title: created.title });
903
617
  }
904
- const profileNameToNewId = new Map();
905
618
  for (const prof of payload.profiles) {
906
619
  const providerId = available.get(prof.provider.name.trim().toLowerCase());
907
620
  if (!providerId) {
908
- throw new common_1.BadRequestException({ message: `Provider not found: ${prof.provider.name}` });
909
- }
910
- let optionsStr = null;
911
- if (typeof prof.options === 'string') {
912
- optionsStr = prof.options;
913
- }
914
- else if (prof.options && typeof prof.options === 'object') {
915
- try {
916
- optionsStr = JSON.stringify(prof.options);
917
- }
918
- catch {
919
- optionsStr = null;
920
- }
621
+ throw new error_types_1.NotFoundError('Provider', prof.provider.name);
921
622
  }
922
623
  const created = await this.storage.createAgentProfile({
923
624
  projectId: input.projectId,
924
625
  name: prof.name,
925
626
  providerId,
926
- options: optionsStr ?? null,
627
+ options: this.normalizeProfileOptions(prof.options),
927
628
  systemPrompt: null,
928
629
  instructions: prof.instructions ?? null,
929
630
  temperature: prof.temperature ?? null,
930
631
  maxTokens: prof.maxTokens ?? null,
931
632
  });
932
- if (prof.id)
933
- profileIdMap[prof.id] = created.id;
934
- profileNameToNewId.set(prof.name.trim().toLowerCase(), created.id);
633
+ const profKey = prof.id || `name:${prof.name.trim().toLowerCase()}`;
634
+ profileIdMap[profKey] = created.id;
935
635
  }
936
- const agentNameToNewId = new Map();
937
636
  for (const a of payload.agents) {
938
637
  const oldProfileId = a.profileId ?? '';
939
638
  const newProfileId = oldProfileId && profileIdMap[oldProfileId] ? profileIdMap[oldProfileId] : undefined;
940
639
  if (!newProfileId) {
941
- throw new common_1.BadRequestException({
942
- message: `Profile mapping missing for agent ${a.name}`,
640
+ throw new error_types_1.ValidationError(`Profile mapping missing for agent ${a.name}`, {
943
641
  profileId: oldProfileId || null,
944
642
  });
945
643
  }
@@ -949,75 +647,26 @@ let ProjectsService = class ProjectsService {
949
647
  profileId: newProfileId,
950
648
  description: a.description ?? null,
951
649
  });
952
- if (a.id)
953
- agentIdMap[a.id] = created.id;
954
- agentNameToNewId.set(a.name.trim().toLowerCase(), created.id);
955
- }
956
- const watcherIdMap = {};
957
- for (const w of payload.watchers) {
958
- let scopeFilterId = null;
959
- if (w.scopeFilterName && w.scope !== 'all') {
960
- const scopeFilterNameLower = w.scopeFilterName.trim().toLowerCase();
961
- switch (w.scope) {
962
- case 'agent': {
963
- scopeFilterId = agentNameToNewId.get(scopeFilterNameLower) ?? null;
964
- break;
965
- }
966
- case 'profile': {
967
- scopeFilterId = profileNameToNewId.get(scopeFilterNameLower) ?? null;
968
- break;
969
- }
970
- case 'provider': {
971
- const provider = providers.items.find((p) => p.name.trim().toLowerCase() === scopeFilterNameLower);
972
- scopeFilterId = provider?.id ?? null;
973
- break;
974
- }
975
- }
976
- if (!scopeFilterId) {
977
- logger.warn({ watcherName: w.name, scope: w.scope, scopeFilterName: w.scopeFilterName }, 'Could not resolve scope filter, setting scope to "all"');
978
- }
979
- }
980
- const created = await this.storage.createWatcher({
981
- projectId: input.projectId,
982
- name: w.name,
983
- description: w.description ?? null,
984
- enabled: w.enabled,
985
- scope: scopeFilterId ? w.scope : 'all',
986
- scopeFilterId,
987
- pollIntervalMs: w.pollIntervalMs,
988
- viewportLines: w.viewportLines,
989
- condition: w.condition,
990
- cooldownMs: w.cooldownMs,
991
- cooldownMode: w.cooldownMode,
992
- eventName: w.eventName,
993
- });
994
- if (w.id)
995
- watcherIdMap[w.id] = created.id;
996
- if (created.enabled) {
997
- await this.watcherRunner.startWatcher(created);
998
- }
999
- }
1000
- const subscriberIdMap = {};
1001
- for (const s of payload.subscribers) {
1002
- const created = await this.storage.createSubscriber({
1003
- projectId: input.projectId,
1004
- name: s.name,
1005
- description: s.description ?? null,
1006
- enabled: s.enabled,
1007
- eventName: s.eventName,
1008
- eventFilter: s.eventFilter ?? null,
1009
- actionType: s.actionType,
1010
- actionInputs: s.actionInputs,
1011
- delayMs: s.delayMs,
1012
- cooldownMs: s.cooldownMs,
1013
- retryOnError: s.retryOnError,
1014
- groupName: s.groupName ?? null,
1015
- position: s.position ?? 0,
1016
- priority: s.priority ?? 0,
1017
- });
1018
- if (s.id)
1019
- subscriberIdMap[s.id] = created.id;
650
+ const agentKey = a.id || `name:${a.name.trim().toLowerCase()}`;
651
+ agentIdMap[agentKey] = created.id;
1020
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);
1021
670
  logger.info({
1022
671
  watchersCreated: payload.watchers.length,
1023
672
  subscribersCreated: payload.subscribers.length,
@@ -1049,62 +698,31 @@ let ProjectsService = class ProjectsService {
1049
698
  }
1050
699
  }
1051
700
  logger.info({ epicsRemapped, epicsCleared }, 'Epic agent references updated after import');
1052
- let initialPromptSet = false;
1053
- if (payload.initialPrompt) {
1054
- let targetPromptId = null;
1055
- if (payload.initialPrompt.title) {
1056
- const match = createdPrompts.find((cp) => cp.title.trim().toLowerCase() === payload.initialPrompt.title.trim().toLowerCase());
1057
- if (match)
1058
- targetPromptId = match.id;
1059
- }
1060
- if (!targetPromptId && payload.initialPrompt.promptId) {
1061
- const mapped = promptIdMap[payload.initialPrompt.promptId];
1062
- if (mapped)
1063
- targetPromptId = mapped;
1064
- }
1065
- await this.settings.updateSettings({
1066
- projectId: input.projectId,
1067
- initialSessionPromptId: targetPromptId ?? null,
1068
- });
1069
- initialPromptSet = Boolean(targetPromptId);
1070
- }
1071
- if (payload.projectSettings) {
1072
- const ps = payload.projectSettings;
1073
- if (ps.initialPromptTitle) {
1074
- const matchByTitle = createdPrompts.find((cp) => cp.title.toLowerCase() === ps.initialPromptTitle.toLowerCase());
1075
- if (matchByTitle) {
1076
- await this.settings.updateSettings({
1077
- projectId: input.projectId,
1078
- initialSessionPromptId: matchByTitle.id,
1079
- });
1080
- initialPromptSet = true;
1081
- logger.info({ projectId: input.projectId, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
1082
- }
1083
- }
1084
- if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
1085
- const autoCleanStatusIds = ps.autoCleanStatusLabels
1086
- .map((label) => templateLabelToStatusId.get(label.toLowerCase()))
1087
- .filter((id) => !!id);
1088
- if (autoCleanStatusIds.length > 0) {
1089
- const currentSettings = this.settings.getSettings();
1090
- const existingAutoClean = currentSettings.autoClean?.statusIds ?? {};
1091
- await this.settings.updateSettings({
1092
- autoClean: {
1093
- statusIds: { ...existingAutoClean, [input.projectId]: autoCleanStatusIds },
1094
- },
1095
- });
1096
- logger.info({ projectId: input.projectId, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
1097
- }
1098
- }
1099
- if (ps.epicAssignedTemplate) {
1100
- await this.settings.updateSettings({
1101
- events: {
1102
- epicAssigned: { template: ps.epicAssignedTemplate },
1103
- },
1104
- });
1105
- 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;
1106
709
  }
1107
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;
1108
726
  return {
1109
727
  success: true,
1110
728
  mode: 'replace',
@@ -1148,8 +766,210 @@ let ProjectsService = class ProjectsService {
1148
766
  catch (error) {
1149
767
  logger.error({ error, projectId: input.projectId }, 'Import failed');
1150
768
  const message = this.getImportErrorMessage(error);
1151
- 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);
1152
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
+ }
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;
1153
973
  }
1154
974
  getImportErrorMessage(error) {
1155
975
  const errorCode = error?.code;
@@ -1173,6 +993,12 @@ let ProjectsService = class ProjectsService {
1173
993
  }
1174
994
  return 'Import failed: An unexpected error occurred.';
1175
995
  }
996
+ slugify(name) {
997
+ return name
998
+ .toLowerCase()
999
+ .replace(/[^a-z0-9]+/g, '-')
1000
+ .replace(/(^-|-$)/g, '');
1001
+ }
1176
1002
  };
1177
1003
  exports.ProjectsService = ProjectsService;
1178
1004
  exports.ProjectsService = ProjectsService = __decorate([
@@ -1181,6 +1007,7 @@ exports.ProjectsService = ProjectsService = __decorate([
1181
1007
  __metadata("design:paramtypes", [Object, sessions_service_1.SessionsService,
1182
1008
  settings_service_1.SettingsService,
1183
1009
  watchers_service_1.WatchersService,
1184
- watcher_runner_service_1.WatcherRunnerService])
1010
+ watcher_runner_service_1.WatcherRunnerService,
1011
+ unified_template_service_1.UnifiedTemplateService])
1185
1012
  ], ProjectsService);
1186
1013
  //# sourceMappingURL=projects.service.js.map