crewx 0.9.0-rc.73 → 0.9.0-rc.74

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.
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapThreadLabelValidationError = mapThreadLabelValidationError;
4
+ exports.createServerValidationPipe = createServerValidationPipe;
5
+ const common_1 = require("@nestjs/common");
6
+ const THREAD_LABEL_DTO_NAMES = new Set([
7
+ 'ThreadLabelListQueryDto',
8
+ 'CreateThreadLabelDto',
9
+ 'UpdateThreadLabelDto',
10
+ 'ArchiveThreadLabelDto',
11
+ ]);
12
+ const SERVER_VALIDATION_PIPE_OPTIONS = {
13
+ whitelist: true,
14
+ transform: true,
15
+ };
16
+ // Delegate non-thread-label errors to Nest's own factory so nested paths and envelopes stay unchanged.
17
+ const defaultExceptionFactory = new common_1.ValidationPipe(SERVER_VALIDATION_PIPE_OPTIONS).createExceptionFactory();
18
+ function isThreadLabelValidationError(error) {
19
+ const target = error.target;
20
+ if (!target || typeof target !== 'object')
21
+ return false;
22
+ const constructorName = target.constructor?.name;
23
+ return typeof constructorName === 'string' && THREAD_LABEL_DTO_NAMES.has(constructorName);
24
+ }
25
+ function hasConstraint(errors, properties, constraint) {
26
+ return errors.some((error) => isThreadLabelValidationError(error)
27
+ && properties.includes(error.property)
28
+ && Boolean(error.constraints?.[constraint]));
29
+ }
30
+ /** Translate only thread-label DTO type errors; other DTOs keep Nest's normal messages. */
31
+ function mapThreadLabelValidationError(errors) {
32
+ if (hasConstraint(errors, ['kind'], 'isString'))
33
+ return 'THREAD_LABEL_KIND_INVALID';
34
+ if (hasConstraint(errors, ['name', 'nameOverride'], 'isString'))
35
+ return 'THREAD_LABEL_INVALID_NAME';
36
+ if (hasConstraint(errors, ['color'], 'isString'))
37
+ return 'THREAD_LABEL_INVALID_COLOR';
38
+ return undefined;
39
+ }
40
+ /** The application-wide pipe, including stable thread-label validation codes. */
41
+ function createServerValidationPipe() {
42
+ return new common_1.ValidationPipe({
43
+ ...SERVER_VALIDATION_PIPE_OPTIONS,
44
+ exceptionFactory: (errors) => {
45
+ const machineCode = mapThreadLabelValidationError(errors);
46
+ return machineCode
47
+ ? new common_1.BadRequestException(machineCode)
48
+ : defaultExceptionFactory(errors);
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ArchiveThreadLabelDto = exports.UpdateThreadLabelDto = exports.CreateThreadLabelDto = exports.ThreadLabelListQueryDto = void 0;
13
+ const swagger_1 = require("@nestjs/swagger");
14
+ const class_transformer_1 = require("class-transformer");
15
+ const class_validator_1 = require("class-validator");
16
+ const repository_1 = require("@crewx/sdk/repository");
17
+ class ThreadLabelListQueryDto {
18
+ kind;
19
+ includeArchived;
20
+ }
21
+ exports.ThreadLabelListQueryDto = ThreadLabelListQueryDto;
22
+ __decorate([
23
+ (0, swagger_1.ApiProperty)({ enum: ['state', 'priority'] }),
24
+ (0, class_validator_1.IsString)(),
25
+ __metadata("design:type", String)
26
+ ], ThreadLabelListQueryDto.prototype, "kind", void 0);
27
+ __decorate([
28
+ (0, swagger_1.ApiPropertyOptional)({ default: false }),
29
+ (0, class_validator_1.IsOptional)(),
30
+ (0, class_transformer_1.Transform)(({ value }) => {
31
+ if (typeof value === 'boolean')
32
+ return value;
33
+ if (value === 'true')
34
+ return true;
35
+ if (value === 'false')
36
+ return false;
37
+ return value;
38
+ }),
39
+ (0, class_validator_1.IsBoolean)(),
40
+ __metadata("design:type", Boolean)
41
+ ], ThreadLabelListQueryDto.prototype, "includeArchived", void 0);
42
+ class CreateThreadLabelDto {
43
+ name;
44
+ color;
45
+ }
46
+ exports.CreateThreadLabelDto = CreateThreadLabelDto;
47
+ __decorate([
48
+ (0, swagger_1.ApiProperty)(),
49
+ (0, class_validator_1.IsOptional)(),
50
+ (0, class_validator_1.IsString)(),
51
+ __metadata("design:type", String)
52
+ ], CreateThreadLabelDto.prototype, "name", void 0);
53
+ __decorate([
54
+ (0, swagger_1.ApiProperty)({ enum: repository_1.COMMON_CODE_COLORS }),
55
+ (0, class_validator_1.IsOptional)(),
56
+ (0, class_validator_1.IsString)(),
57
+ __metadata("design:type", String)
58
+ ], CreateThreadLabelDto.prototype, "color", void 0);
59
+ class UpdateThreadLabelDto {
60
+ nameOverride;
61
+ color;
62
+ }
63
+ exports.UpdateThreadLabelDto = UpdateThreadLabelDto;
64
+ __decorate([
65
+ (0, swagger_1.ApiPropertyOptional)({ nullable: true }),
66
+ (0, class_validator_1.IsOptional)(),
67
+ (0, class_validator_1.IsString)(),
68
+ __metadata("design:type", Object)
69
+ ], UpdateThreadLabelDto.prototype, "nameOverride", void 0);
70
+ __decorate([
71
+ (0, swagger_1.ApiPropertyOptional)({ enum: repository_1.COMMON_CODE_COLORS }),
72
+ (0, class_validator_1.IsOptional)(),
73
+ (0, class_validator_1.IsString)(),
74
+ __metadata("design:type", String)
75
+ ], UpdateThreadLabelDto.prototype, "color", void 0);
76
+ class ArchiveThreadLabelDto {
77
+ archived;
78
+ }
79
+ exports.ArchiveThreadLabelDto = ArchiveThreadLabelDto;
80
+ __decorate([
81
+ (0, swagger_1.ApiProperty)(),
82
+ (0, class_transformer_1.Transform)(({ value }) => {
83
+ if (typeof value === 'boolean')
84
+ return value;
85
+ if (value === 'true')
86
+ return true;
87
+ if (value === 'false')
88
+ return false;
89
+ return value;
90
+ }),
91
+ (0, class_validator_1.IsBoolean)(),
92
+ __metadata("design:type", Boolean)
93
+ ], ArchiveThreadLabelDto.prototype, "archived", void 0);
@@ -21,10 +21,10 @@ function IsThreadStateValue() {
21
21
  propertyName,
22
22
  validator: {
23
23
  validate(value) {
24
- return (0, thread_title_types_js_1.isThreadState)(value);
24
+ return (0, thread_title_types_js_1.isThreadState)(value) && !(typeof value === 'string' && value.startsWith('custom:'));
25
25
  },
26
26
  defaultMessage() {
27
- return 'state must be a built-in thread state or custom:<label> (1-20 chars)';
27
+ return 'state must be a stable thread-state code';
28
28
  },
29
29
  },
30
30
  });
@@ -37,7 +37,7 @@ class UpdateThreadDto {
37
37
  }
38
38
  exports.UpdateThreadDto = UpdateThreadDto;
39
39
  __decorate([
40
- (0, swagger_1.ApiPropertyOptional)({ description: 'Thread name only (1–120 chars). Server does not parse chips.' }),
40
+ (0, swagger_1.ApiPropertyOptional)({ description: 'Thread name (1–120 chars). Legacy composite titles are parsed only when exact aliases match.' }),
41
41
  (0, class_validator_1.ValidateIf)((_obj, value) => value !== undefined),
42
42
  (0, class_validator_1.IsString)(),
43
43
  (0, class_validator_1.MinLength)(1),
@@ -57,7 +57,7 @@ __decorate([
57
57
  ], UpdateThreadDto.prototype, "priority", void 0);
58
58
  __decorate([
59
59
  (0, swagger_1.ApiPropertyOptional)({
60
- description: 'Thread progress value. null clears the stored state. custom:<label> is allowed.',
60
+ description: 'Thread progress code. null clears the stored state.',
61
61
  nullable: true,
62
62
  }),
63
63
  (0, class_validator_1.ValidateIf)((_obj, value) => value !== undefined && value !== null),
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ThreadLabelController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const swagger_1 = require("@nestjs/swagger");
18
+ const workspace_decorator_js_1 = require("../../common/decorators/workspace.decorator.js");
19
+ const thread_label_dto_js_1 = require("./dto/thread-label.dto.js");
20
+ const thread_label_service_js_1 = require("./thread-label.service.js");
21
+ let ThreadLabelController = class ThreadLabelController {
22
+ service;
23
+ constructor(service) {
24
+ this.service = service;
25
+ }
26
+ list(query, ws) {
27
+ return this.service.list(query.kind, ws.id, query.includeArchived ?? false);
28
+ }
29
+ create(kind, dto, ws) {
30
+ return { item: this.service.create(kind, ws.id, dto) };
31
+ }
32
+ update(kind, code, dto, ws) {
33
+ return { item: this.service.update(kind, code, ws.id, dto) };
34
+ }
35
+ archive(kind, code, dto, ws) {
36
+ return { item: this.service.archive(kind, code, ws.id, dto.archived) };
37
+ }
38
+ };
39
+ exports.ThreadLabelController = ThreadLabelController;
40
+ __decorate([
41
+ (0, swagger_1.ApiOperation)({ operationId: 'THD.LABEL.LIST', summary: 'List conversation state or priority labels' }),
42
+ (0, swagger_1.ApiQuery)({ name: 'kind', required: true, enum: ['state', 'priority'] }),
43
+ (0, swagger_1.ApiQuery)({ name: 'includeArchived', required: false, type: Boolean }),
44
+ (0, common_1.Get)(),
45
+ __param(0, (0, common_1.Query)()),
46
+ __param(1, (0, workspace_decorator_js_1.Workspace)()),
47
+ __metadata("design:type", Function),
48
+ __metadata("design:paramtypes", [thread_label_dto_js_1.ThreadLabelListQueryDto, Object]),
49
+ __metadata("design:returntype", void 0)
50
+ ], ThreadLabelController.prototype, "list", null);
51
+ __decorate([
52
+ (0, swagger_1.ApiOperation)({ operationId: 'THD.LABEL.CREATE', summary: 'Create a conversation state label' }),
53
+ (0, common_1.Post)(':kind'),
54
+ (0, common_1.HttpCode)(common_1.HttpStatus.CREATED),
55
+ __param(0, (0, common_1.Param)('kind')),
56
+ __param(1, (0, common_1.Body)()),
57
+ __param(2, (0, workspace_decorator_js_1.Workspace)()),
58
+ __metadata("design:type", Function),
59
+ __metadata("design:paramtypes", [String, thread_label_dto_js_1.CreateThreadLabelDto, Object]),
60
+ __metadata("design:returntype", void 0)
61
+ ], ThreadLabelController.prototype, "create", null);
62
+ __decorate([
63
+ (0, swagger_1.ApiOperation)({ operationId: 'THD.LABEL.UPDATE', summary: 'Update a conversation label display' }),
64
+ (0, common_1.Patch)(':kind/:code'),
65
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
66
+ __param(0, (0, common_1.Param)('kind')),
67
+ __param(1, (0, common_1.Param)('code')),
68
+ __param(2, (0, common_1.Body)()),
69
+ __param(3, (0, workspace_decorator_js_1.Workspace)()),
70
+ __metadata("design:type", Function),
71
+ __metadata("design:paramtypes", [String, String, thread_label_dto_js_1.UpdateThreadLabelDto, Object]),
72
+ __metadata("design:returntype", void 0)
73
+ ], ThreadLabelController.prototype, "update", null);
74
+ __decorate([
75
+ (0, swagger_1.ApiOperation)({ operationId: 'THD.LABEL.ARCHIVE', summary: 'Archive or restore a conversation label' }),
76
+ (0, common_1.Post)(':kind/:code/archive'),
77
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
78
+ __param(0, (0, common_1.Param)('kind')),
79
+ __param(1, (0, common_1.Param)('code')),
80
+ __param(2, (0, common_1.Body)()),
81
+ __param(3, (0, workspace_decorator_js_1.Workspace)()),
82
+ __metadata("design:type", Function),
83
+ __metadata("design:paramtypes", [String, String, thread_label_dto_js_1.ArchiveThreadLabelDto, Object]),
84
+ __metadata("design:returntype", void 0)
85
+ ], ThreadLabelController.prototype, "archive", null);
86
+ exports.ThreadLabelController = ThreadLabelController = __decorate([
87
+ (0, swagger_1.ApiTags)('thread-labels'),
88
+ (0, common_1.Controller)('ws/:slug/thread-labels'),
89
+ __metadata("design:paramtypes", [thread_label_service_js_1.ThreadLabelService])
90
+ ], ThreadLabelController);
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ThreadLabelService = void 0;
13
+ exports.parseThreadLabelKind = parseThreadLabelKind;
14
+ const common_1 = require("@nestjs/common");
15
+ const repository_1 = require("@crewx/sdk/repository");
16
+ const thread_label_repository_js_1 = require("../../repository/thread-label.repository.js");
17
+ const PRIORITY_DEFAULT_ALIASES = {
18
+ urgent: 'U 긴급 Urgent',
19
+ high: 'P0 급함 High',
20
+ medium: 'P1 중요 Medium',
21
+ low: 'P2 보통 Low',
22
+ };
23
+ const STATE_DEFAULT_ALIASES = {
24
+ investigating: '조사중 Looking into',
25
+ 'in-progress': '진행중 In progress',
26
+ 'in-review': '검토중 In review',
27
+ 'on-hold': '보류 On hold',
28
+ done: '완료 Done',
29
+ };
30
+ const DEFAULT_NAME_KEYS = {
31
+ urgent: 'threadTitle.priority.urgent',
32
+ high: 'threadTitle.priority.high',
33
+ medium: 'threadTitle.priority.medium',
34
+ low: 'threadTitle.priority.low',
35
+ investigating: 'threadTitle.state.investigating',
36
+ 'in-progress': 'threadTitle.state.in-progress',
37
+ 'in-review': 'threadTitle.state.in-review',
38
+ 'on-hold': 'threadTitle.state.on-hold',
39
+ done: 'threadTitle.state.done',
40
+ };
41
+ function appendSearchAlias(existing, name) {
42
+ const normalized = name.trim();
43
+ if (!existing)
44
+ return normalized;
45
+ if (!normalized || existing.includes(normalized))
46
+ return existing;
47
+ return `${existing} ${normalized}`;
48
+ }
49
+ function parseThreadLabelKind(value) {
50
+ if (value === 'state' || value === 'priority')
51
+ return value;
52
+ throw new common_1.BadRequestException('THREAD_LABEL_KIND_INVALID');
53
+ }
54
+ function isThreadLabelColor(value) {
55
+ return typeof value === 'string' && repository_1.COMMON_CODE_COLORS.includes(value);
56
+ }
57
+ function normalizeName(value) {
58
+ const name = typeof value === 'string' ? value.trim() : '';
59
+ const length = [...name].length;
60
+ if (length < 1 || length > 20) {
61
+ throw new common_1.BadRequestException('THREAD_LABEL_INVALID_NAME');
62
+ }
63
+ return name;
64
+ }
65
+ function normalizeColor(value) {
66
+ if (!isThreadLabelColor(value)) {
67
+ throw new common_1.BadRequestException('THREAD_LABEL_INVALID_COLOR');
68
+ }
69
+ return value;
70
+ }
71
+ function mapDefaultNameKey(row) {
72
+ return row.is_builtin === 1 ? DEFAULT_NAME_KEYS[row.code] ?? null : null;
73
+ }
74
+ function mapItem(kind, row) {
75
+ const color = isThreadLabelColor(row.color) ? row.color : 'gray';
76
+ return {
77
+ group: kind,
78
+ code: row.code,
79
+ nameOverride: row.code_name,
80
+ nameSource: row.code_name === null ? 'default' : 'user',
81
+ defaultNameKey: mapDefaultNameKey(row),
82
+ color,
83
+ sortOrder: row.sort_order,
84
+ scope: row.workspace_id === null ? 'global' : 'workspace',
85
+ isBuiltin: row.is_builtin === 1,
86
+ isArchived: row.is_archived === 1,
87
+ };
88
+ }
89
+ function mapRepositoryError(error) {
90
+ if (!(error instanceof repository_1.CommonCodeRepositoryError))
91
+ throw error;
92
+ switch (error.reason) {
93
+ case 'COMMON_CODE_NAME_INVALID':
94
+ case 'COMMON_CODE_UPDATE_EMPTY':
95
+ throw new common_1.BadRequestException('THREAD_LABEL_INVALID_NAME');
96
+ case 'COMMON_CODE_COLOR_INVALID':
97
+ throw new common_1.BadRequestException('THREAD_LABEL_INVALID_COLOR');
98
+ case 'COMMON_CODE_BUILTIN_ARCHIVE_FORBIDDEN':
99
+ throw new common_1.BadRequestException('THREAD_LABEL_BUILTIN_ARCHIVE_FORBIDDEN');
100
+ case 'COMMON_CODE_NAME_CONFLICT':
101
+ throw new common_1.ConflictException('THREAD_LABEL_NAME_CONFLICT');
102
+ case 'COMMON_CODE_NOT_FOUND':
103
+ case 'COMMON_CODE_SCOPE_FORBIDDEN':
104
+ throw new common_1.NotFoundException('THREAD_LABEL_NOT_FOUND');
105
+ default:
106
+ throw error;
107
+ }
108
+ }
109
+ /** Feature-level service that translates SDK ledger rows to the thread-label wire contract. */
110
+ let ThreadLabelService = class ThreadLabelService {
111
+ labels;
112
+ constructor(labels = new thread_label_repository_js_1.ThreadLabelRepository()) {
113
+ this.labels = labels;
114
+ }
115
+ list(kindValue, workspaceId, includeArchived = false) {
116
+ const kind = parseThreadLabelKind(kindValue);
117
+ try {
118
+ return { items: this.labels.list(kind, workspaceId, includeArchived).map((row) => mapItem(kind, row)) };
119
+ }
120
+ catch (error) {
121
+ return mapRepositoryError(error);
122
+ }
123
+ }
124
+ create(kindValue, workspaceId, input) {
125
+ const kind = parseThreadLabelKind(kindValue);
126
+ if (kind === 'priority')
127
+ throw new common_1.BadRequestException('THREAD_LABEL_KIND_READ_ONLY');
128
+ const name = normalizeName(input.name);
129
+ const color = normalizeColor(input.color);
130
+ try {
131
+ return mapItem(kind, this.labels.createState(workspaceId, name, color));
132
+ }
133
+ catch (error) {
134
+ return mapRepositoryError(error);
135
+ }
136
+ }
137
+ update(kindValue, code, workspaceId, input) {
138
+ const kind = parseThreadLabelKind(kindValue);
139
+ const update = {};
140
+ if (input.nameOverride !== undefined) {
141
+ if (input.nameOverride === null)
142
+ update.name = null;
143
+ else
144
+ update.name = normalizeName(input.nameOverride);
145
+ }
146
+ if (input.color !== undefined)
147
+ update.color = normalizeColor(input.color);
148
+ try {
149
+ return mapItem(kind, this.labels.update(kind, code, workspaceId, update));
150
+ }
151
+ catch (error) {
152
+ return mapRepositoryError(error);
153
+ }
154
+ }
155
+ archive(kindValue, code, workspaceId, archivedValue) {
156
+ const kind = parseThreadLabelKind(kindValue);
157
+ if (typeof archivedValue !== 'boolean')
158
+ throw new common_1.BadRequestException('THREAD_LABEL_INVALID_NAME');
159
+ try {
160
+ return mapItem(kind, this.labels.archive(kind, code, workspaceId, archivedValue));
161
+ }
162
+ catch (error) {
163
+ return mapRepositoryError(error);
164
+ }
165
+ }
166
+ /** Validate a new metadata reference; archived codes are read-only history. */
167
+ assertSelectable(kindValue, code, workspaceId) {
168
+ const kind = parseThreadLabelKind(kindValue);
169
+ const row = this.labels.resolve(kind, code, workspaceId);
170
+ if (!row)
171
+ throw new common_1.NotFoundException('THREAD_LABEL_NOT_FOUND');
172
+ if (row.is_archived === 1)
173
+ throw new common_1.ConflictException('THREAD_LABEL_ARCHIVED');
174
+ return row;
175
+ }
176
+ resolve(kindValue, code, workspaceId) {
177
+ const kind = parseThreadLabelKind(kindValue);
178
+ return this.labels.resolve(kind, code, workspaceId);
179
+ }
180
+ /** Map exact legacy state suffix names to stable codes for compatibility parsing. */
181
+ stateAliases(workspaceId) {
182
+ const aliases = new Map();
183
+ for (const [code, names] of Object.entries(STATE_DEFAULT_ALIASES)) {
184
+ aliases.set(names.split(' ')[0], code);
185
+ aliases.set(names.split(' ').slice(1).join(' '), code);
186
+ }
187
+ try {
188
+ for (const row of this.labels.list('state', workspaceId, true)) {
189
+ if (row.code_name !== null)
190
+ aliases.set(row.code_name, row.code);
191
+ }
192
+ }
193
+ catch {
194
+ // Search/backfill remains available for builtin aliases if the ledger is unavailable.
195
+ }
196
+ return aliases;
197
+ }
198
+ /** Include stable codes, built-in KO/EN aliases, and current display names in q search. */
199
+ searchAliases(workspaceId) {
200
+ const aliases = new Map();
201
+ for (const [code, names] of Object.entries(PRIORITY_DEFAULT_ALIASES)) {
202
+ aliases.set(code, names);
203
+ }
204
+ for (const [code, names] of Object.entries(STATE_DEFAULT_ALIASES)) {
205
+ aliases.set(code, names);
206
+ }
207
+ try {
208
+ for (const kind of ['state', 'priority']) {
209
+ for (const row of this.labels.list(kind, workspaceId, true)) {
210
+ if (row.code_name !== null) {
211
+ aliases.set(row.code, appendSearchAlias(aliases.get(row.code), row.code_name));
212
+ }
213
+ }
214
+ }
215
+ }
216
+ catch {
217
+ // A missing optional ledger must not hide the raw thread code from reads.
218
+ }
219
+ return aliases;
220
+ }
221
+ /** Return a code for a strict legacy state name, including archived history. */
222
+ findStateCodeByName(name, workspaceId) {
223
+ const builtin = this.stateAliases(workspaceId).get(name);
224
+ if (builtin)
225
+ return builtin;
226
+ try {
227
+ return this.labels.findByName('state', name, workspaceId)?.code ?? null;
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ };
234
+ exports.ThreadLabelService = ThreadLabelService;
235
+ exports.ThreadLabelService = ThreadLabelService = __decorate([
236
+ (0, common_1.Injectable)(),
237
+ __metadata("design:paramtypes", [thread_label_repository_js_1.ThreadLabelRepository])
238
+ ], ThreadLabelService);
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runThreadTitleCommonCodeBackfill = runThreadTitleCommonCodeBackfill;
4
+ const sdk_1 = require("@crewx/sdk");
5
+ const thread_title_types_js_1 = require("./thread-title.types.js");
6
+ const thread_title_backfill_js_1 = require("./thread-title.backfill.js");
7
+ /**
8
+ * Run the SDK data migration and then clean only strictly recognized composite
9
+ * titles. The settings value is read as migration input and never updated.
10
+ */
11
+ function runThreadTitleCommonCodeBackfill(deps) {
12
+ const configuredLabels = (0, thread_title_backfill_js_1.readProgressExtras)(deps.globalSettingsStore);
13
+ const migration = (0, sdk_1.migrateLegacyThreadTitleCommonCodes)({ configuredLabels });
14
+ let compositeTitlesCleaned = 0;
15
+ const aliasesByWorkspace = new Map();
16
+ for (const row of deps.threadRepo.findAllThreads()) {
17
+ const workspaceKey = row.workspace_id ?? '';
18
+ let aliases = aliasesByWorkspace.get(workspaceKey);
19
+ if (!aliases) {
20
+ aliases = deps.threadLabelService.stateAliases(row.workspace_id ?? undefined);
21
+ aliasesByWorkspace.set(workspaceKey, aliases);
22
+ }
23
+ const parsed = (0, thread_title_backfill_js_1.parseCompositeThreadTitle)(row.title ?? '', aliases);
24
+ if (!parsed)
25
+ continue;
26
+ // Existing threadTitle metadata is authoritative. Only remove the stale
27
+ // string decoration from the title; do not infer or overwrite metadata.
28
+ if ((0, thread_title_types_js_1.hasThreadTitleKey)(row.metadata)) {
29
+ if (parsed.name !== (row.title ?? '')) {
30
+ deps.threadRepo.updateThreadTitle(row.id, parsed.name, row.workspace_id ?? undefined);
31
+ compositeTitlesCleaned += 1;
32
+ }
33
+ continue;
34
+ }
35
+ const written = deps.threadRepo.updateThreadTitleMeta(row.id, {
36
+ ...(parsed.priority !== undefined ? { priority: parsed.priority } : {}),
37
+ ...(parsed.state !== undefined ? { state: parsed.state } : {}),
38
+ }, row.workspace_id ?? undefined, { title: parsed.name });
39
+ if (written)
40
+ compositeTitlesCleaned += 1;
41
+ }
42
+ deps.logger?.log(`[thread-title] common-code migration: migratedRows=${migration.migratedRows}, ` +
43
+ `createdCodes=${migration.createdCodes}, remainingCustomReferences=${migration.remainingCustomReferences}, ` +
44
+ `orphanReferences=${migration.orphanReferences}, compositeTitlesCleaned=${compositeTitlesCleaned}`);
45
+ if (migration.remainingCustomReferences > 0 || migration.orphanReferences > 0) {
46
+ deps.logger?.warn(`[thread-title] common-code migration diagnostics: remaining custom/orphan references require attention ` +
47
+ `(custom=${migration.remainingCustomReferences}, orphan=${migration.orphanReferences})`);
48
+ }
49
+ return { ...migration, compositeTitlesCleaned };
50
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LEGACY_PROGRESS_LABELS = void 0;
4
4
  exports.readProgressExtras = readProgressExtras;
5
5
  exports.parseLegacyThreadTitle = parseLegacyThreadTitle;
6
+ exports.parseCompositeThreadTitle = parseCompositeThreadTitle;
6
7
  exports.runThreadTitleBackfill = runThreadTitleBackfill;
7
8
  const sdk_1 = require("@crewx/sdk");
8
9
  const thread_title_types_js_1 = require("./thread-title.types.js");
@@ -20,6 +21,7 @@ const PREFIX_MAP = {
20
21
  P2: 'low',
21
22
  };
22
23
  const PREFIX_RE = /^(U|P0|P1|P2) /;
24
+ const COMPOSITE_PREFIX_RE = /^(U|P0|P1|P2)(?= |$)/;
23
25
  const TITLE_SEP = ' - ';
24
26
  const BUILTIN_STATE_LABELS = {
25
27
  조사중: 'investigating',
@@ -110,6 +112,37 @@ function parseLegacyThreadTitle(title, extraLabels) {
110
112
  return null;
111
113
  return { name, priority, state, matchedLegacyLabel };
112
114
  }
115
+ /**
116
+ * Parse a legacy composite title against the already migrated state ledger.
117
+ * Unknown suffixes and empty post-split names return null so callers preserve
118
+ * the exact original title. This parser is compatibility-only; new requests
119
+ * send title, priority, and state separately.
120
+ */
121
+ function parseCompositeThreadTitle(title, stateAliases) {
122
+ let rest = title;
123
+ let priority;
124
+ const prefix = rest.match(COMPOSITE_PREFIX_RE);
125
+ if (prefix) {
126
+ priority = PREFIX_MAP[prefix[1]];
127
+ rest = rest.slice(prefix[0].length);
128
+ }
129
+ let state;
130
+ const sepIdx = rest.lastIndexOf(TITLE_SEP);
131
+ if (sepIdx >= 0) {
132
+ const suffix = rest.slice(sepIdx + TITLE_SEP.length);
133
+ const parsedState = stateAliases.get(suffix);
134
+ if (!parsedState)
135
+ return null;
136
+ state = parsedState;
137
+ rest = rest.slice(0, sepIdx);
138
+ }
139
+ if (!priority && !state)
140
+ return null;
141
+ const name = rest.trim();
142
+ if (name.length === 0)
143
+ return null;
144
+ return { name, priority, state };
145
+ }
113
146
  function appendProgressExtras(store, labels) {
114
147
  if (labels.length === 0)
115
148
  return [];
@@ -37,6 +37,11 @@ const STATE_SEARCH_TEXT = {
37
37
  'on-hold': '보류 On hold',
38
38
  done: '완료 Done',
39
39
  };
40
+ function searchTextWithDefault(defaultText, currentText) {
41
+ if (!currentText || currentText.includes(defaultText))
42
+ return currentText ?? defaultText;
43
+ return `${defaultText} ${currentText}`;
44
+ }
40
45
  function isThreadPriority(value) {
41
46
  return typeof value === 'string' && PRIORITY_SET.has(value);
42
47
  }
@@ -45,6 +50,8 @@ function isThreadState(value) {
45
50
  return false;
46
51
  if (BUILTIN_STATE_SET.has(value))
47
52
  return true;
53
+ if (value.startsWith('c_'))
54
+ return value.length > 2;
48
55
  if (!value.startsWith(CUSTOM_STATE_PREFIX))
49
56
  return false;
50
57
  const label = value.slice(CUSTOM_STATE_PREFIX.length);
@@ -119,16 +126,27 @@ function mergeThreadTitleMeta(metadataJson, patch) {
119
126
  return Object.keys(root).length === 0 ? null : JSON.stringify(root);
120
127
  }
121
128
  /** Search haystack so `q=P1` / `q=진행중` still match after title is name-only. */
122
- function threadTitleSearchHaystack(meta) {
129
+ function threadTitleSearchHaystack(meta, labelNames = new Map()) {
123
130
  const parts = [];
124
- if (meta.priority)
125
- parts.push(PRIORITY_SEARCH_TEXT[meta.priority]);
131
+ if (meta.priority) {
132
+ parts.push(searchTextWithDefault(PRIORITY_SEARCH_TEXT[meta.priority], labelNames.get(meta.priority)));
133
+ }
126
134
  if (meta.state) {
127
135
  if (meta.state.startsWith(CUSTOM_STATE_PREFIX)) {
128
136
  parts.push(customStateLabel(meta.state) ?? '');
129
137
  }
138
+ else if (STATE_SEARCH_TEXT[meta.state]) {
139
+ parts.push(searchTextWithDefault(STATE_SEARCH_TEXT[meta.state], labelNames.get(meta.state)));
140
+ }
141
+ else if (labelNames.has(meta.state)) {
142
+ parts.push(meta.state, labelNames.get(meta.state) ?? '');
143
+ }
144
+ else if (meta.state.startsWith('c_')) {
145
+ // Keep an orphan stable code visible for fail-soft diagnostics.
146
+ parts.push(meta.state);
147
+ }
130
148
  else {
131
- parts.push(STATE_SEARCH_TEXT[meta.state]);
149
+ parts.push(STATE_SEARCH_TEXT[meta.state] ?? '');
132
150
  }
133
151
  }
134
152
  return parts.filter(Boolean).join(' ');
@@ -9,7 +9,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.ThreadModule = void 0;
10
10
  const common_1 = require("@nestjs/common");
11
11
  const thread_controller_js_1 = require("./thread.controller.js");
12
+ const thread_label_controller_js_1 = require("./thread-label.controller.js");
12
13
  const thread_service_js_1 = require("./thread.service.js");
14
+ const thread_label_service_js_1 = require("./thread-label.service.js");
13
15
  const box_service_js_1 = require("../box/box.service.js");
14
16
  const crewx_module_js_1 = require("../../modules/crewx.module.js");
15
17
  let ThreadModule = class ThreadModule {
@@ -18,8 +20,8 @@ exports.ThreadModule = ThreadModule;
18
20
  exports.ThreadModule = ThreadModule = __decorate([
19
21
  (0, common_1.Module)({
20
22
  imports: [crewx_module_js_1.CrewxModule],
21
- controllers: [thread_controller_js_1.ThreadController],
22
- providers: [thread_service_js_1.ThreadService, box_service_js_1.BoxService],
23
+ controllers: [thread_controller_js_1.ThreadController, thread_label_controller_js_1.ThreadLabelController],
24
+ providers: [thread_service_js_1.ThreadService, thread_label_service_js_1.ThreadLabelService, box_service_js_1.BoxService],
23
25
  exports: [thread_service_js_1.ThreadService],
24
26
  })
25
27
  ], ThreadModule);
@@ -27,40 +27,63 @@ const provider_resume_js_1 = require("../../common/task-log/provider-resume.js")
27
27
  const pending_instruction_js_1 = require("../../common/pending-instruction.js");
28
28
  const task_error_projection_js_1 = require("../../common/task-error-projection.js");
29
29
  const overdrive_types_js_1 = require("./overdrive.types.js");
30
+ const thread_title_common_code_backfill_js_1 = require("./thread-title-common-code.backfill.js");
31
+ const thread_label_service_js_1 = require("./thread-label.service.js");
30
32
  const thread_title_backfill_js_1 = require("./thread-title.backfill.js");
31
33
  const thread_title_types_js_1 = require("./thread-title.types.js");
32
34
  // Legacy running-instruction text entries were persisted with a localized display
33
35
  // prefix before the structured `running_instruction` type existed. Match every known
34
36
  // locale prefix so classification stays language-neutral for a global product.
35
37
  const RUNNING_INSTRUCTION_LEGACY_PREFIXES = ['Running instruction:', '추가 지시:'];
36
- let ThreadService = ThreadService_1 = class ThreadService {
38
+ let ThreadService = class ThreadService {
39
+ static { ThreadService_1 = this; }
37
40
  threadRepo;
38
41
  crewxPool;
39
42
  taskRepo;
40
43
  globalSettingsStore;
44
+ threadLabelService;
41
45
  logger = new common_1.Logger(ThreadService_1.name);
42
- constructor(threadRepo, crewxPool, taskRepo = new task_repository_js_1.TaskRepository(), globalSettingsStore = (0, sdk_1.createGlobalSettingsStore)()) {
46
+ static commonCodeBackfillPaths = new Set();
47
+ constructor(threadRepo, crewxPool, taskRepo = new task_repository_js_1.TaskRepository(), globalSettingsStore = (0, sdk_1.createGlobalSettingsStore)(), threadLabelService = new thread_label_service_js_1.ThreadLabelService()) {
43
48
  this.threadRepo = threadRepo;
44
49
  this.crewxPool = crewxPool;
45
50
  this.taskRepo = taskRepo;
46
51
  this.globalSettingsStore = globalSettingsStore;
52
+ this.threadLabelService = threadLabelService;
47
53
  }
48
54
  onModuleInit() {
55
+ const dbPath = process.env.CREWX_DB || process.env.CREWX_TRACES_DB || 'default-crewx-db';
56
+ if (ThreadService_1.commonCodeBackfillPaths.has(dbPath))
57
+ return;
49
58
  try {
50
- (0, thread_title_backfill_js_1.runThreadTitleBackfill)({
59
+ (0, thread_title_common_code_backfill_js_1.runThreadTitleCommonCodeBackfill)({
51
60
  threadRepo: this.threadRepo,
61
+ threadLabelService: this.threadLabelService,
52
62
  globalSettingsStore: this.globalSettingsStore,
53
63
  logger: this.logger,
54
64
  });
65
+ ThreadService_1.commonCodeBackfillPaths.add(dbPath);
55
66
  }
56
67
  catch (e) {
57
68
  const err = e;
58
- this.logger.error(`[thread-title] backfill failed: ${err.message}`);
69
+ this.logger.error(`[thread-title] common-code migration failed: ${err.message}`, err.stack);
59
70
  }
60
71
  }
61
72
  // THD.LIST: List threads from threads table (with optional q search)
62
73
  listThreads(params, workspaceId) {
63
- const rows = this.threadRepo.findAllThreads(workspaceId);
74
+ const priorityFilter = (0, thread_title_types_js_1.parseThreadPriorityFilter)(params.priority);
75
+ const stateFilter = (0, thread_title_types_js_1.parseThreadStateFilter)(params.state);
76
+ const titleFilters = {
77
+ ...(priorityFilter ? {
78
+ priority: { values: [...priorityFilter.values], includeNone: priorityFilter.includeNone },
79
+ } : {}),
80
+ ...(stateFilter ? {
81
+ state: { values: [...stateFilter.values], includeNone: stateFilter.includeNone },
82
+ } : {}),
83
+ };
84
+ const rows = priorityFilter || stateFilter
85
+ ? this.threadRepo.findAllThreadsByTitleFilters(workspaceId, titleFilters)
86
+ : this.threadRepo.findAllThreads(workspaceId);
64
87
  // Filter by platform
65
88
  let filtered = rows;
66
89
  const platform = params.platform || 'all';
@@ -68,22 +91,14 @@ let ThreadService = ThreadService_1 = class ThreadService {
68
91
  filtered = filtered.filter((r) => r.platform === platform);
69
92
  }
70
93
  // Filter by stored priority/state values (between platform and q)
71
- const priorityFilter = (0, thread_title_types_js_1.parseThreadPriorityFilter)(params.priority);
72
- const stateFilter = (0, thread_title_types_js_1.parseThreadStateFilter)(params.state);
73
- if (priorityFilter || stateFilter) {
74
- filtered = filtered.filter((r) => {
75
- const meta = (0, thread_title_types_js_1.readThreadTitleMeta)(r.metadata);
76
- return ((0, thread_title_types_js_1.matchesThreadTitleFilter)(meta.priority, priorityFilter) &&
77
- (0, thread_title_types_js_1.matchesThreadTitleFilter)(meta.state, stateFilter));
78
- });
79
- }
94
+ const labelNames = this.threadLabelService.searchAliases(workspaceId);
80
95
  // Filter by search keyword (q param). Haystack includes tab codes + KO/EN labels
81
96
  // so habits like q=P1 / q=진행중 still match after title is name-only.
82
97
  if (params.q && params.q.trim().length > 0) {
83
98
  const queryLower = params.q.toLowerCase();
84
99
  filtered = filtered.filter((r) => {
85
100
  const meta = (0, thread_title_types_js_1.readThreadTitleMeta)(r.metadata);
86
- const text = [r.title, r.first_message, r.last_message, (0, thread_title_types_js_1.threadTitleSearchHaystack)(meta)]
101
+ const text = [r.title, r.first_message, r.last_message, (0, thread_title_types_js_1.threadTitleSearchHaystack)(meta, labelNames)]
87
102
  .filter(Boolean)
88
103
  .join(' ')
89
104
  .toLowerCase();
@@ -216,19 +231,43 @@ let ThreadService = ThreadService_1 = class ThreadService {
216
231
  }
217
232
  title = trimmed;
218
233
  }
219
- if (hasPriority || hasState) {
220
- const written = this.threadRepo.updateThreadTitleMeta(threadId, {
221
- ...(hasPriority ? { priority: patch.priority } : {}),
222
- ...(hasState ? { state: patch.state } : {}),
223
- }, workspaceId, hasTitle ? { title: title } : undefined);
234
+ const parsedTitle = hasTitle
235
+ ? (0, thread_title_backfill_js_1.parseCompositeThreadTitle)(title, this.threadLabelService.stateAliases(workspaceId))
236
+ : null;
237
+ if (parsedTitle)
238
+ title = parsedTitle.name;
239
+ const effectivePriority = hasPriority
240
+ ? patch.priority
241
+ : parsedTitle?.priority;
242
+ const effectiveState = hasState
243
+ ? patch.state
244
+ : parsedTitle?.state;
245
+ const hasEffectivePriority = hasPriority || parsedTitle?.priority !== undefined;
246
+ const hasEffectiveState = hasState || parsedTitle?.state !== undefined;
247
+ // Validate the thread before touching the ledger so cross-workspace thread
248
+ // updates never reveal or mutate another workspace's label set.
249
+ const existing = this.threadRepo.findThreadById(threadId, workspaceId);
250
+ if (!existing)
251
+ throw new common_1.NotFoundException('Thread not found');
252
+ if (hasEffectivePriority && effectivePriority !== null && effectivePriority !== undefined) {
253
+ this.threadLabelService.assertSelectable('priority', effectivePriority, workspaceId);
254
+ }
255
+ if (hasEffectiveState && effectiveState !== null && effectiveState !== undefined) {
256
+ this.threadLabelService.assertSelectable('state', effectiveState, workspaceId);
257
+ }
258
+ if (hasEffectivePriority || hasEffectiveState) {
259
+ const written = this.threadRepo.patchThreadTitleMetadata(threadId, {
260
+ ...(hasEffectivePriority ? { priority: effectivePriority } : {}),
261
+ ...(hasEffectiveState ? { state: effectiveState } : {}),
262
+ }, workspaceId);
224
263
  if (!written) {
225
- throw new common_1.NotFoundException(`Thread not found: ${threadId}`);
264
+ throw new common_1.NotFoundException('Thread not found');
226
265
  }
227
266
  }
228
- else {
267
+ if (hasTitle) {
229
268
  const updated = this.threadRepo.updateThreadTitle(threadId, title, workspaceId);
230
269
  if (!updated) {
231
- throw new common_1.NotFoundException(`Thread not found: ${threadId}`);
270
+ throw new common_1.NotFoundException('Thread not found');
232
271
  }
233
272
  }
234
273
  const row = this.threadRepo.findThreadById(threadId, workspaceId);
@@ -741,7 +780,8 @@ exports.ThreadService = ThreadService;
741
780
  exports.ThreadService = ThreadService = ThreadService_1 = __decorate([
742
781
  (0, common_1.Injectable)(),
743
782
  __param(3, (0, common_1.Optional)()),
783
+ __param(4, (0, common_1.Optional)()),
744
784
  __metadata("design:paramtypes", [thread_repository_js_1.ThreadRepository,
745
785
  crewx_pool_service_js_1.CrewxPoolService,
746
- task_repository_js_1.TaskRepository, Object])
786
+ task_repository_js_1.TaskRepository, Object, thread_label_service_js_1.ThreadLabelService])
747
787
  ], ThreadService);
@@ -53,6 +53,7 @@ const auth_constants_js_1 = require("./domain/auth/auth.constants.js");
53
53
  const ip_utils_js_1 = require("./domain/auth/utils/ip.utils.js");
54
54
  const project_repository_js_1 = require("./repository/project.repository.js");
55
55
  const browser_open_js_1 = require("./bootstrap/browser-open.js");
56
+ const validation_pipe_js_1 = require("./common/validation.pipe.js");
56
57
  async function bootstrap() {
57
58
  const httpsOptions = await (0, tls_js_1.resolveHttpsOptions)();
58
59
  // RT-6: bodyParser disabled to preserve raw body for adapter webhook signature verification.
@@ -110,7 +111,7 @@ async function bootstrap() {
110
111
  exposedHeaders: ['Mcp-Session-Id'],
111
112
  });
112
113
  // Enable class-validator DTO validation
113
- app.useGlobalPipes(new common_1.ValidationPipe({ whitelist: true, transform: true }));
114
+ app.useGlobalPipes((0, validation_pipe_js_1.createServerValidationPipe)());
114
115
  // Global prefix for API routes (exclude MCP and auth login/logout — they live at /mcp, /auth/*)
115
116
  app.setGlobalPrefix('api/v1', {
116
117
  exclude: [
@@ -18,6 +18,7 @@ const usage_limit_snapshot_repository_js_1 = require("./usage-limit-snapshot.rep
18
18
  const usage_report_repository_js_1 = require("./usage-report.repository.js");
19
19
  const notification_repository_js_1 = require("./notification.repository.js");
20
20
  const agent_suggestion_repository_js_1 = require("./agent-suggestion.repository.js");
21
+ const thread_label_repository_js_1 = require("./thread-label.repository.js");
21
22
  const repositories = [
22
23
  thread_repository_js_1.ThreadRepository,
23
24
  task_repository_js_1.TaskRepository,
@@ -29,6 +30,7 @@ const repositories = [
29
30
  usage_report_repository_js_1.UsageReportRepository,
30
31
  notification_repository_js_1.NotificationRepository,
31
32
  agent_suggestion_repository_js_1.AgentSuggestionRepository,
33
+ thread_label_repository_js_1.ThreadLabelRepository,
32
34
  ];
33
35
  let RepositoryModule = class RepositoryModule {
34
36
  };
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ThreadLabelRepository = void 0;
16
+ exports.threadLabelGroup = threadLabelGroup;
17
+ const common_1 = require("@nestjs/common");
18
+ const repository_1 = require("@crewx/sdk/repository");
19
+ const GLOBAL_LOOKUP_WORKSPACE = '__crewx_global_lookup__';
20
+ function threadLabelGroup(kind) {
21
+ return kind === 'state' ? 'THREAD_STATE' : 'THREAD_PRIORITY';
22
+ }
23
+ /**
24
+ * Server adapter around the SDK common-code repository.
25
+ *
26
+ * The adapter translates feature terminology (`state`/`priority`) to SDK
27
+ * groups. It deliberately does not own a second group list, palette, or SQL
28
+ * implementation of common-code rules.
29
+ */
30
+ let ThreadLabelRepository = class ThreadLabelRepository {
31
+ sdkRepository;
32
+ constructor(sdkRepository) {
33
+ this.sdkRepository = sdkRepository ?? new repository_1.CommonCodeRepository();
34
+ }
35
+ list(kind, workspaceId, includeArchived) {
36
+ const group = threadLabelGroup(kind);
37
+ const lookupWorkspace = workspaceId || GLOBAL_LOOKUP_WORKSPACE;
38
+ return includeArchived
39
+ ? this.sdkRepository.listForHistory(group, lookupWorkspace)
40
+ : this.sdkRepository.listSelectable(group, lookupWorkspace);
41
+ }
42
+ resolve(kind, code, workspaceId) {
43
+ const row = this.sdkRepository.resolve(threadLabelGroup(kind), code);
44
+ if (!row)
45
+ return null;
46
+ if (row.workspace_id !== null && row.workspace_id !== (workspaceId ?? ''))
47
+ return null;
48
+ return row;
49
+ }
50
+ findByName(kind, name, workspaceId) {
51
+ const rows = this.sdkRepository.listForHistory(threadLabelGroup(kind), workspaceId || GLOBAL_LOOKUP_WORKSPACE);
52
+ const workspaceMatch = workspaceId
53
+ ? rows.find((row) => row.workspace_id === workspaceId && row.code_name === name)
54
+ : undefined;
55
+ return workspaceMatch
56
+ ?? rows.find((row) => row.workspace_id === null && row.code_name === name)
57
+ ?? null;
58
+ }
59
+ createState(workspaceId, name, color) {
60
+ return this.sdkRepository.createThreadState({ workspaceId, name, color });
61
+ }
62
+ update(kind, code, workspaceId, input) {
63
+ return this.sdkRepository.updateDisplay({
64
+ group: threadLabelGroup(kind),
65
+ code,
66
+ workspaceId,
67
+ ...input,
68
+ });
69
+ }
70
+ archive(kind, code, workspaceId, archived) {
71
+ return this.sdkRepository.archive(threadLabelGroup(kind), code, workspaceId, archived);
72
+ }
73
+ };
74
+ exports.ThreadLabelRepository = ThreadLabelRepository;
75
+ exports.ThreadLabelRepository = ThreadLabelRepository = __decorate([
76
+ (0, common_1.Injectable)(),
77
+ __param(0, (0, common_1.Optional)()),
78
+ __metadata("design:paramtypes", [repository_1.CommonCodeRepository])
79
+ ], ThreadLabelRepository);
@@ -13,6 +13,10 @@ const drizzle_orm_1 = require("drizzle-orm");
13
13
  const repository_1 = require("@crewx/sdk/repository");
14
14
  const base_sqlite_repository_js_1 = require("./base-sqlite.repository.js");
15
15
  const thread_title_types_js_1 = require("../domain/thread/thread-title.types.js");
16
+ const THREAD_STATE_INDEX_EXPR = "json_extract(metadata, '$.threadTitle.state')";
17
+ const THREAD_PRIORITY_INDEX_EXPR = "json_extract(metadata, '$.threadTitle.priority')";
18
+ const THREAD_STATE_INDEX = 'idx_threads_workspace_thread_state';
19
+ const THREAD_PRIORITY_INDEX = 'idx_threads_workspace_thread_priority';
16
20
  let ThreadRepository = class ThreadRepository extends base_sqlite_repository_js_1.BaseSqliteRepository {
17
21
  sdkThread = new repository_1.ThreadRepository();
18
22
  /** Fetch all threads from all DB paths, deduplicated by id.
@@ -28,6 +32,99 @@ let ThreadRepository = class ThreadRepository extends base_sqlite_repository_js_
28
32
  throw e;
29
33
  }
30
34
  }
35
+ /**
36
+ * Query exact metadata values through the same JSON expressions used by the
37
+ * SDK-owned expression indexes. The in-memory list path remains the fallback
38
+ * for unscoped legacy callers; HTTP workspace requests always provide an id.
39
+ */
40
+ findAllThreadsByTitleFilters(workspaceId, filters) {
41
+ if (!workspaceId) {
42
+ const rows = this.findAllThreads(workspaceId);
43
+ return rows.filter((row) => {
44
+ const state = this.readRawThreadTitleValue(row.metadata, 'state');
45
+ const priority = this.readRawThreadTitleValue(row.metadata, 'priority');
46
+ return this.matchesRawFilter(state, filters.state) && this.matchesRawFilter(priority, filters.priority);
47
+ });
48
+ }
49
+ const query = this.buildThreadTitleFilterQuery(workspaceId, filters);
50
+ const seenIds = new Set();
51
+ const result = [];
52
+ for (const dbPath of this.resolveDbPaths()) {
53
+ if (!(0, fs_1.existsSync)(dbPath))
54
+ continue;
55
+ const handle = (0, repository_1.openDrizzleDb)(dbPath);
56
+ try {
57
+ for (const row of handle.db.all(query)) {
58
+ if (seenIds.has(row.id))
59
+ continue;
60
+ seenIds.add(row.id);
61
+ result.push(row);
62
+ }
63
+ }
64
+ finally {
65
+ handle.close();
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+ /** Test/diagnostic plan for the exact THD.LIST title-filter SQL. */
71
+ explainThreadTitleFilterPlan(workspaceId, filters) {
72
+ const query = this.buildThreadTitleFilterQuery(workspaceId, filters);
73
+ for (const dbPath of this.resolveDbPaths()) {
74
+ if (!(0, fs_1.existsSync)(dbPath))
75
+ continue;
76
+ const handle = (0, repository_1.openDrizzleDb)(dbPath);
77
+ try {
78
+ return handle.db.all((0, drizzle_orm_1.sql) `EXPLAIN QUERY PLAN ${query}`).map((row) => row.detail);
79
+ }
80
+ finally {
81
+ handle.close();
82
+ }
83
+ }
84
+ return [];
85
+ }
86
+ buildThreadTitleFilterQuery(workspaceId, filters) {
87
+ const clauses = [(0, drizzle_orm_1.sql) `workspace_id = ${workspaceId}`];
88
+ if (filters.state)
89
+ clauses.push(this.buildRawFilterSql(THREAD_STATE_INDEX_EXPR, filters.state));
90
+ if (filters.priority)
91
+ clauses.push(this.buildRawFilterSql(THREAD_PRIORITY_INDEX_EXPR, filters.priority));
92
+ const index = filters.state ? THREAD_STATE_INDEX : filters.priority ? THREAD_PRIORITY_INDEX : undefined;
93
+ const indexedTable = index
94
+ ? (0, drizzle_orm_1.sql) `threads INDEXED BY ${drizzle_orm_1.sql.raw(index)}`
95
+ : (0, drizzle_orm_1.sql) `threads`;
96
+ return (0, drizzle_orm_1.sql) `SELECT * FROM ${indexedTable} WHERE ${drizzle_orm_1.sql.join(clauses, (0, drizzle_orm_1.sql) ` AND `)} ORDER BY updated_at DESC`;
97
+ }
98
+ buildRawFilterSql(expression, filter) {
99
+ const expressionSql = drizzle_orm_1.sql.raw(expression);
100
+ const valuesSql = filter.values.length > 0
101
+ ? (0, drizzle_orm_1.sql) `${expressionSql} IN (${drizzle_orm_1.sql.join(filter.values.map((value) => (0, drizzle_orm_1.sql) `${value}`), (0, drizzle_orm_1.sql) `, `)})`
102
+ : undefined;
103
+ if (filter.includeNone && valuesSql)
104
+ return (0, drizzle_orm_1.sql) `(${valuesSql} OR ${expressionSql} IS NULL)`;
105
+ if (filter.includeNone)
106
+ return (0, drizzle_orm_1.sql) `${expressionSql} IS NULL`;
107
+ return valuesSql ?? (0, drizzle_orm_1.sql) `1 = 1`;
108
+ }
109
+ readRawThreadTitleValue(metadata, key) {
110
+ if (!metadata)
111
+ return null;
112
+ try {
113
+ const root = JSON.parse(metadata);
114
+ const value = root.threadTitle?.[key];
115
+ return typeof value === 'string' ? value : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ matchesRawFilter(value, filter) {
122
+ if (!filter)
123
+ return true;
124
+ if (value === null)
125
+ return filter.includeNone;
126
+ return filter.values.includes(value);
127
+ }
31
128
  /** Find only the threads whose id or title exactly matches one of `ids`.
32
129
  * Avoids loading the entire threads table for scoped lookups (WI-20260718-001). */
33
130
  findThreadsByIdsOrTitles(ids, workspaceId) {
@@ -189,6 +286,13 @@ let ThreadRepository = class ThreadRepository extends base_sqlite_repository_js_
189
286
  handle.close();
190
287
  }
191
288
  }
289
+ /** Delegate metadata writes to the SDK's atomic read/merge/write patch. */
290
+ patchThreadTitleMetadata(threadId, patch, workspaceId) {
291
+ const result = this.sdkThread.patchThreadTitleMetadata(threadId, patch, workspaceId);
292
+ if (result instanceof repository_1.ThreadTitleMetadataPatchError)
293
+ throw result;
294
+ return result;
295
+ }
192
296
  /** Toggle pinned 0↔1 for a thread. Returns new state or null if not found. */
193
297
  togglePin(threadId, workspaceId) {
194
298
  try {
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Transport-only contract for conversation state and priority labels.
4
+ *
5
+ * This file intentionally has no Nest, Node, database, or SDK dependency so
6
+ * browser clients can reuse the wire shape without pulling server code.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crewx",
3
- "version": "0.9.0-rc.73",
3
+ "version": "0.9.0-rc.74",
4
4
  "description": "CrewX — AI agent team dashboard with Electron UI and CLI (Web + Electron + Global CLI)",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -82,20 +82,20 @@
82
82
  "wink-nlp-utils": "2.1.0",
83
83
  "yargs": "17.7.0",
84
84
  "zod": "3.25.76",
85
- "@crewx/chromex": "0.1.0-rc.105",
86
- "@crewx/cli": "0.9.0-rc.73",
87
- "@crewx/cron": "0.1.10-rc.107",
88
- "@crewx/dreaming": "0.1.0-rc.98",
89
- "@crewx/doc": "0.1.9-rc.65",
90
- "@crewx/knowledge-core": "0.1.17-rc.82",
91
- "@crewx/memory": "0.1.23-rc.89",
85
+ "@crewx/chromex": "0.1.0-rc.106",
86
+ "@crewx/cron": "0.1.10-rc.108",
87
+ "@crewx/cli": "0.9.0-rc.74",
88
+ "@crewx/dreaming": "0.1.0-rc.99",
89
+ "@crewx/doc": "0.1.9-rc.66",
90
+ "@crewx/memory": "0.1.23-rc.90",
91
+ "@crewx/sdk": "0.9.0-rc.74",
92
92
  "@crewx/shared": "0.0.6",
93
- "@crewx/sdk": "0.9.0-rc.73",
94
- "@crewx/search": "0.1.10-rc.68",
95
93
  "@crewx/skill": "0.1.20",
96
- "@crewx/wbs": "0.1.10-rc.98",
97
- "@crewx/wi": "0.1.10-rc.93",
98
- "@crewx/workflow": "0.3.22-rc.119"
94
+ "@crewx/knowledge-core": "0.1.17-rc.83",
95
+ "@crewx/search": "0.1.10-rc.69",
96
+ "@crewx/workflow": "0.3.22-rc.120",
97
+ "@crewx/wi": "0.1.10-rc.94",
98
+ "@crewx/wbs": "0.1.10-rc.99"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@anthropic-ai/claude-agent-sdk": "0.2.132",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewx/cli",
3
- "version": "0.9.0-rc.73",
3
+ "version": "0.9.0-rc.74",
4
4
  "license": "UNLICENSED",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"