devchain-cli 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/cli.js +184 -16
  2. package/dist/drizzle/0017_agents_description.sql +1 -0
  3. package/dist/drizzle/meta/_journal.json +7 -0
  4. package/dist/server/common/filters/http-exception.filter.js +3 -0
  5. package/dist/server/common/filters/http-exception.filter.js.map +1 -1
  6. package/dist/server/common/filters/ws-exception.filter.js +1 -1
  7. package/dist/server/common/filters/ws-exception.filter.js.map +1 -1
  8. package/dist/server/main.js +18 -1
  9. package/dist/server/main.js.map +1 -1
  10. package/dist/server/modules/agents/controllers/agents.controller.js +2 -0
  11. package/dist/server/modules/agents/controllers/agents.controller.js.map +1 -1
  12. package/dist/server/modules/epics/epics.module.js +2 -1
  13. package/dist/server/modules/epics/epics.module.js.map +1 -1
  14. package/dist/server/modules/epics/services/epics.service.d.ts +6 -1
  15. package/dist/server/modules/epics/services/epics.service.js +48 -3
  16. package/dist/server/modules/epics/services/epics.service.js.map +1 -1
  17. package/dist/server/modules/events/services/events.service.js +7 -0
  18. package/dist/server/modules/events/services/events.service.js.map +1 -1
  19. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.d.ts +3 -1
  20. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.js +8 -4
  21. package/dist/server/modules/events/subscribers/epic-assignment-notifier.subscriber.js.map +1 -1
  22. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +1 -0
  23. package/dist/server/modules/mcp/dtos/mcp.dto.js.map +1 -1
  24. package/dist/server/modules/mcp/services/mcp-provider-registration.service.d.ts +6 -2
  25. package/dist/server/modules/mcp/services/mcp-provider-registration.service.js +32 -2
  26. package/dist/server/modules/mcp/services/mcp-provider-registration.service.js.map +1 -1
  27. package/dist/server/modules/mcp/services/mcp.service.js +2 -0
  28. package/dist/server/modules/mcp/services/mcp.service.js.map +1 -1
  29. package/dist/server/modules/projects/controllers/projects.controller.d.ts +28 -1
  30. package/dist/server/modules/projects/controllers/projects.controller.js +7 -1
  31. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  32. package/dist/server/modules/projects/services/projects.service.d.ts +26 -0
  33. package/dist/server/modules/projects/services/projects.service.js +388 -119
  34. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  35. package/dist/server/modules/providers/controllers/providers.controller.d.ts +1 -0
  36. package/dist/server/modules/providers/controllers/providers.controller.js +33 -0
  37. package/dist/server/modules/providers/controllers/providers.controller.js.map +1 -1
  38. package/dist/server/modules/sessions/services/session-coordinator.service.d.ts +4 -0
  39. package/dist/server/modules/sessions/services/session-coordinator.service.js +31 -0
  40. package/dist/server/modules/sessions/services/session-coordinator.service.js.map +1 -0
  41. package/dist/server/modules/sessions/services/sessions.service.d.ts +1 -0
  42. package/dist/server/modules/sessions/services/sessions.service.js +27 -0
  43. package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
  44. package/dist/server/modules/sessions/sessions.module.js +3 -2
  45. package/dist/server/modules/sessions/sessions.module.js.map +1 -1
  46. package/dist/server/modules/settings/controllers/settings.controller.d.ts +3 -0
  47. package/dist/server/modules/settings/controllers/settings.controller.js +27 -0
  48. package/dist/server/modules/settings/controllers/settings.controller.js.map +1 -1
  49. package/dist/server/modules/settings/dtos/settings.dto.d.ts +13 -0
  50. package/dist/server/modules/settings/dtos/settings.dto.js +4 -0
  51. package/dist/server/modules/settings/dtos/settings.dto.js.map +1 -1
  52. package/dist/server/modules/settings/services/settings.service.d.ts +16 -0
  53. package/dist/server/modules/settings/services/settings.service.js +74 -1
  54. package/dist/server/modules/settings/services/settings.service.js.map +1 -1
  55. package/dist/server/modules/storage/db/schema.d.ts +19 -0
  56. package/dist/server/modules/storage/db/schema.js +1 -0
  57. package/dist/server/modules/storage/db/schema.js.map +1 -1
  58. package/dist/server/modules/storage/interfaces/storage.interface.d.ts +2 -0
  59. package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
  60. package/dist/server/modules/storage/local/local-storage.service.d.ts +2 -0
  61. package/dist/server/modules/storage/local/local-storage.service.js +50 -1
  62. package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
  63. package/dist/server/modules/storage/models/domain.models.d.ts +4 -1
  64. package/dist/server/templates/claude-opus.json +146 -0
  65. package/dist/server/templates/codex-claude.json +178 -0
  66. package/dist/server/templates/simple-codex.json +146 -0
  67. package/dist/server/tsconfig.tsbuildinfo +1 -1
  68. package/dist/server/ui/assets/index-5Xb7jFMJ.js +641 -0
  69. package/dist/server/ui/assets/index-CbYIbCQV.css +32 -0
  70. package/dist/server/ui/favicon.svg +17 -0
  71. package/dist/server/ui/index.html +3 -2
  72. package/dist/templates/claude-opus.json +146 -0
  73. package/dist/templates/codex-claude.json +178 -0
  74. package/dist/templates/simple-codex.json +20 -8
  75. package/package.json +4 -2
  76. package/dist/server/ui/assets/index-DFChYYFN.css +0 -32
  77. package/dist/server/ui/assets/index-DpXRypHy.js +0 -636
@@ -55,6 +55,7 @@ const ExportSchema = zod_1.z
55
55
  id: zod_1.z.string().uuid().optional(),
56
56
  name: zod_1.z.string().min(1),
57
57
  profileId: zod_1.z.string().uuid().optional(),
58
+ description: zod_1.z.string().nullable().optional(),
58
59
  }))
59
60
  .optional()
60
61
  .default([]),
@@ -71,6 +72,13 @@ const ExportSchema = zod_1.z
71
72
  .object({ promptId: zod_1.z.string().uuid().optional(), title: zod_1.z.string().optional() })
72
73
  .nullable()
73
74
  .optional(),
75
+ projectSettings: zod_1.z
76
+ .object({
77
+ initialPromptTitle: zod_1.z.string().optional(),
78
+ autoCleanStatusLabels: zod_1.z.array(zod_1.z.string()).optional(),
79
+ epicAssignedTemplate: zod_1.z.string().optional(),
80
+ })
81
+ .optional(),
74
82
  })
75
83
  .strict();
76
84
  let ProjectsService = class ProjectsService {
@@ -230,6 +238,7 @@ let ProjectsService = class ProjectsService {
230
238
  id: a.id,
231
239
  name: a.name,
232
240
  profileId: a.profileId,
241
+ description: a.description,
233
242
  })),
234
243
  statuses: payload.statuses.map((s) => ({
235
244
  id: s.id,
@@ -266,6 +275,76 @@ let ProjectsService = class ProjectsService {
266
275
  });
267
276
  initialPromptSet = Boolean(targetPromptId);
268
277
  }
278
+ if (payload.projectSettings) {
279
+ const ps = payload.projectSettings;
280
+ if (ps.initialPromptTitle) {
281
+ const promptByTitle = payload.prompts.find((p) => p.title.toLowerCase() === ps.initialPromptTitle.toLowerCase());
282
+ if (promptByTitle?.id) {
283
+ const mappedPromptId = result.mappings.promptIdMap[promptByTitle.id];
284
+ if (mappedPromptId) {
285
+ await this.settings.updateSettings({
286
+ projectId: result.project.id,
287
+ initialSessionPromptId: mappedPromptId,
288
+ });
289
+ initialPromptSet = true;
290
+ logger.info({ projectId: result.project.id, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
291
+ }
292
+ }
293
+ }
294
+ if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
295
+ const statusLabelMap = new Map(templatePayload.statuses.map((s) => [s.label.toLowerCase(), s.id]));
296
+ const autoCleanStatusIds = ps.autoCleanStatusLabels
297
+ .map((label) => {
298
+ const templateId = statusLabelMap.get(label.toLowerCase());
299
+ return templateId ? result.mappings.statusIdMap[templateId] : undefined;
300
+ })
301
+ .filter((id) => !!id);
302
+ if (autoCleanStatusIds.length > 0) {
303
+ await this.settings.updateSettings({
304
+ autoClean: {
305
+ statusIds: { [result.project.id]: autoCleanStatusIds },
306
+ },
307
+ });
308
+ logger.info({ projectId: result.project.id, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
309
+ }
310
+ }
311
+ else {
312
+ const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
313
+ if (archiveTemplateStatus?.id) {
314
+ const archiveNewId = result.mappings.statusIdMap[archiveTemplateStatus.id];
315
+ if (archiveNewId) {
316
+ await this.settings.updateSettings({
317
+ autoClean: {
318
+ statusIds: { [result.project.id]: [archiveNewId] },
319
+ },
320
+ });
321
+ logger.info({ projectId: result.project.id, archiveStatusId: archiveNewId }, 'Auto-configured Archive status for auto-clean (fallback)');
322
+ }
323
+ }
324
+ }
325
+ if (ps.epicAssignedTemplate) {
326
+ await this.settings.updateSettings({
327
+ events: {
328
+ epicAssigned: { template: ps.epicAssignedTemplate },
329
+ },
330
+ });
331
+ logger.info({ projectId: result.project.id }, 'Applied epicAssigned template from projectSettings');
332
+ }
333
+ }
334
+ else {
335
+ const archiveTemplateStatus = templatePayload.statuses.find((s) => s.label.toLowerCase() === 'archive');
336
+ if (archiveTemplateStatus?.id) {
337
+ const archiveNewId = result.mappings.statusIdMap[archiveTemplateStatus.id];
338
+ if (archiveNewId) {
339
+ await this.settings.updateSettings({
340
+ autoClean: {
341
+ statusIds: { [result.project.id]: [archiveNewId] },
342
+ },
343
+ });
344
+ logger.info({ projectId: result.project.id, archiveStatusId: archiveNewId }, 'Auto-configured Archive status for auto-clean');
345
+ }
346
+ }
347
+ }
269
348
  return {
270
349
  success: true,
271
350
  project: result.project,
@@ -277,12 +356,13 @@ let ProjectsService = class ProjectsService {
277
356
  }
278
357
  async exportProject(projectId) {
279
358
  logger.info({ projectId }, 'exportProject');
280
- const [promptsRes, profilesRes, agentsRes, statusesRes, initialPrompt] = await Promise.all([
359
+ const [promptsRes, profilesRes, agentsRes, statusesRes, initialPrompt, settings] = await Promise.all([
281
360
  this.storage.listPrompts(projectId, { limit: 1000, offset: 0 }),
282
361
  this.storage.listAgentProfiles({ projectId, limit: 1000, offset: 0 }),
283
362
  this.storage.listAgents(projectId, { limit: 1000, offset: 0 }),
284
363
  this.storage.listStatuses(projectId, { limit: 1000, offset: 0 }),
285
364
  this.storage.getInitialSessionPrompt(projectId),
365
+ Promise.resolve(this.settings.getSettings()),
286
366
  ]);
287
367
  const secretKeys = new Set([
288
368
  'apikey',
@@ -349,13 +429,36 @@ let ProjectsService = class ProjectsService {
349
429
  maxTokens: prof.maxTokens,
350
430
  };
351
431
  }));
352
- const agents = agentsRes.items.map((a) => ({ id: a.id, name: a.name, profileId: a.profileId }));
432
+ const agents = agentsRes.items.map((a) => ({
433
+ id: a.id,
434
+ name: a.name,
435
+ profileId: a.profileId,
436
+ description: a.description,
437
+ }));
353
438
  const statuses = statusesRes.items.map((s) => ({
354
439
  id: s.id,
355
440
  label: s.label,
356
441
  color: s.color,
357
442
  position: s.position,
358
443
  }));
444
+ const projectSettings = {};
445
+ if (initialPrompt?.title) {
446
+ projectSettings.initialPromptTitle = initialPrompt.title;
447
+ }
448
+ const autoCleanStatusIds = settings.autoClean?.statusIds?.[projectId] ?? [];
449
+ if (autoCleanStatusIds.length > 0) {
450
+ const statusMap = new Map(statusesRes.items.map((s) => [s.id, s.label]));
451
+ const autoCleanLabels = autoCleanStatusIds
452
+ .map((id) => statusMap.get(id))
453
+ .filter((label) => !!label);
454
+ if (autoCleanLabels.length > 0) {
455
+ projectSettings.autoCleanStatusLabels = autoCleanLabels;
456
+ }
457
+ }
458
+ const epicAssignedTemplate = settings.events?.epicAssigned?.template;
459
+ if (epicAssignedTemplate) {
460
+ projectSettings.epicAssignedTemplate = epicAssignedTemplate;
461
+ }
359
462
  const exportPayload = {
360
463
  version: 1,
361
464
  exportedAt: new Date().toISOString(),
@@ -366,6 +469,7 @@ let ProjectsService = class ProjectsService {
366
469
  initialPrompt: initialPrompt
367
470
  ? { promptId: initialPrompt.id, title: initialPrompt.title }
368
471
  : null,
472
+ ...(Object.keys(projectSettings).length > 0 && { projectSettings }),
369
473
  };
370
474
  return exportPayload;
371
475
  }
@@ -386,10 +490,31 @@ let ProjectsService = class ProjectsService {
386
490
  this.storage.listAgents(input.projectId, { limit: 10000, offset: 0 }),
387
491
  this.storage.listStatuses(input.projectId, { limit: 10000, offset: 0 }),
388
492
  ]);
493
+ const templateStatusLabels = new Set(payload.statuses.map((s) => s.label.trim().toLowerCase()));
494
+ const unmatchedStatuses = [];
495
+ for (const s of existingStatuses.items) {
496
+ const labelKey = s.label.trim().toLowerCase();
497
+ if (!templateStatusLabels.has(labelKey)) {
498
+ const epicCount = await this.storage.countEpicsByStatus(s.id);
499
+ if (epicCount > 0) {
500
+ unmatchedStatuses.push({
501
+ id: s.id,
502
+ label: s.label,
503
+ color: s.color,
504
+ epicCount,
505
+ });
506
+ }
507
+ }
508
+ }
389
509
  if (isDryRun) {
390
510
  return {
391
511
  dryRun: true,
392
512
  missingProviders,
513
+ unmatchedStatuses,
514
+ templateStatuses: payload.statuses.map((s) => ({
515
+ label: s.label,
516
+ color: s.color,
517
+ })),
393
518
  counts: {
394
519
  toImport: {
395
520
  prompts: payload.prompts.length,
@@ -413,7 +538,7 @@ let ProjectsService = class ProjectsService {
413
538
  hint: 'Install/configure providers by name before importing profiles.',
414
539
  });
415
540
  }
416
- const activeSessions = await this.sessions.listActiveSessions(input.projectId);
541
+ const activeSessions = this.sessions.getActiveSessionsForProject(input.projectId);
417
542
  if (activeSessions.length > 0) {
418
543
  throw new common_1.BadRequestException({
419
544
  message: 'Import aborted: active agent sessions detected',
@@ -421,137 +546,281 @@ let ProjectsService = class ProjectsService {
421
546
  hint: 'Terminate all running sessions for this project before importing.',
422
547
  });
423
548
  }
424
- for (const a of existingAgents.items) {
425
- await this.storage.deleteAgent(a.id);
426
- }
427
- for (const p of existingProfiles.items) {
428
- await this.storage.deleteAgentProfile(p.id);
429
- }
430
- for (const pr of existingPrompts.items) {
431
- await this.storage.deletePrompt(pr.id);
432
- }
433
- for (const s of existingStatuses.items) {
434
- await this.storage.deleteStatus(s.id);
435
- }
436
- await this.settings.updateSettings({
437
- projectId: input.projectId,
438
- initialSessionPromptId: null,
439
- });
440
- const statusIdMap = {};
441
- const promptIdMap = {};
442
- const profileIdMap = {};
443
- const agentIdMap = {};
444
- for (const s of payload.statuses.sort((a, b) => a.position - b.position)) {
445
- const created = await this.storage.createStatus({
549
+ try {
550
+ const oldAgentIdToName = new Map();
551
+ for (const a of existingAgents.items) {
552
+ oldAgentIdToName.set(a.id, a.name.trim().toLowerCase());
553
+ }
554
+ for (const a of existingAgents.items) {
555
+ await this.storage.deleteAgent(a.id);
556
+ }
557
+ for (const p of existingProfiles.items) {
558
+ await this.storage.deleteAgentProfile(p.id);
559
+ }
560
+ for (const pr of existingPrompts.items) {
561
+ await this.storage.deletePrompt(pr.id);
562
+ }
563
+ await this.settings.updateSettings({
446
564
  projectId: input.projectId,
447
- label: s.label,
448
- color: s.color,
449
- position: s.position,
565
+ initialSessionPromptId: null,
450
566
  });
451
- if (s.id)
452
- statusIdMap[s.id] = created.id;
453
- }
454
- const createdPrompts = [];
455
- for (const p of payload.prompts) {
456
- const created = await this.storage.createPrompt({
457
- projectId: input.projectId,
458
- title: p.title,
459
- content: p.content,
460
- tags: p.tags ?? [],
567
+ const statusIdMap = {};
568
+ const promptIdMap = {};
569
+ const profileIdMap = {};
570
+ const agentIdMap = {};
571
+ const existingStatusByLabel = new Map();
572
+ for (const s of existingStatuses.items) {
573
+ existingStatusByLabel.set(s.label.trim().toLowerCase(), s);
574
+ }
575
+ for (const s of payload.statuses.sort((a, b) => a.position - b.position)) {
576
+ const labelKey = s.label.trim().toLowerCase();
577
+ const existing = existingStatusByLabel.get(labelKey);
578
+ if (existing) {
579
+ const updated = await this.storage.updateStatus(existing.id, {
580
+ color: s.color,
581
+ position: s.position,
582
+ });
583
+ if (s.id)
584
+ statusIdMap[s.id] = updated.id;
585
+ existingStatusByLabel.delete(labelKey);
586
+ }
587
+ else {
588
+ const created = await this.storage.createStatus({
589
+ projectId: input.projectId,
590
+ label: s.label,
591
+ color: s.color,
592
+ position: s.position,
593
+ });
594
+ if (s.id)
595
+ statusIdMap[s.id] = created.id;
596
+ }
597
+ }
598
+ const templateLabelToStatusId = new Map();
599
+ const allStatuses = await this.storage.listStatuses(input.projectId, {
600
+ limit: 10000,
601
+ offset: 0,
461
602
  });
462
- if (p.id)
463
- promptIdMap[p.id] = created.id;
464
- createdPrompts.push({ id: created.id, title: created.title });
465
- }
466
- for (const prof of payload.profiles) {
467
- const providerId = available.get(prof.provider.name.trim().toLowerCase());
468
- if (!providerId) {
469
- throw new common_1.BadRequestException({ message: `Provider not found: ${prof.provider.name}` });
603
+ for (const s of allStatuses.items) {
604
+ templateLabelToStatusId.set(s.label.trim().toLowerCase(), s.id);
605
+ }
606
+ if (input.statusMappings && Object.keys(input.statusMappings).length > 0) {
607
+ let epicsMapped = 0;
608
+ let statusesDeleted = 0;
609
+ for (const [oldStatusId, targetLabel] of Object.entries(input.statusMappings)) {
610
+ const targetStatusId = templateLabelToStatusId.get(targetLabel.trim().toLowerCase());
611
+ if (targetStatusId) {
612
+ const remapped = await this.storage.updateEpicsStatus(oldStatusId, targetStatusId);
613
+ epicsMapped += remapped;
614
+ await this.storage.deleteStatus(oldStatusId);
615
+ statusesDeleted++;
616
+ }
617
+ }
618
+ logger.info({ epicsMapped, statusesDeleted }, 'Applied status mappings: epics remapped and old statuses deleted');
470
619
  }
471
- let optionsStr = null;
472
- if (typeof prof.options === 'string') {
473
- optionsStr = prof.options;
620
+ const createdPrompts = [];
621
+ for (const p of payload.prompts) {
622
+ const created = await this.storage.createPrompt({
623
+ projectId: input.projectId,
624
+ title: p.title,
625
+ content: p.content,
626
+ tags: p.tags ?? [],
627
+ });
628
+ if (p.id)
629
+ promptIdMap[p.id] = created.id;
630
+ createdPrompts.push({ id: created.id, title: created.title });
631
+ }
632
+ for (const prof of payload.profiles) {
633
+ const providerId = available.get(prof.provider.name.trim().toLowerCase());
634
+ if (!providerId) {
635
+ throw new common_1.BadRequestException({ message: `Provider not found: ${prof.provider.name}` });
636
+ }
637
+ let optionsStr = null;
638
+ if (typeof prof.options === 'string') {
639
+ optionsStr = prof.options;
640
+ }
641
+ else if (prof.options && typeof prof.options === 'object') {
642
+ try {
643
+ optionsStr = JSON.stringify(prof.options);
644
+ }
645
+ catch {
646
+ optionsStr = null;
647
+ }
648
+ }
649
+ const created = await this.storage.createAgentProfile({
650
+ projectId: input.projectId,
651
+ name: prof.name,
652
+ providerId,
653
+ options: optionsStr ?? null,
654
+ systemPrompt: null,
655
+ instructions: prof.instructions ?? null,
656
+ temperature: prof.temperature ?? null,
657
+ maxTokens: prof.maxTokens ?? null,
658
+ });
659
+ if (prof.id)
660
+ profileIdMap[prof.id] = created.id;
474
661
  }
475
- else if (prof.options && typeof prof.options === 'object') {
476
- try {
477
- optionsStr = JSON.stringify(prof.options);
662
+ for (const a of payload.agents) {
663
+ const oldProfileId = a.profileId ?? '';
664
+ const newProfileId = oldProfileId && profileIdMap[oldProfileId] ? profileIdMap[oldProfileId] : undefined;
665
+ if (!newProfileId) {
666
+ throw new common_1.BadRequestException({
667
+ message: `Profile mapping missing for agent ${a.name}`,
668
+ profileId: oldProfileId || null,
669
+ });
478
670
  }
479
- catch {
480
- optionsStr = null;
671
+ const created = await this.storage.createAgent({
672
+ projectId: input.projectId,
673
+ name: a.name,
674
+ profileId: newProfileId,
675
+ description: a.description ?? null,
676
+ });
677
+ if (a.id)
678
+ agentIdMap[a.id] = created.id;
679
+ }
680
+ const newAgentByName = new Map();
681
+ for (const a of payload.agents) {
682
+ const created = agentIdMap[a.id ?? ''];
683
+ if (created) {
684
+ newAgentByName.set(a.name.trim().toLowerCase(), created);
481
685
  }
482
686
  }
483
- const created = await this.storage.createAgentProfile({
484
- projectId: input.projectId,
485
- name: prof.name,
486
- providerId,
487
- options: optionsStr ?? null,
488
- systemPrompt: null,
489
- instructions: prof.instructions ?? null,
490
- temperature: prof.temperature ?? null,
491
- maxTokens: prof.maxTokens ?? null,
687
+ const existingEpics = await this.storage.listEpics(input.projectId, {
688
+ limit: 100000,
689
+ offset: 0,
492
690
  });
493
- if (prof.id)
494
- profileIdMap[prof.id] = created.id;
495
- }
496
- for (const a of payload.agents) {
497
- const oldProfileId = a.profileId ?? '';
498
- const newProfileId = oldProfileId && profileIdMap[oldProfileId] ? profileIdMap[oldProfileId] : undefined;
499
- if (!newProfileId) {
500
- throw new common_1.BadRequestException({
501
- message: `Profile mapping missing for agent ${a.name}`,
502
- profileId: oldProfileId || null,
691
+ let epicsRemapped = 0;
692
+ let epicsCleared = 0;
693
+ for (const epic of existingEpics.items) {
694
+ if (epic.agentId) {
695
+ const oldAgentName = oldAgentIdToName.get(epic.agentId);
696
+ if (oldAgentName) {
697
+ const newAgentId = newAgentByName.get(oldAgentName);
698
+ if (newAgentId) {
699
+ await this.storage.updateEpic(epic.id, { agentId: newAgentId }, epic.version);
700
+ epicsRemapped++;
701
+ }
702
+ else {
703
+ await this.storage.updateEpic(epic.id, { agentId: null }, epic.version);
704
+ epicsCleared++;
705
+ }
706
+ }
707
+ else {
708
+ await this.storage.updateEpic(epic.id, { agentId: null }, epic.version);
709
+ epicsCleared++;
710
+ }
711
+ }
712
+ }
713
+ logger.info({ epicsRemapped, epicsCleared }, 'Epic agent references updated after import');
714
+ let initialPromptSet = false;
715
+ if (payload.initialPrompt) {
716
+ let targetPromptId = null;
717
+ if (payload.initialPrompt.title) {
718
+ const match = createdPrompts.find((cp) => cp.title.trim().toLowerCase() === payload.initialPrompt.title.trim().toLowerCase());
719
+ if (match)
720
+ targetPromptId = match.id;
721
+ }
722
+ if (!targetPromptId && payload.initialPrompt.promptId) {
723
+ const mapped = promptIdMap[payload.initialPrompt.promptId];
724
+ if (mapped)
725
+ targetPromptId = mapped;
726
+ }
727
+ await this.settings.updateSettings({
728
+ projectId: input.projectId,
729
+ initialSessionPromptId: targetPromptId ?? null,
503
730
  });
731
+ initialPromptSet = Boolean(targetPromptId);
504
732
  }
505
- const created = await this.storage.createAgent({
506
- projectId: input.projectId,
507
- name: a.name,
508
- profileId: newProfileId,
509
- });
510
- if (a.id)
511
- agentIdMap[a.id] = created.id;
733
+ if (payload.projectSettings) {
734
+ const ps = payload.projectSettings;
735
+ if (ps.initialPromptTitle) {
736
+ const matchByTitle = createdPrompts.find((cp) => cp.title.toLowerCase() === ps.initialPromptTitle.toLowerCase());
737
+ if (matchByTitle) {
738
+ await this.settings.updateSettings({
739
+ projectId: input.projectId,
740
+ initialSessionPromptId: matchByTitle.id,
741
+ });
742
+ initialPromptSet = true;
743
+ logger.info({ projectId: input.projectId, promptTitle: ps.initialPromptTitle }, 'Applied initial prompt from projectSettings');
744
+ }
745
+ }
746
+ if (ps.autoCleanStatusLabels && ps.autoCleanStatusLabels.length > 0) {
747
+ const autoCleanStatusIds = ps.autoCleanStatusLabels
748
+ .map((label) => templateLabelToStatusId.get(label.toLowerCase()))
749
+ .filter((id) => !!id);
750
+ if (autoCleanStatusIds.length > 0) {
751
+ await this.settings.updateSettings({
752
+ autoClean: {
753
+ statusIds: { [input.projectId]: autoCleanStatusIds },
754
+ },
755
+ });
756
+ logger.info({ projectId: input.projectId, autoCleanStatusIds }, 'Applied autoClean statuses from projectSettings');
757
+ }
758
+ }
759
+ if (ps.epicAssignedTemplate) {
760
+ await this.settings.updateSettings({
761
+ events: {
762
+ epicAssigned: { template: ps.epicAssignedTemplate },
763
+ },
764
+ });
765
+ logger.info({ projectId: input.projectId }, 'Applied epicAssigned template from projectSettings');
766
+ }
767
+ }
768
+ return {
769
+ success: true,
770
+ mode: 'replace',
771
+ replaced: true,
772
+ missingProviders: [],
773
+ counts: {
774
+ imported: {
775
+ prompts: payload.prompts.length,
776
+ profiles: payload.profiles.length,
777
+ agents: payload.agents.length,
778
+ statuses: payload.statuses.length,
779
+ },
780
+ deleted: {
781
+ prompts: existingPrompts.total,
782
+ profiles: existingProfiles.total,
783
+ agents: existingAgents.total,
784
+ statuses: 0,
785
+ },
786
+ epics: {
787
+ preserved: existingEpics.total,
788
+ agentRemapped: epicsRemapped,
789
+ agentCleared: epicsCleared,
790
+ },
791
+ },
792
+ mappings: { promptIdMap, profileIdMap, agentIdMap, statusIdMap },
793
+ initialPromptSet,
794
+ message: 'Project configuration replaced. Epics preserved.',
795
+ };
512
796
  }
513
- let initialPromptSet = false;
514
- if (payload.initialPrompt) {
515
- let targetPromptId = null;
516
- if (payload.initialPrompt.title) {
517
- const match = createdPrompts.find((cp) => cp.title.trim().toLowerCase() === payload.initialPrompt.title.trim().toLowerCase());
518
- if (match)
519
- targetPromptId = match.id;
797
+ catch (error) {
798
+ logger.error({ error, projectId: input.projectId }, 'Import failed');
799
+ const message = this.getImportErrorMessage(error);
800
+ throw new common_1.BadRequestException({ message });
801
+ }
802
+ }
803
+ getImportErrorMessage(error) {
804
+ const errorCode = error?.code;
805
+ if (errorCode === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
806
+ return 'Import failed: Cannot delete items that are still referenced. Check for cross-project agents using these profiles.';
807
+ }
808
+ if (errorCode === 'SQLITE_CONSTRAINT_UNIQUE') {
809
+ return 'Import failed: Duplicate entry detected.';
810
+ }
811
+ if (error instanceof Error) {
812
+ if (error.message.includes('FOREIGN KEY constraint failed')) {
813
+ return 'Import failed: Cannot delete items that are still referenced. Check for cross-project agents using these profiles.';
520
814
  }
521
- if (!targetPromptId && payload.initialPrompt.promptId) {
522
- const mapped = promptIdMap[payload.initialPrompt.promptId];
523
- if (mapped)
524
- targetPromptId = mapped;
815
+ if (error.message.includes('UNIQUE constraint failed')) {
816
+ return 'Import failed: Duplicate entry detected.';
525
817
  }
526
- await this.settings.updateSettings({
527
- projectId: input.projectId,
528
- initialSessionPromptId: targetPromptId ?? null,
529
- });
530
- initialPromptSet = Boolean(targetPromptId);
818
+ if (error.message.startsWith('Import failed')) {
819
+ return error.message;
820
+ }
821
+ return `Import failed: ${error.message}`;
531
822
  }
532
- return {
533
- success: true,
534
- mode: 'replace',
535
- replaced: true,
536
- missingProviders: [],
537
- counts: {
538
- imported: {
539
- prompts: payload.prompts.length,
540
- profiles: payload.profiles.length,
541
- agents: payload.agents.length,
542
- statuses: payload.statuses.length,
543
- },
544
- deleted: {
545
- prompts: existingPrompts.total,
546
- profiles: existingProfiles.total,
547
- agents: existingAgents.total,
548
- statuses: existingStatuses.total,
549
- },
550
- },
551
- mappings: { promptIdMap, profileIdMap, agentIdMap, statusIdMap },
552
- initialPromptSet,
553
- message: 'Project configuration replaced.',
554
- };
823
+ return 'Import failed: An unexpected error occurred.';
555
824
  }
556
825
  };
557
826
  exports.ProjectsService = ProjectsService;