@happyvertical/smrt-projects 0.37.2 → 0.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1018 +1,438 @@
1
- import { ObjectRegistry, foreignKey, smrt, SmrtObject, config, SmrtCollection } from "@happyvertical/smrt-core";
2
- import { tenantId, TenantScoped, queryGlobal, queryWithGlobals } from "@happyvertical/smrt-tenancy";
3
- import { getAI } from "@happyvertical/ai";
4
- import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
5
- import { loadEnvConfig } from "@happyvertical/utils";
1
+ import { i as __exportAll, r as issueIncorporateFeedbackPrompt, t as Issue } from "./chunks/Issue-DITLxBl7.js";
2
+ import "./chunks/constants-BhVfX4Jn.js";
3
+ import { t as PullRequest } from "./chunks/PullRequest-C6mck19s.js";
4
+ import { PROJECTS_MODULE_META, PROJECTS_UI_SLOTS } from "./ui.js";
5
+ import { ObjectRegistry, SmrtCollection, SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
6
+ import { TenantScoped, queryGlobal, queryWithGlobals, tenantId } from "@happyvertical/smrt-tenancy";
6
7
  import { createLogger } from "@happyvertical/logger";
7
8
  import { getProject } from "@happyvertical/projects";
8
9
  import { getModuleConfig } from "@happyvertical/smrt-config";
9
10
  import { getRepository } from "@happyvertical/repos";
10
- import { PROJECTS_MODULE_META, PROJECTS_UI_SLOTS } from "./ui.js";
11
- ObjectRegistry.registerPackageManifest(
12
- new URL("./manifest.json", import.meta.url)
13
- );
14
- const SYNC_THROTTLE_MS = 5 * 60 * 1e3;
15
- const issueIncorporateFeedbackPrompt = definePrompt({
16
- key: "projects.issue.incorporateFeedback",
17
- template: `You are updating a specification document based on team feedback.
18
-
19
- Current specification:
20
- {body}
21
-
22
- Team comments and feedback:
23
- {comments}
24
-
25
- Instructions:
26
- 1. Analyze the comments for consensus, changes, and new requirements
27
- 2. Update the specification to reflect the agreed-upon changes
28
- 3. Maintain the original structure and formatting where possible
29
- 4. Mark any conflicting feedback that needs resolution
30
- 5. Return ONLY the updated specification text, no additional commentary`,
31
- ai: {
32
- temperature: 0.2
33
- },
34
- editable: {
35
- template: true,
36
- profile: true,
37
- model: true,
38
- params: true
39
- }
40
- });
41
- var __defProp$4 = Object.defineProperty;
42
- var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
43
- var __decorateClass$5 = (decorators, target, key, kind) => {
44
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
45
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
46
- if (decorator = decorators[i])
47
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
48
- if (kind && result) __defProp$4(target, key, result);
49
- return result;
11
+ //#region src/__smrt-register__.ts
12
+ ObjectRegistry.registerPackageManifest(new URL("./manifest.json", "" + import.meta.url));
13
+ //#endregion
14
+ //#region src/collections/Issues.ts
15
+ var Issues_exports = /* @__PURE__ */ __exportAll({ IssueCollection: () => IssueCollection });
16
+ var IssueCollection = class extends SmrtCollection {
17
+ static _itemClass = Issue;
18
+ /**
19
+ * Discover issues from a repository and sync to database
20
+ *
21
+ * This method:
22
+ * 1. Fetches issues from the provider via SDK
23
+ * 2. Creates/updates SMRT Issue records in the database
24
+ * 3. Returns the synced Issue objects
25
+ *
26
+ * @param options - Discovery options
27
+ * @returns Array of Issue objects
28
+ */
29
+ async discover(options) {
30
+ const { repository, filters } = options;
31
+ const repositoryId = repository.id ?? void 0;
32
+ if (!repositoryId) throw new Error("Repository must be saved before discovering issues");
33
+ const remoteIssues = await (await repository.getClient()).searchIssues("", filters);
34
+ const issues = [];
35
+ for (const remote of remoteIssues) {
36
+ let issue = await this.findOne({ where: {
37
+ repositoryId,
38
+ number: remote.number
39
+ } });
40
+ if (!issue) issue = await this.create({
41
+ repositoryId,
42
+ number: remote.number,
43
+ nodeId: remote.id,
44
+ title: remote.title,
45
+ body: remote.body,
46
+ state: remote.state,
47
+ author: remote.author.login,
48
+ labels: remote.labels.map((l) => l.name),
49
+ assignees: remote.assignees.map((a) => a.login),
50
+ commentsCount: remote.commentsCount,
51
+ lastSyncedAt: /* @__PURE__ */ new Date()
52
+ });
53
+ else {
54
+ issue.nodeId = remote.id;
55
+ issue.title = remote.title;
56
+ issue.body = remote.body;
57
+ issue.state = remote.state;
58
+ issue.author = remote.author.login;
59
+ issue.labels = remote.labels.map((l) => l.name);
60
+ issue.assignees = remote.assignees.map((a) => a.login);
61
+ issue.commentsCount = remote.commentsCount;
62
+ issue.lastSyncedAt = /* @__PURE__ */ new Date();
63
+ }
64
+ await issue.save();
65
+ issues.push(issue);
66
+ }
67
+ return issues;
68
+ }
69
+ /**
70
+ * Find issues by repository
71
+ *
72
+ * @param repositoryId - Repository ID
73
+ * @returns Array of issues
74
+ */
75
+ async findByRepository(repositoryId) {
76
+ return await this.list({ where: { repositoryId } });
77
+ }
78
+ /**
79
+ * Find open issues
80
+ *
81
+ * @param repositoryId - Optional repository filter
82
+ * @returns Array of open issues
83
+ */
84
+ async findOpen(repositoryId) {
85
+ const where = { state: "open" };
86
+ if (repositoryId) where.repositoryId = repositoryId;
87
+ return await this.list({ where });
88
+ }
89
+ /**
90
+ * Find issues by label
91
+ *
92
+ * @param label - Label name
93
+ * @param repositoryId - Optional repository filter
94
+ * @returns Array of issues with the label
95
+ */
96
+ async findByLabel(label, repositoryId) {
97
+ return (await this.list({ where: repositoryId ? { repositoryId } : {} })).filter((issue) => issue.labels.includes(label));
98
+ }
99
+ /**
100
+ * Find issues by assignee
101
+ *
102
+ * @param assignee - Assignee login
103
+ * @param repositoryId - Optional repository filter
104
+ * @returns Array of issues assigned to the user
105
+ */
106
+ async findByAssignee(assignee, repositoryId) {
107
+ return (await this.list({ where: repositoryId ? { repositoryId } : {} })).filter((issue) => issue.assignees.includes(assignee));
108
+ }
109
+ /**
110
+ * Find issues needing review (AI-powered)
111
+ *
112
+ * @param repositoryId - Optional repository filter
113
+ * @returns Array of issues that need review
114
+ */
115
+ async findNeedingReview(repositoryId) {
116
+ const openIssues = await this.findOpen(repositoryId);
117
+ const needingReview = [];
118
+ for (const issue of openIssues) if (await issue.needsReview()) needingReview.push(issue);
119
+ return needingReview;
120
+ }
121
+ /**
122
+ * Find issue by number in a repository
123
+ *
124
+ * @param repositoryId - Repository ID
125
+ * @param number - Issue number
126
+ * @returns Issue or null
127
+ */
128
+ async findByNumber(repositoryId, number) {
129
+ return (await this.list({
130
+ where: {
131
+ repositoryId,
132
+ number
133
+ },
134
+ limit: 1
135
+ }))[0] || null;
136
+ }
137
+ /**
138
+ * Get issues with unincorporated feedback
139
+ *
140
+ * Issues that have comments but haven't had feedback incorporated
141
+ *
142
+ * @param repositoryId - Optional repository filter
143
+ * @returns Array of issues
144
+ */
145
+ async findWithUnincorporatedFeedback(repositoryId) {
146
+ return (await this.findOpen(repositoryId)).filter((issue) => issue.commentsCount > 0 && issue.synthesisCount === 0);
147
+ }
148
+ /**
149
+ * Batch sync issues from repository
150
+ *
151
+ * @param repository - Repository to sync from
152
+ * @param options - Sync options
153
+ * @returns Array of synced issues
154
+ */
155
+ async batchSync(repository, options = {}) {
156
+ const issues = await this.findByRepository(repository.id);
157
+ const synced = [];
158
+ for (const issue of issues) {
159
+ await issue.sync(options);
160
+ synced.push(issue);
161
+ }
162
+ return synced;
163
+ }
164
+ /**
165
+ * Find issues by tenant ID
166
+ *
167
+ * @param tenantId - Tenant ID to filter by
168
+ * @returns Array of issues for the tenant
169
+ */
170
+ async findByTenant(tenantId) {
171
+ return this.list({ where: { tenantId } });
172
+ }
173
+ /**
174
+ * Find all global issues (no tenant association).
175
+ *
176
+ * Routes through the shared tenant-global helper so it does not throw under
177
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
178
+ * flagged as an isolation violation). (#1600)
179
+ *
180
+ * @returns Array of global issues
181
+ */
182
+ async findGlobal() {
183
+ return queryGlobal(this);
184
+ }
185
+ /**
186
+ * Find issues for a tenant plus all global issues.
187
+ *
188
+ * Fails closed if an active tenant context requests a different tenant's
189
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
190
+ *
191
+ * @param tenantId - Tenant ID to filter by
192
+ * @returns Array of tenant and global issues
193
+ */
194
+ async findWithGlobals(tenantId) {
195
+ return queryWithGlobals(this, tenantId, "Issue.findWithGlobals");
196
+ }
50
197
  };
51
- let Issue = class extends SmrtObject {
52
- tenantId = null;
53
- repositoryId;
54
- /**
55
- * Issue number (provider-specific)
56
- */
57
- number = 0;
58
- /**
59
- * Node ID for GraphQL operations (GitHub Projects API)
60
- */
61
- nodeId = "";
62
- /**
63
- * Issue title
64
- */
65
- title = "";
66
- /**
67
- * Issue body/description
68
- */
69
- body = "";
70
- /**
71
- * Issue state
72
- */
73
- state = "open";
74
- /**
75
- * Author's login/username
76
- */
77
- author = "";
78
- /**
79
- * Labels attached to the issue
80
- */
81
- labels = [];
82
- /**
83
- * Assignee logins
84
- */
85
- assignees = [];
86
- /**
87
- * Number of comments on the issue
88
- */
89
- commentsCount = 0;
90
- /**
91
- * Last sync timestamp
92
- */
93
- lastSyncedAt = null;
94
- /**
95
- * Original body before any AI synthesis (for rollback)
96
- */
97
- originalBody = "";
98
- /**
99
- * Number of times feedback has been incorporated
100
- */
101
- synthesisCount = 0;
102
- /**
103
- * Transient: Cached repository (not persisted)
104
- * Protected so PullRequest can access it
105
- */
106
- _repository;
107
- /**
108
- * Transient: Cached client (not persisted)
109
- */
110
- _client;
111
- constructor(options = {}) {
112
- super(options);
113
- if (options.repositoryId !== void 0)
114
- this.repositoryId = options.repositoryId;
115
- if (options.number !== void 0) this.number = options.number;
116
- if (options.nodeId !== void 0) this.nodeId = options.nodeId;
117
- if (options.title !== void 0) this.title = options.title;
118
- if (options.body !== void 0) this.body = options.body;
119
- if (options.state !== void 0) this.state = options.state;
120
- if (options.author !== void 0) this.author = options.author;
121
- if (options.labels !== void 0) this.labels = options.labels;
122
- if (options.assignees !== void 0) this.assignees = options.assignees;
123
- if (options.commentsCount !== void 0)
124
- this.commentsCount = options.commentsCount;
125
- if (options.lastSyncedAt !== void 0)
126
- this.lastSyncedAt = options.lastSyncedAt;
127
- if (options.originalBody !== void 0)
128
- this.originalBody = options.originalBody;
129
- if (options.synthesisCount !== void 0)
130
- this.synthesisCount = options.synthesisCount;
131
- if (options.tenantId !== void 0) this.tenantId = options.tenantId;
132
- }
133
- /**
134
- * Get the repository this issue belongs to
135
- */
136
- async getRepository() {
137
- if (this._repository) {
138
- return this._repository;
139
- }
140
- if (!this.repositoryId) {
141
- throw new Error("Issue has no repositoryId set");
142
- }
143
- const { RepositoryCollection: RepositoryCollection2 } = await Promise.resolve().then(() => Repositories);
144
- const collection = await RepositoryCollection2.create(this.options);
145
- const repo = await collection.get({ id: this.repositoryId });
146
- if (!repo) {
147
- throw new Error(`Repository ${this.repositoryId} not found`);
148
- }
149
- this._repository = repo;
150
- return repo;
151
- }
152
- /**
153
- * Get the repository client for API operations
154
- */
155
- async getClient() {
156
- if (this._client) {
157
- return this._client;
158
- }
159
- const repo = await this.getRepository();
160
- this._client = await repo.getClient();
161
- return this._client;
162
- }
163
- /**
164
- * Clear cached repository and client
165
- */
166
- clearCache() {
167
- this._repository = void 0;
168
- this._client = void 0;
169
- }
170
- /**
171
- * Sync issue data from the provider
172
- *
173
- * @param options - Sync options
174
- * @returns This issue with updated fields
175
- */
176
- async sync(options = {}) {
177
- if (!options.force && this.lastSyncedAt && Date.now() - this.lastSyncedAt.getTime() < SYNC_THROTTLE_MS) {
178
- return this;
179
- }
180
- const client = await this.getClient();
181
- const issueData = await client.getIssue(this.number);
182
- this.nodeId = issueData.id;
183
- this.title = issueData.title;
184
- this.body = issueData.body;
185
- this.state = issueData.state;
186
- this.author = issueData.author.login;
187
- this.labels = issueData.labels.map((l) => l.name);
188
- this.assignees = issueData.assignees.map((a) => a.login);
189
- this.commentsCount = issueData.commentsCount;
190
- this.lastSyncedAt = /* @__PURE__ */ new Date();
191
- await this.save();
192
- return this;
193
- }
194
- /**
195
- * Get comments on this issue
196
- *
197
- * @returns Array of Comment objects (SMRT models)
198
- */
199
- async getComments() {
200
- const client = await this.getClient();
201
- const comments = await client.listComments(this.number);
202
- const { Comment: CommentClass } = await Promise.resolve().then(() => Comment$1);
203
- return comments.map(
204
- (c) => new CommentClass({
205
- ...this.options,
206
- issueId: this.id ?? void 0,
207
- commentId: c.id,
208
- body: c.body,
209
- author: c.author.login,
210
- createdAt: c.createdAt,
211
- updatedAt: c.updatedAt,
212
- url: c.url
213
- })
214
- );
215
- }
216
- /**
217
- * Add a comment to this issue
218
- *
219
- * @param body - Comment body text
220
- * @returns Created Comment (SMRT model)
221
- */
222
- async addComment(body) {
223
- const client = await this.getClient();
224
- const created = await client.addComment(this.number, body);
225
- const { Comment: CommentClass } = await Promise.resolve().then(() => Comment$1);
226
- const comment = new CommentClass({
227
- ...this.options,
228
- issueId: this.id ?? void 0,
229
- commentId: created.id,
230
- body: created.body,
231
- author: created.author.login,
232
- createdAt: created.createdAt,
233
- updatedAt: created.updatedAt,
234
- url: created.url
235
- });
236
- await comment.save();
237
- this.commentsCount++;
238
- await this.save();
239
- return comment;
240
- }
241
- /**
242
- * Incorporate feedback from comments into the issue body
243
- *
244
- * This is the core "Living Spec" functionality:
245
- * 1. Reads all comments on the issue
246
- * 2. Uses AI to synthesize comments with the current body
247
- * 3. Optionally updates the issue with the synthesized content
248
- *
249
- * @param options - Feedback incorporation options
250
- * @returns Result with synthesized content and status
251
- */
252
- async incorporateFeedback(options = {}) {
253
- const comments = await this.getComments();
254
- let relevantComments = comments;
255
- if (options.since) {
256
- const sinceDate = options.since;
257
- relevantComments = comments.filter(
258
- (c) => c.createdAt && c.createdAt > sinceDate
259
- );
260
- }
261
- if (relevantComments.length === 0) {
262
- return {
263
- synthesized: this.body,
264
- applied: false,
265
- commentsAnalyzed: 0
266
- };
267
- }
268
- const resolvedPrompt = await resolvePrompt(
269
- issueIncorporateFeedbackPrompt.key,
270
- {
271
- db: this.options.db ?? this.options.persistence,
272
- tenantId: this.tenantId,
273
- variables: {
274
- body: this.body,
275
- comments: relevantComments.map((c) => `- ${c.author}: ${c.body}`).join("\n")
276
- },
277
- override: options.prompt ? { template: options.prompt } : void 0
278
- }
279
- );
280
- const aiOptions = {
281
- ...resolvedPrompt.ai.params,
282
- ...resolvedPrompt.ai.model ? { model: resolvedPrompt.ai.model } : {}
283
- };
284
- let synthesized;
285
- if (resolvedPrompt.ai.provider) {
286
- synthesized = await this.runPromptWithResolvedAI(
287
- resolvedPrompt.text,
288
- resolvedPrompt.ai.provider,
289
- aiOptions
290
- );
291
- } else {
292
- synthesized = await this.do(resolvedPrompt.text, {
293
- ...aiOptions,
294
- includeData: false
295
- });
296
- }
297
- const result = {
298
- synthesized,
299
- applied: false,
300
- commentsAnalyzed: relevantComments.length,
301
- previousBody: this.body
302
- };
303
- if (options.apply) {
304
- if (this.synthesisCount === 0) {
305
- this.originalBody = this.body;
306
- }
307
- const client = await this.getClient();
308
- await client.updateIssue(this.number, { body: synthesized });
309
- this.body = synthesized;
310
- this.synthesisCount++;
311
- this.lastSyncedAt = /* @__PURE__ */ new Date();
312
- await this.save();
313
- result.applied = true;
314
- }
315
- return result;
316
- }
317
- async runPromptWithResolvedAI(instructions, provider, options) {
318
- const aiOption = this.options.ai;
319
- if (this.isExplicitAiClientOption(aiOption)) {
320
- const ai2 = await this.getAiClient();
321
- return this.sendPromptMessage(ai2, instructions, options);
322
- }
323
- const globalAiConfig = config.toJSON().ai || {};
324
- const instanceAiConfig = aiOption ?? {};
325
- const aiConfig = loadEnvConfig(
326
- {
327
- ...globalAiConfig,
328
- ...instanceAiConfig
329
- },
330
- {
331
- packageName: "ai",
332
- prefix: "SMRT",
333
- schema: {
334
- provider: "string",
335
- model: "string",
336
- apiKey: "string",
337
- timeout: "number",
338
- maxRetries: "number",
339
- temperature: "number",
340
- maxTokens: "number"
341
- }
342
- }
343
- );
344
- const env = process.env;
345
- const apiKey = aiConfig.apiKey || (provider === "anthropic" ? env.ANTHROPIC_API_KEY : provider === "gemini" ? env.GEMINI_API_KEY : env.OPENAI_API_KEY);
346
- const ai = await getAI({
347
- ...aiConfig,
348
- provider,
349
- type: provider,
350
- model: options.model ?? aiConfig.model,
351
- defaultModel: options.model ?? aiConfig.model,
352
- apiKey
353
- });
354
- return this.sendPromptMessage(ai, instructions, options);
355
- }
356
- /**
357
- * Sends a fully-resolved instruction string to the given AI client.
358
- *
359
- * `incorporateFeedback()` curates the entire prompt (issue body + comments)
360
- * via the resolved prompt template, so this path deliberately does NOT inject
361
- * the object's own field data — that would duplicate the body. The `this.do()`
362
- * fallback path passes `includeData: false` for the same reason, keeping both
363
- * `incorporateFeedback()` paths consistent (#1567).
364
- */
365
- async sendPromptMessage(ai, instructions, options) {
366
- const prompt = `--- Beginning of instructions ---
367
- ${instructions}
368
- --- End of instructions ---
369
- Based on the content body, please follow the instructions and provide a response. Never make use of codeblocks.`;
370
- const tools = this.getAvailableTools();
371
- return await ai.message(prompt, {
372
- ...options,
373
- tools: tools.length > 0 ? tools : void 0
374
- });
375
- }
376
- isExplicitAiClientOption(aiOption) {
377
- return !!(aiOption && typeof aiOption === "object" && typeof aiOption.embed === "function" && !aiOption.provider && !aiOption.type);
378
- }
379
- /**
380
- * Rollback to the original body before AI synthesis
381
- *
382
- * @returns Result with success status
383
- */
384
- async rollback() {
385
- if (!this.originalBody) {
386
- return {
387
- success: false,
388
- message: "No original body to rollback to"
389
- };
390
- }
391
- if (this.synthesisCount === 0) {
392
- return {
393
- success: false,
394
- message: "No synthesis has been applied"
395
- };
396
- }
397
- const client = await this.getClient();
398
- await client.updateIssue(this.number, { body: this.originalBody });
399
- this.body = this.originalBody;
400
- this.originalBody = "";
401
- this.synthesisCount = 0;
402
- this.lastSyncedAt = /* @__PURE__ */ new Date();
403
- await this.save();
404
- return {
405
- success: true,
406
- message: "Successfully rolled back to original body"
407
- };
408
- }
409
- /**
410
- * AI-powered: Check if this issue needs review
411
- *
412
- * @returns True if the issue likely needs attention
413
- */
414
- async needsReview() {
415
- return await this.is(
416
- `This issue needs review because one or more of the following:
417
- - It has been open for a long time without updates
418
- - There are unresolved questions in the comments
419
- - The requirements are unclear or incomplete
420
- - There is conflicting feedback that needs resolution`
421
- );
422
- }
423
- /**
424
- * AI-powered: Check if the issue is a bug report
425
- */
426
- async isBugReport() {
427
- return await this.is(
428
- "This issue describes a bug, defect, or unexpected behavior"
429
- );
430
- }
431
- /**
432
- * AI-powered: Check if the issue is a feature request
433
- */
434
- async isFeatureRequest() {
435
- return await this.is(
436
- "This issue is a feature request or enhancement proposal"
437
- );
438
- }
439
- /**
440
- * AI-powered: Generate suggested labels based on content
441
- *
442
- * @returns Array of suggested label names
443
- */
444
- async suggestLabels() {
445
- const suggestion = await this.do(
446
- `Based on the issue title and body, suggest appropriate labels.
447
- Consider:
448
- - Type: bug, feature, docs, chore, test
449
- - Priority: P0 (critical), P1 (high), P2 (medium), P3 (low)
450
- - Area: specific code areas or components
451
-
452
- Return only a comma-separated list of labels, nothing else.`
453
- );
454
- return suggestion.split(",").map((l) => l.trim()).filter(Boolean);
455
- }
456
- /**
457
- * Close this issue
458
- */
459
- async close() {
460
- const client = await this.getClient();
461
- await client.closeIssue(this.number);
462
- this.state = "closed";
463
- this.lastSyncedAt = /* @__PURE__ */ new Date();
464
- await this.save();
465
- }
466
- /**
467
- * Add labels to this issue
468
- *
469
- * @param labels - Label names to add
470
- */
471
- async addLabels(labels) {
472
- const client = await this.getClient();
473
- await client.addLabels(this.number, labels);
474
- this.labels = [.../* @__PURE__ */ new Set([...this.labels, ...labels])];
475
- await this.save();
476
- }
477
- /**
478
- * Remove a label from this issue
479
- *
480
- * @param label - Label name to remove
481
- */
482
- async removeLabel(label) {
483
- const client = await this.getClient();
484
- await client.removeLabel(this.number, label);
485
- this.labels = this.labels.filter((l) => l !== label);
486
- await this.save();
487
- }
488
- /**
489
- * Assign users to this issue
490
- *
491
- * @param assignees - User logins to assign
492
- */
493
- async assign(assignees) {
494
- const client = await this.getClient();
495
- await client.assignIssue(this.number, assignees);
496
- this.assignees = [.../* @__PURE__ */ new Set([...this.assignees, ...assignees])];
497
- await this.save();
498
- }
499
- /**
500
- * Get issue URL
501
- */
502
- getUrl() {
503
- const repo = this._repository;
504
- if (repo) {
505
- return `https://github.com/${repo.owner}/${repo.name}/issues/${this.number}`;
506
- }
507
- return "";
508
- }
509
- };
510
- __decorateClass$5([
511
- tenantId({ nullable: true })
512
- ], Issue.prototype, "tenantId", 2);
513
- __decorateClass$5([
514
- foreignKey("Repository", { required: true })
515
- ], Issue.prototype, "repositoryId", 2);
516
- Issue = __decorateClass$5([
517
- TenantScoped({ mode: "optional" }),
518
- smrt({
519
- tableStrategy: "sti",
520
- api: { include: ["list", "get", "create", "update"] },
521
- mcp: { include: ["list", "get", "sync", "incorporateFeedback"] },
522
- // sync/incorporateFeedback/rollback are operator commands invoked in-process
523
- // via the CLI; they intentionally aren't exposed over HTTP today.
524
- cli: {
525
- include: ["list", "get", "sync", "incorporateFeedback", "rollback"],
526
- skipApiCheck: true
527
- }
528
- })
529
- ], Issue);
530
- const Issue$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
531
- __proto__: null,
532
- get Issue() {
533
- return Issue;
534
- }
535
- }, Symbol.toStringTag, { value: "Module" }));
536
- class IssueCollection extends SmrtCollection {
537
- static _itemClass = Issue;
538
- /**
539
- * Discover issues from a repository and sync to database
540
- *
541
- * This method:
542
- * 1. Fetches issues from the provider via SDK
543
- * 2. Creates/updates SMRT Issue records in the database
544
- * 3. Returns the synced Issue objects
545
- *
546
- * @param options - Discovery options
547
- * @returns Array of Issue objects
548
- */
549
- async discover(options) {
550
- const { repository, filters } = options;
551
- const repositoryId = repository.id ?? void 0;
552
- if (!repositoryId) {
553
- throw new Error("Repository must be saved before discovering issues");
554
- }
555
- const repoClient = await repository.getClient();
556
- const remoteIssues = await repoClient.searchIssues("", filters);
557
- const issues = [];
558
- for (const remote of remoteIssues) {
559
- let issue = await this.findOne({
560
- where: {
561
- repositoryId,
562
- number: remote.number
563
- }
564
- });
565
- if (!issue) {
566
- issue = await this.create({
567
- repositoryId,
568
- number: remote.number,
569
- nodeId: remote.id,
570
- title: remote.title,
571
- body: remote.body,
572
- state: remote.state,
573
- author: remote.author.login,
574
- labels: remote.labels.map((l) => l.name),
575
- assignees: remote.assignees.map((a) => a.login),
576
- commentsCount: remote.commentsCount,
577
- lastSyncedAt: /* @__PURE__ */ new Date()
578
- });
579
- } else {
580
- issue.nodeId = remote.id;
581
- issue.title = remote.title;
582
- issue.body = remote.body;
583
- issue.state = remote.state;
584
- issue.author = remote.author.login;
585
- issue.labels = remote.labels.map((l) => l.name);
586
- issue.assignees = remote.assignees.map((a) => a.login);
587
- issue.commentsCount = remote.commentsCount;
588
- issue.lastSyncedAt = /* @__PURE__ */ new Date();
589
- }
590
- await issue.save();
591
- issues.push(issue);
592
- }
593
- return issues;
594
- }
595
- /**
596
- * Find issues by repository
597
- *
598
- * @param repositoryId - Repository ID
599
- * @returns Array of issues
600
- */
601
- async findByRepository(repositoryId) {
602
- return await this.list({
603
- where: { repositoryId }
604
- });
605
- }
606
- /**
607
- * Find open issues
608
- *
609
- * @param repositoryId - Optional repository filter
610
- * @returns Array of open issues
611
- */
612
- async findOpen(repositoryId) {
613
- const where = { state: "open" };
614
- if (repositoryId) {
615
- where.repositoryId = repositoryId;
616
- }
617
- return await this.list({ where });
618
- }
619
- /**
620
- * Find issues by label
621
- *
622
- * @param label - Label name
623
- * @param repositoryId - Optional repository filter
624
- * @returns Array of issues with the label
625
- */
626
- async findByLabel(label, repositoryId) {
627
- const issues = await this.list({
628
- where: repositoryId ? { repositoryId } : {}
629
- });
630
- return issues.filter((issue) => issue.labels.includes(label));
631
- }
632
- /**
633
- * Find issues by assignee
634
- *
635
- * @param assignee - Assignee login
636
- * @param repositoryId - Optional repository filter
637
- * @returns Array of issues assigned to the user
638
- */
639
- async findByAssignee(assignee, repositoryId) {
640
- const issues = await this.list({
641
- where: repositoryId ? { repositoryId } : {}
642
- });
643
- return issues.filter((issue) => issue.assignees.includes(assignee));
644
- }
645
- /**
646
- * Find issues needing review (AI-powered)
647
- *
648
- * @param repositoryId - Optional repository filter
649
- * @returns Array of issues that need review
650
- */
651
- async findNeedingReview(repositoryId) {
652
- const openIssues = await this.findOpen(repositoryId);
653
- const needingReview = [];
654
- for (const issue of openIssues) {
655
- if (await issue.needsReview()) {
656
- needingReview.push(issue);
657
- }
658
- }
659
- return needingReview;
660
- }
661
- /**
662
- * Find issue by number in a repository
663
- *
664
- * @param repositoryId - Repository ID
665
- * @param number - Issue number
666
- * @returns Issue or null
667
- */
668
- async findByNumber(repositoryId, number) {
669
- const results = await this.list({
670
- where: { repositoryId, number },
671
- limit: 1
672
- });
673
- return results[0] || null;
674
- }
675
- /**
676
- * Get issues with unincorporated feedback
677
- *
678
- * Issues that have comments but haven't had feedback incorporated
679
- *
680
- * @param repositoryId - Optional repository filter
681
- * @returns Array of issues
682
- */
683
- async findWithUnincorporatedFeedback(repositoryId) {
684
- const issues = await this.findOpen(repositoryId);
685
- return issues.filter(
686
- (issue) => issue.commentsCount > 0 && issue.synthesisCount === 0
687
- );
688
- }
689
- /**
690
- * Batch sync issues from repository
691
- *
692
- * @param repository - Repository to sync from
693
- * @param options - Sync options
694
- * @returns Array of synced issues
695
- */
696
- async batchSync(repository, options = {}) {
697
- const issues = await this.findByRepository(repository.id);
698
- const synced = [];
699
- for (const issue of issues) {
700
- await issue.sync(options);
701
- synced.push(issue);
702
- }
703
- return synced;
704
- }
705
- /**
706
- * Find issues by tenant ID
707
- *
708
- * @param tenantId - Tenant ID to filter by
709
- * @returns Array of issues for the tenant
710
- */
711
- async findByTenant(tenantId2) {
712
- return this.list({ where: { tenantId: tenantId2 } });
713
- }
714
- /**
715
- * Find all global issues (no tenant association).
716
- *
717
- * Routes through the shared tenant-global helper so it does not throw under
718
- * an active tenant context (an explicit `tenant_id IS NULL` filter would be
719
- * flagged as an isolation violation). (#1600)
720
- *
721
- * @returns Array of global issues
722
- */
723
- async findGlobal() {
724
- return queryGlobal(this);
725
- }
726
- /**
727
- * Find issues for a tenant plus all global issues.
728
- *
729
- * Fails closed if an active tenant context requests a different tenant's
730
- * rows; the admin/system path keeps the cross-tenant capability. (#1600)
731
- *
732
- * @param tenantId - Tenant ID to filter by
733
- * @returns Array of tenant and global issues
734
- */
735
- async findWithGlobals(tenantId2) {
736
- return queryWithGlobals(this, tenantId2, "Issue.findWithGlobals");
737
- }
738
- }
739
- const Issues = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
740
- __proto__: null,
741
- IssueCollection
742
- }, Symbol.toStringTag, { value: "Module" }));
198
+ //#endregion
199
+ //#region src/models/Project.ts
743
200
  var __defProp$3 = Object.defineProperty;
744
- var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
745
- var __decorateClass$4 = (decorators, target, key, kind) => {
746
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
747
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
748
- if (decorator = decorators[i])
749
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
750
- if (kind && result) __defProp$3(target, key, result);
751
- return result;
201
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
202
+ var __decorateClass$3 = (decorators, target, key, kind) => {
203
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
204
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
205
+ if (kind && result) __defProp$3(target, key, result);
206
+ return result;
752
207
  };
753
- let Project = class extends SmrtObject {
754
- tenantId = null;
755
- /**
756
- * Provider-specific project ID (e.g., GitHub GraphQL node ID)
757
- */
758
- projectId = "";
759
- /**
760
- * Human-readable project number
761
- */
762
- projectNumber = 0;
763
- /**
764
- * Project title
765
- */
766
- title = "";
767
- /**
768
- * Project description
769
- */
770
- description = "";
771
- /**
772
- * Project owner (organization or user)
773
- */
774
- owner = "";
775
- /**
776
- * Project URL
777
- */
778
- url = "";
779
- /**
780
- * Project provider type
781
- */
782
- providerType = "github";
783
- /**
784
- * Environment variable name or config key for token resolution
785
- */
786
- tokenConfigKey = "GITHUB_TOKEN";
787
- /**
788
- * Available statuses (columns) in the project
789
- */
790
- statuses = [];
791
- /**
792
- * Custom fields defined in the project
793
- */
794
- fields = [];
795
- /**
796
- * Status field ID for GitHub Projects V2
797
- */
798
- statusFieldId = "";
799
- /**
800
- * Maps status name to option ID (for GitHub Projects V2)
801
- */
802
- statusOptions = {};
803
- /**
804
- * Last sync timestamp
805
- */
806
- lastSyncedAt = null;
807
- /**
808
- * Transient: Cached project client (not persisted)
809
- */
810
- _client;
811
- constructor(options = {}) {
812
- super(options);
813
- if (options.projectId !== void 0) this.projectId = options.projectId;
814
- if (options.projectNumber !== void 0)
815
- this.projectNumber = options.projectNumber;
816
- if (options.title !== void 0) this.title = options.title;
817
- if (options.description !== void 0)
818
- this.description = options.description;
819
- if (options.owner !== void 0) this.owner = options.owner;
820
- if (options.url !== void 0) this.url = options.url;
821
- if (options.providerType !== void 0)
822
- this.providerType = options.providerType;
823
- if (options.tokenConfigKey !== void 0)
824
- this.tokenConfigKey = options.tokenConfigKey;
825
- if (options.statuses !== void 0) this.statuses = options.statuses;
826
- if (options.fields !== void 0) this.fields = options.fields;
827
- if (options.statusFieldId !== void 0)
828
- this.statusFieldId = options.statusFieldId;
829
- if (options.statusOptions !== void 0)
830
- this.statusOptions = options.statusOptions;
831
- if (options.tenantId !== void 0) this.tenantId = options.tenantId;
832
- }
833
- /**
834
- * Get the project client, resolving token from config
835
- *
836
- * @returns Project client for API operations
837
- * @throws Error if token cannot be resolved
838
- */
839
- async getClient() {
840
- if (this._client) {
841
- return this._client;
842
- }
843
- const token = process.env[this.tokenConfigKey] || getModuleConfig("smrt-projects", {})[this.tokenConfigKey];
844
- if (!token) {
845
- throw new Error(
846
- `Token not found for key '${this.tokenConfigKey}'. Set the ${this.tokenConfigKey} environment variable or configure it in smrt.config.`
847
- );
848
- }
849
- this._client = await getProject({
850
- type: this.providerType,
851
- projectId: this.projectId,
852
- token,
853
- statusFieldId: this.statusFieldId || void 0,
854
- statusOptions: Object.keys(this.statusOptions).length > 0 ? this.statusOptions : void 0
855
- });
856
- return this._client;
857
- }
858
- /**
859
- * Clear the cached client
860
- */
861
- clearClient() {
862
- this._client = void 0;
863
- }
864
- /**
865
- * Sync project metadata from the provider
866
- *
867
- * @param options - Sync options
868
- * @returns This project with updated fields
869
- */
870
- async sync(options = {}) {
871
- if (!options.force && this.lastSyncedAt && Date.now() - this.lastSyncedAt.getTime() < SYNC_THROTTLE_MS) {
872
- return this;
873
- }
874
- const client = await this.getClient();
875
- const projectData = await client.getProject();
876
- this.title = projectData.title;
877
- this.description = projectData.description || "";
878
- this.owner = projectData.owner;
879
- this.url = projectData.url;
880
- this.statuses = projectData.statuses;
881
- this.fields = projectData.fields;
882
- this.lastSyncedAt = /* @__PURE__ */ new Date();
883
- await this.save();
884
- return this;
885
- }
886
- /**
887
- * Add an issue or PR to this project
888
- *
889
- * @param item - Issue or PullRequest to add
890
- * @returns Created ProjectItem
891
- */
892
- async addItem(item) {
893
- if (!item.nodeId) {
894
- throw new Error("Item must have a nodeId (sync the item first)");
895
- }
896
- const client = await this.getClient();
897
- return await client.addItem(item.nodeId);
898
- }
899
- /**
900
- * Remove an item from this project
901
- *
902
- * @param itemId - Project item ID to remove
903
- */
904
- async removeItem(itemId) {
905
- const client = await this.getClient();
906
- await client.removeItem(itemId);
907
- }
908
- /**
909
- * Get a specific item from the project
910
- *
911
- * @param itemId - Project item ID
912
- * @returns ProjectItem or null
913
- */
914
- async getItem(itemId) {
915
- const client = await this.getClient();
916
- return await client.getItem(itemId);
917
- }
918
- /**
919
- * List items in this project
920
- *
921
- * @param filters - Optional filters
922
- * @returns Array of ProjectItems
923
- */
924
- async listItems(filters) {
925
- const client = await this.getClient();
926
- return await client.listItems(filters);
927
- }
928
- /**
929
- * Update an item's status (column)
930
- *
931
- * @param itemId - Project item ID
932
- * @param status - New status name
933
- */
934
- async updateItemStatus(itemId, status) {
935
- const client = await this.getClient();
936
- await client.updateItemStatus(itemId, status);
937
- }
938
- /**
939
- * Update an item's custom field
940
- *
941
- * @param itemId - Project item ID
942
- * @param fieldId - Field ID
943
- * @param value - New value
944
- */
945
- async updateItemField(itemId, fieldId, value) {
946
- const client = await this.getClient();
947
- await client.updateItemField(itemId, fieldId, value);
948
- }
949
- /**
950
- * Get available statuses
951
- *
952
- * @returns Array of status definitions
953
- */
954
- async getStatuses() {
955
- if (this.statuses.length > 0) {
956
- return this.statuses;
957
- }
958
- const client = await this.getClient();
959
- this.statuses = await client.listStatuses();
960
- await this.save();
961
- return this.statuses;
962
- }
963
- /**
964
- * Get available fields
965
- *
966
- * @returns Array of field definitions
967
- */
968
- async getFields() {
969
- if (this.fields.length > 0) {
970
- return this.fields;
971
- }
972
- const client = await this.getClient();
973
- this.fields = await client.listFields();
974
- await this.save();
975
- return this.fields;
976
- }
977
- /**
978
- * Get items in a specific status/column
979
- *
980
- * @param status - Status name
981
- * @returns Array of ProjectItems in that status
982
- */
983
- async getItemsByStatus(status) {
984
- return await this.listItems({ status });
985
- }
986
- /**
987
- * Move an item to a new status
988
- *
989
- * @param item - Issue or PullRequest
990
- * @param status - Target status name
991
- */
992
- async moveItem(item, status) {
993
- const items = await this.listItems();
994
- const projectItem = items.find((i) => i.contentId === item.nodeId);
995
- if (!projectItem) {
996
- throw new Error("Item not found in project");
997
- }
998
- await this.updateItemStatus(projectItem.id, status);
999
- }
1000
- /**
1001
- * AI-powered: Analyze project health and suggest improvements
1002
- *
1003
- * @returns Analysis of project status
1004
- */
1005
- async analyzeHealth() {
1006
- const items = await this.listItems();
1007
- const statuses = await this.getStatuses();
1008
- const statusCounts = {};
1009
- for (const status of statuses) {
1010
- statusCounts[status.name] = items.filter(
1011
- (i) => i.status === status.name
1012
- ).length;
1013
- }
1014
- return await this.do(
1015
- `Analyze the health of this project board and suggest improvements.
208
+ var Project = class extends SmrtObject {
209
+ tenantId = null;
210
+ /**
211
+ * Provider-specific project ID (e.g., GitHub GraphQL node ID)
212
+ */
213
+ projectId = "";
214
+ /**
215
+ * Human-readable project number
216
+ */
217
+ projectNumber = 0;
218
+ /**
219
+ * Project title
220
+ */
221
+ title = "";
222
+ /**
223
+ * Project description
224
+ */
225
+ description = "";
226
+ /**
227
+ * Project owner (organization or user)
228
+ */
229
+ owner = "";
230
+ /**
231
+ * Project URL
232
+ */
233
+ url = "";
234
+ /**
235
+ * Project provider type
236
+ */
237
+ providerType = "github";
238
+ /**
239
+ * Environment variable name or config key for token resolution
240
+ */
241
+ tokenConfigKey = "GITHUB_TOKEN";
242
+ /**
243
+ * Available statuses (columns) in the project
244
+ */
245
+ statuses = [];
246
+ /**
247
+ * Custom fields defined in the project
248
+ */
249
+ fields = [];
250
+ /**
251
+ * Status field ID for GitHub Projects V2
252
+ */
253
+ statusFieldId = "";
254
+ /**
255
+ * Maps status name to option ID (for GitHub Projects V2)
256
+ */
257
+ statusOptions = {};
258
+ /**
259
+ * Last sync timestamp
260
+ */
261
+ lastSyncedAt = null;
262
+ /**
263
+ * Transient: Cached project client (not persisted)
264
+ */
265
+ _client;
266
+ constructor(options = {}) {
267
+ super(options);
268
+ if (options.projectId !== void 0) this.projectId = options.projectId;
269
+ if (options.projectNumber !== void 0) this.projectNumber = options.projectNumber;
270
+ if (options.title !== void 0) this.title = options.title;
271
+ if (options.description !== void 0) this.description = options.description;
272
+ if (options.owner !== void 0) this.owner = options.owner;
273
+ if (options.url !== void 0) this.url = options.url;
274
+ if (options.providerType !== void 0) this.providerType = options.providerType;
275
+ if (options.tokenConfigKey !== void 0) this.tokenConfigKey = options.tokenConfigKey;
276
+ if (options.statuses !== void 0) this.statuses = options.statuses;
277
+ if (options.fields !== void 0) this.fields = options.fields;
278
+ if (options.statusFieldId !== void 0) this.statusFieldId = options.statusFieldId;
279
+ if (options.statusOptions !== void 0) this.statusOptions = options.statusOptions;
280
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
281
+ }
282
+ /**
283
+ * Get the project client, resolving token from config
284
+ *
285
+ * @returns Project client for API operations
286
+ * @throws Error if token cannot be resolved
287
+ */
288
+ async getClient() {
289
+ if (this._client) return this._client;
290
+ const token = process.env[this.tokenConfigKey] || getModuleConfig("smrt-projects", {})[this.tokenConfigKey];
291
+ if (!token) throw new Error(`Token not found for key '${this.tokenConfigKey}'. Set the ${this.tokenConfigKey} environment variable or configure it in smrt.config.`);
292
+ this._client = await getProject({
293
+ type: this.providerType,
294
+ projectId: this.projectId,
295
+ token,
296
+ statusFieldId: this.statusFieldId || void 0,
297
+ statusOptions: Object.keys(this.statusOptions).length > 0 ? this.statusOptions : void 0
298
+ });
299
+ return this._client;
300
+ }
301
+ /**
302
+ * Clear the cached client
303
+ */
304
+ clearClient() {
305
+ this._client = void 0;
306
+ }
307
+ /**
308
+ * Sync project metadata from the provider
309
+ *
310
+ * @param options - Sync options
311
+ * @returns This project with updated fields
312
+ */
313
+ async sync(options = {}) {
314
+ if (!options.force && this.lastSyncedAt && Date.now() - this.lastSyncedAt.getTime() < 3e5) return this;
315
+ const projectData = await (await this.getClient()).getProject();
316
+ this.title = projectData.title;
317
+ this.description = projectData.description || "";
318
+ this.owner = projectData.owner;
319
+ this.url = projectData.url;
320
+ this.statuses = projectData.statuses;
321
+ this.fields = projectData.fields;
322
+ this.lastSyncedAt = /* @__PURE__ */ new Date();
323
+ await this.save();
324
+ return this;
325
+ }
326
+ /**
327
+ * Add an issue or PR to this project
328
+ *
329
+ * @param item - Issue or PullRequest to add
330
+ * @returns Created ProjectItem
331
+ */
332
+ async addItem(item) {
333
+ if (!item.nodeId) throw new Error("Item must have a nodeId (sync the item first)");
334
+ return await (await this.getClient()).addItem(item.nodeId);
335
+ }
336
+ /**
337
+ * Remove an item from this project
338
+ *
339
+ * @param itemId - Project item ID to remove
340
+ */
341
+ async removeItem(itemId) {
342
+ await (await this.getClient()).removeItem(itemId);
343
+ }
344
+ /**
345
+ * Get a specific item from the project
346
+ *
347
+ * @param itemId - Project item ID
348
+ * @returns ProjectItem or null
349
+ */
350
+ async getItem(itemId) {
351
+ return await (await this.getClient()).getItem(itemId);
352
+ }
353
+ /**
354
+ * List items in this project
355
+ *
356
+ * @param filters - Optional filters
357
+ * @returns Array of ProjectItems
358
+ */
359
+ async listItems(filters) {
360
+ return await (await this.getClient()).listItems(filters);
361
+ }
362
+ /**
363
+ * Update an item's status (column)
364
+ *
365
+ * @param itemId - Project item ID
366
+ * @param status - New status name
367
+ */
368
+ async updateItemStatus(itemId, status) {
369
+ await (await this.getClient()).updateItemStatus(itemId, status);
370
+ }
371
+ /**
372
+ * Update an item's custom field
373
+ *
374
+ * @param itemId - Project item ID
375
+ * @param fieldId - Field ID
376
+ * @param value - New value
377
+ */
378
+ async updateItemField(itemId, fieldId, value) {
379
+ await (await this.getClient()).updateItemField(itemId, fieldId, value);
380
+ }
381
+ /**
382
+ * Get available statuses
383
+ *
384
+ * @returns Array of status definitions
385
+ */
386
+ async getStatuses() {
387
+ if (this.statuses.length > 0) return this.statuses;
388
+ const client = await this.getClient();
389
+ this.statuses = await client.listStatuses();
390
+ await this.save();
391
+ return this.statuses;
392
+ }
393
+ /**
394
+ * Get available fields
395
+ *
396
+ * @returns Array of field definitions
397
+ */
398
+ async getFields() {
399
+ if (this.fields.length > 0) return this.fields;
400
+ const client = await this.getClient();
401
+ this.fields = await client.listFields();
402
+ await this.save();
403
+ return this.fields;
404
+ }
405
+ /**
406
+ * Get items in a specific status/column
407
+ *
408
+ * @param status - Status name
409
+ * @returns Array of ProjectItems in that status
410
+ */
411
+ async getItemsByStatus(status) {
412
+ return await this.listItems({ status });
413
+ }
414
+ /**
415
+ * Move an item to a new status
416
+ *
417
+ * @param item - Issue or PullRequest
418
+ * @param status - Target status name
419
+ */
420
+ async moveItem(item, status) {
421
+ const projectItem = (await this.listItems()).find((i) => i.contentId === item.nodeId);
422
+ if (!projectItem) throw new Error("Item not found in project");
423
+ await this.updateItemStatus(projectItem.id, status);
424
+ }
425
+ /**
426
+ * AI-powered: Analyze project health and suggest improvements
427
+ *
428
+ * @returns Analysis of project status
429
+ */
430
+ async analyzeHealth() {
431
+ const items = await this.listItems();
432
+ const statuses = await this.getStatuses();
433
+ const statusCounts = {};
434
+ for (const status of statuses) statusCounts[status.name] = items.filter((i) => i.status === status.name).length;
435
+ return await this.do(`Analyze the health of this project board and suggest improvements.
1016
436
 
1017
437
  Project: ${this.title}
1018
438
  Description: ${this.description}
@@ -1025,1473 +445,1084 @@ let Project = class extends SmrtObject {
1025
445
  Provide:
1026
446
  1. Overall health assessment
1027
447
  2. Potential bottlenecks
1028
- 3. Suggestions for improvement`,
1029
- // Title/description/status counts hand-rolled above; skip do()'s
1030
- // object-data injection so the board context is not duplicated.
1031
- { includeData: false }
1032
- );
1033
- }
1034
- /**
1035
- * Get project by title
1036
- *
1037
- * @param title - Project title
1038
- * @param options - Additional options
1039
- * @returns Project or null
1040
- */
1041
- static async getByTitle(title, options = {}) {
1042
- const { ProjectCollection: ProjectCollection2 } = await Promise.resolve().then(() => Projects);
1043
- const collection = await ProjectCollection2.create(options);
1044
- return await collection.findByTitle(title);
1045
- }
448
+ 3. Suggestions for improvement`, { includeData: false });
449
+ }
450
+ /**
451
+ * Get project by title
452
+ *
453
+ * @param title - Project title
454
+ * @param options - Additional options
455
+ * @returns Project or null
456
+ */
457
+ static async getByTitle(title, options = {}) {
458
+ const { ProjectCollection } = await Promise.resolve().then(() => Projects_exports);
459
+ return await (await ProjectCollection.create(options)).findByTitle(title);
460
+ }
1046
461
  };
1047
- __decorateClass$4([
1048
- tenantId({ nullable: true })
1049
- ], Project.prototype, "tenantId", 2);
1050
- Project = __decorateClass$4([
1051
- TenantScoped({ mode: "optional" }),
1052
- smrt({
1053
- api: { include: ["list", "get", "create", "update"] },
1054
- mcp: { include: ["list", "get", "sync", "addItem", "updateItemStatus"] },
1055
- // sync/addItem/updateItemStatus/listItems are operator commands invoked
1056
- // in-process via the CLI; they intentionally aren't exposed over HTTP today.
1057
- cli: {
1058
- include: [
1059
- "list",
1060
- "get",
1061
- "sync",
1062
- "addItem",
1063
- "updateItemStatus",
1064
- "listItems"
1065
- ],
1066
- skipApiCheck: true
1067
- }
1068
- })
1069
- ], Project);
1070
- const logger$3 = createLogger({ level: "info" });
1071
- class ProjectCollection extends SmrtCollection {
1072
- static _itemClass = Project;
1073
- /**
1074
- * Find project by title
1075
- *
1076
- * @param title - Project title
1077
- * @returns Project or null
1078
- */
1079
- async findByTitle(title) {
1080
- const results = await this.list({
1081
- where: { title },
1082
- limit: 1
1083
- });
1084
- return results[0] || null;
1085
- }
1086
- /**
1087
- * Find projects by owner
1088
- *
1089
- * @param owner - Project owner (organization or user)
1090
- * @returns Array of projects
1091
- */
1092
- async findByOwner(owner) {
1093
- return await this.list({
1094
- where: { owner }
1095
- });
1096
- }
1097
- /**
1098
- * Find projects by provider type
1099
- *
1100
- * @param providerType - Provider type
1101
- * @returns Array of projects
1102
- */
1103
- async findByProvider(providerType) {
1104
- return await this.list({
1105
- where: { providerType }
1106
- });
1107
- }
1108
- /**
1109
- * Get or create a project by ID
1110
- *
1111
- * @param projectId - Provider-specific project ID
1112
- * @param options - Additional options for creation
1113
- * @returns Project (existing or newly created)
1114
- */
1115
- async getOrCreate(projectId, options = {}) {
1116
- let project = await this.findOne({
1117
- where: { projectId }
1118
- });
1119
- if (!project) {
1120
- project = await this.create({
1121
- projectId,
1122
- title: options.title || "",
1123
- owner: options.owner || "",
1124
- providerType: options.providerType || "github",
1125
- tokenConfigKey: options.tokenConfigKey || "GITHUB_TOKEN",
1126
- statusFieldId: options.statusFieldId || "",
1127
- statusOptions: options.statusOptions || {}
1128
- });
1129
- await project.save();
1130
- await project.sync({ force: true });
1131
- }
1132
- return project;
1133
- }
1134
- /**
1135
- * Sync all projects
1136
- *
1137
- * @param options - Sync options
1138
- * @returns Array of synced projects
1139
- */
1140
- async syncAll(options = {}) {
1141
- const projects = await this.list({});
1142
- const synced = [];
1143
- for (const project of projects) {
1144
- await project.sync(options);
1145
- synced.push(project);
1146
- }
1147
- return synced;
1148
- }
1149
- /**
1150
- * Find projects with items in a specific status
1151
- *
1152
- * @param status - Status name
1153
- * @returns Array of projects
1154
- */
1155
- async findWithItemsInStatus(status) {
1156
- const projects = await this.list({});
1157
- const matching = [];
1158
- for (const project of projects) {
1159
- try {
1160
- const items = await project.getItemsByStatus(status);
1161
- if (items.length > 0) {
1162
- matching.push(project);
1163
- }
1164
- } catch (error) {
1165
- logger$3.warn(`Error accessing items for project ${project.projectId}`, {
1166
- error: error instanceof Error ? error.message : error
1167
- });
1168
- }
1169
- }
1170
- return matching;
1171
- }
1172
- /**
1173
- * Get project statistics
1174
- *
1175
- * @param projectId - Project ID
1176
- * @returns Statistics object
1177
- */
1178
- async getStatistics(projectId) {
1179
- const project = await this.findOne({ where: { projectId } });
1180
- if (!project) {
1181
- throw new Error(`Project ${projectId} not found`);
1182
- }
1183
- const items = await project.listItems();
1184
- const statuses = await project.getStatuses();
1185
- const itemsByStatus = {};
1186
- for (const status of statuses) {
1187
- itemsByStatus[status.name] = 0;
1188
- }
1189
- const itemsByType = {
1190
- Issue: 0,
1191
- PullRequest: 0,
1192
- DraftIssue: 0
1193
- };
1194
- for (const item of items) {
1195
- if (item.status && itemsByStatus[item.status] !== void 0) {
1196
- itemsByStatus[item.status]++;
1197
- }
1198
- if (itemsByType[item.type] !== void 0) {
1199
- itemsByType[item.type]++;
1200
- }
1201
- }
1202
- return {
1203
- totalItems: items.length,
1204
- itemsByStatus,
1205
- itemsByType
1206
- };
1207
- }
1208
- /**
1209
- * Find projects by tenant ID
1210
- *
1211
- * @param tenantId - Tenant ID to filter by
1212
- * @returns Array of projects for the tenant
1213
- */
1214
- async findByTenant(tenantId2) {
1215
- return this.list({ where: { tenantId: tenantId2 } });
1216
- }
1217
- /**
1218
- * Find all global projects (no tenant association).
1219
- *
1220
- * Routes through the shared tenant-global helper so it does not throw under
1221
- * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1222
- * flagged as an isolation violation). (#1600)
1223
- *
1224
- * @returns Array of global projects
1225
- */
1226
- async findGlobal() {
1227
- return queryGlobal(this);
1228
- }
1229
- /**
1230
- * Find projects for a tenant plus all global projects.
1231
- *
1232
- * Fails closed if an active tenant context requests a different tenant's
1233
- * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1234
- *
1235
- * @param tenantId - Tenant ID to filter by
1236
- * @returns Array of tenant and global projects
1237
- */
1238
- async findWithGlobals(tenantId2) {
1239
- return queryWithGlobals(this, tenantId2, "Project.findWithGlobals");
1240
- }
1241
- }
1242
- const Projects = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1243
- __proto__: null,
1244
- ProjectCollection
1245
- }, Symbol.toStringTag, { value: "Module" }));
1246
- var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
1247
- var __decorateClass$3 = (decorators, target, key, kind) => {
1248
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
1249
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
1250
- if (decorator = decorators[i])
1251
- result = decorator(result) || result;
1252
- return result;
462
+ __decorateClass$3([tenantId({ nullable: true })], Project.prototype, "tenantId", 2);
463
+ Project = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
464
+ api: { include: [
465
+ "list",
466
+ "get",
467
+ "create",
468
+ "update"
469
+ ] },
470
+ mcp: { include: [
471
+ "list",
472
+ "get",
473
+ "sync",
474
+ "addItem",
475
+ "updateItemStatus"
476
+ ] },
477
+ cli: {
478
+ include: [
479
+ "list",
480
+ "get",
481
+ "sync",
482
+ "addItem",
483
+ "updateItemStatus",
484
+ "listItems"
485
+ ],
486
+ skipApiCheck: true
487
+ }
488
+ })], Project);
489
+ //#endregion
490
+ //#region src/collections/Projects.ts
491
+ var Projects_exports = /* @__PURE__ */ __exportAll({ ProjectCollection: () => ProjectCollection });
492
+ var logger$3 = createLogger({ level: "info" });
493
+ var ProjectCollection = class extends SmrtCollection {
494
+ static _itemClass = Project;
495
+ /**
496
+ * Find project by title
497
+ *
498
+ * @param title - Project title
499
+ * @returns Project or null
500
+ */
501
+ async findByTitle(title) {
502
+ return (await this.list({
503
+ where: { title },
504
+ limit: 1
505
+ }))[0] || null;
506
+ }
507
+ /**
508
+ * Find projects by owner
509
+ *
510
+ * @param owner - Project owner (organization or user)
511
+ * @returns Array of projects
512
+ */
513
+ async findByOwner(owner) {
514
+ return await this.list({ where: { owner } });
515
+ }
516
+ /**
517
+ * Find projects by provider type
518
+ *
519
+ * @param providerType - Provider type
520
+ * @returns Array of projects
521
+ */
522
+ async findByProvider(providerType) {
523
+ return await this.list({ where: { providerType } });
524
+ }
525
+ /**
526
+ * Get or create a project by ID
527
+ *
528
+ * @param projectId - Provider-specific project ID
529
+ * @param options - Additional options for creation
530
+ * @returns Project (existing or newly created)
531
+ */
532
+ async getOrCreate(projectId, options = {}) {
533
+ let project = await this.findOne({ where: { projectId } });
534
+ if (!project) {
535
+ project = await this.create({
536
+ projectId,
537
+ title: options.title || "",
538
+ owner: options.owner || "",
539
+ providerType: options.providerType || "github",
540
+ tokenConfigKey: options.tokenConfigKey || "GITHUB_TOKEN",
541
+ statusFieldId: options.statusFieldId || "",
542
+ statusOptions: options.statusOptions || {}
543
+ });
544
+ await project.save();
545
+ await project.sync({ force: true });
546
+ }
547
+ return project;
548
+ }
549
+ /**
550
+ * Sync all projects
551
+ *
552
+ * @param options - Sync options
553
+ * @returns Array of synced projects
554
+ */
555
+ async syncAll(options = {}) {
556
+ const projects = await this.list({});
557
+ const synced = [];
558
+ for (const project of projects) {
559
+ await project.sync(options);
560
+ synced.push(project);
561
+ }
562
+ return synced;
563
+ }
564
+ /**
565
+ * Find projects with items in a specific status
566
+ *
567
+ * @param status - Status name
568
+ * @returns Array of projects
569
+ */
570
+ async findWithItemsInStatus(status) {
571
+ const projects = await this.list({});
572
+ const matching = [];
573
+ for (const project of projects) try {
574
+ if ((await project.getItemsByStatus(status)).length > 0) matching.push(project);
575
+ } catch (error) {
576
+ logger$3.warn(`Error accessing items for project ${project.projectId}`, { error: error instanceof Error ? error.message : error });
577
+ }
578
+ return matching;
579
+ }
580
+ /**
581
+ * Get project statistics
582
+ *
583
+ * @param projectId - Project ID
584
+ * @returns Statistics object
585
+ */
586
+ async getStatistics(projectId) {
587
+ const project = await this.findOne({ where: { projectId } });
588
+ if (!project) throw new Error(`Project ${projectId} not found`);
589
+ const items = await project.listItems();
590
+ const statuses = await project.getStatuses();
591
+ const itemsByStatus = {};
592
+ for (const status of statuses) itemsByStatus[status.name] = 0;
593
+ const itemsByType = {
594
+ Issue: 0,
595
+ PullRequest: 0,
596
+ DraftIssue: 0
597
+ };
598
+ for (const item of items) {
599
+ if (item.status && itemsByStatus[item.status] !== void 0) itemsByStatus[item.status]++;
600
+ if (itemsByType[item.type] !== void 0) itemsByType[item.type]++;
601
+ }
602
+ return {
603
+ totalItems: items.length,
604
+ itemsByStatus,
605
+ itemsByType
606
+ };
607
+ }
608
+ /**
609
+ * Find projects by tenant ID
610
+ *
611
+ * @param tenantId - Tenant ID to filter by
612
+ * @returns Array of projects for the tenant
613
+ */
614
+ async findByTenant(tenantId) {
615
+ return this.list({ where: { tenantId } });
616
+ }
617
+ /**
618
+ * Find all global projects (no tenant association).
619
+ *
620
+ * Routes through the shared tenant-global helper so it does not throw under
621
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
622
+ * flagged as an isolation violation). (#1600)
623
+ *
624
+ * @returns Array of global projects
625
+ */
626
+ async findGlobal() {
627
+ return queryGlobal(this);
628
+ }
629
+ /**
630
+ * Find projects for a tenant plus all global projects.
631
+ *
632
+ * Fails closed if an active tenant context requests a different tenant's
633
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
634
+ *
635
+ * @param tenantId - Tenant ID to filter by
636
+ * @returns Array of tenant and global projects
637
+ */
638
+ async findWithGlobals(tenantId) {
639
+ return queryWithGlobals(this, tenantId, "Project.findWithGlobals");
640
+ }
1253
641
  };
1254
- let PullRequest = class extends Issue {
1255
- /**
1256
- * Source branch ref
1257
- */
1258
- headRef = "";
1259
- /**
1260
- * Target branch ref
1261
- */
1262
- baseRef = "";
1263
- /**
1264
- * Whether the PR has been merged
1265
- */
1266
- merged = false;
1267
- /**
1268
- * When the PR was merged
1269
- */
1270
- mergedAt = null;
1271
- /**
1272
- * Whether the PR can be merged
1273
- */
1274
- mergeable = true;
1275
- /**
1276
- * Whether this is a draft PR
1277
- */
1278
- draft = false;
1279
- /**
1280
- * Lines added
1281
- */
1282
- additions = 0;
1283
- /**
1284
- * Lines deleted
1285
- */
1286
- deletions = 0;
1287
- /**
1288
- * Number of files changed
1289
- */
1290
- changedFiles = 0;
1291
- constructor(options = {}) {
1292
- super(options);
1293
- if (options.headRef !== void 0) this.headRef = options.headRef;
1294
- if (options.baseRef !== void 0) this.baseRef = options.baseRef;
1295
- if (options.merged !== void 0) this.merged = options.merged;
1296
- if (options.mergedAt !== void 0) this.mergedAt = options.mergedAt;
1297
- if (options.mergeable !== void 0) this.mergeable = options.mergeable;
1298
- if (options.draft !== void 0) this.draft = options.draft;
1299
- if (options.additions !== void 0) this.additions = options.additions;
1300
- if (options.deletions !== void 0) this.deletions = options.deletions;
1301
- if (options.changedFiles !== void 0)
1302
- this.changedFiles = options.changedFiles;
1303
- }
1304
- /**
1305
- * Sync PR data from the provider
1306
- *
1307
- * @param options - Sync options
1308
- * @returns This PR with updated fields
1309
- */
1310
- async sync(options = {}) {
1311
- if (!options.force && this.lastSyncedAt && Date.now() - this.lastSyncedAt.getTime() < SYNC_THROTTLE_MS) {
1312
- return this;
1313
- }
1314
- const client = await this.getClient();
1315
- const prData = await client.getPullRequest(this.number);
1316
- this.nodeId = prData.id;
1317
- this.title = prData.title;
1318
- this.body = prData.body;
1319
- this.state = prData.state;
1320
- this.author = prData.author.login;
1321
- this.labels = prData.labels.map((l) => l.name);
1322
- this.assignees = prData.assignees.map((a) => a.login);
1323
- this.commentsCount = prData.commentsCount;
1324
- this.headRef = prData.headRef;
1325
- this.baseRef = prData.baseRef;
1326
- this.merged = prData.merged;
1327
- this.mergedAt = prData.mergedAt || null;
1328
- this.mergeable = prData.mergeable;
1329
- this.draft = prData.draft;
1330
- this.lastSyncedAt = /* @__PURE__ */ new Date();
1331
- await this.save();
1332
- return this;
1333
- }
1334
- /**
1335
- * AI-powered: Generate a summary of PR changes
1336
- *
1337
- * @returns Summary of what this PR does
1338
- */
1339
- async summarize() {
1340
- return await this.do(
1341
- `Summarize this pull request concisely.
1342
-
1343
- Title: ${this.title}
1344
- Description: ${this.body}
1345
-
1346
- Changes: ${this.additions} additions, ${this.deletions} deletions across ${this.changedFiles} files
1347
- Source: ${this.headRef} ${this.baseRef}
1348
-
1349
- Provide a 2-3 sentence summary focusing on:
1350
- 1. What the PR does
1351
- 2. Why it matters
1352
- 3. Any notable implementation details`,
1353
- // Title/body/stats hand-rolled above; skip do()'s object-data injection.
1354
- { includeData: false }
1355
- );
1356
- }
1357
- /**
1358
- * Merge this pull request
1359
- *
1360
- * @param method - Merge method (merge, squash, rebase)
1361
- */
1362
- async merge(method = "squash") {
1363
- if (this.merged) {
1364
- throw new Error("Pull request is already merged");
1365
- }
1366
- if (this.draft) {
1367
- throw new Error("Cannot merge a draft pull request");
1368
- }
1369
- if (!this.mergeable) {
1370
- throw new Error("Pull request is not mergeable");
1371
- }
1372
- const client = await this.getClient();
1373
- await client.mergePullRequest(this.number, method);
1374
- this.merged = true;
1375
- this.mergedAt = /* @__PURE__ */ new Date();
1376
- this.state = "closed";
1377
- this.lastSyncedAt = /* @__PURE__ */ new Date();
1378
- await this.save();
1379
- }
1380
- /**
1381
- * Mark this draft PR as ready for review
1382
- */
1383
- async markReady() {
1384
- if (!this.draft) {
1385
- throw new Error("Pull request is not a draft");
1386
- }
1387
- const client = await this.getClient();
1388
- await client.markPRReady(this.number);
1389
- this.draft = false;
1390
- this.lastSyncedAt = /* @__PURE__ */ new Date();
1391
- await this.save();
1392
- }
1393
- /**
1394
- * Convert this PR back to draft
1395
- */
1396
- async convertToDraft() {
1397
- if (this.draft) {
1398
- throw new Error("Pull request is already a draft");
1399
- }
1400
- const client = await this.getClient();
1401
- await client.convertPRToDraft(this.number);
1402
- this.draft = true;
1403
- this.lastSyncedAt = /* @__PURE__ */ new Date();
1404
- await this.save();
1405
- }
1406
- /**
1407
- * Request review from specified users
1408
- *
1409
- * @param reviewers - User logins to request review from
1410
- */
1411
- async requestReviewers(reviewers) {
1412
- const client = await this.getClient();
1413
- await client.requestReview(this.number, reviewers);
1414
- }
1415
- /**
1416
- * Find related issue for this PR
1417
- *
1418
- * @returns Related Issue or null
1419
- */
1420
- async findLinkedIssue() {
1421
- const client = await this.getClient();
1422
- const issue = await client.findIssueForPR(this.number);
1423
- if (!issue) {
1424
- return null;
1425
- }
1426
- const { IssueCollection: IssueCollection2 } = await Promise.resolve().then(() => Issues);
1427
- const collection = await IssueCollection2.create(this.options);
1428
- return await collection.findOne({
1429
- where: { repositoryId: this.repositoryId, number: issue.number }
1430
- });
1431
- }
1432
- /**
1433
- * AI-powered: Check if this PR is ready to merge
1434
- *
1435
- * @returns True if the PR appears ready
1436
- */
1437
- async isReadyToMerge() {
1438
- if (this.draft) return false;
1439
- if (!this.mergeable) return false;
1440
- if (this.state === "closed") return false;
1441
- return await this.is(
1442
- `This pull request is ready to merge because:
1443
- - It has a clear description of what it does
1444
- - It addresses a specific issue or feature
1445
- - The scope is appropriate (not too large)
1446
- - There are no unresolved review comments`
1447
- );
1448
- }
1449
- /**
1450
- * AI-powered: Suggest reviewers based on changed files
1451
- *
1452
- * @returns Array of suggested reviewer logins
1453
- */
1454
- async suggestReviewers() {
1455
- const suggestion = await this.do(
1456
- `Based on this pull request's title, description, and scope,
1457
- suggest who should review it.
1458
-
1459
- Title: ${this.title}
1460
- Description: ${this.body}
1461
- Changes: ${this.changedFiles} files changed
1462
-
1463
- Consider:
1464
- - Code owners for the affected areas
1465
- - Team members with relevant expertise
1466
- - People who have previously worked on related code
1467
-
1468
- Return only a comma-separated list of GitHub usernames, nothing else.
1469
- If you cannot determine reviewers, return an empty string.`,
1470
- // Title/description hand-rolled above; skip do()'s object-data injection.
1471
- { includeData: false }
1472
- );
1473
- return suggestion.split(",").map((r) => r.trim()).filter(Boolean);
1474
- }
1475
- /**
1476
- * Get PR URL
1477
- */
1478
- getUrl() {
1479
- const repo = this._repository;
1480
- if (repo) {
1481
- return `https://github.com/${repo.owner}/${repo.name}/pull/${this.number}`;
1482
- }
1483
- return "";
1484
- }
1485
- /**
1486
- * Get the change size classification
1487
- *
1488
- * @returns Size classification (xs, s, m, l, xl)
1489
- */
1490
- getChangeSize() {
1491
- const total = this.additions + this.deletions;
1492
- if (total < 10) return "xs";
1493
- if (total < 50) return "s";
1494
- if (total < 200) return "m";
1495
- if (total < 500) return "l";
1496
- return "xl";
1497
- }
642
+ //#endregion
643
+ //#region src/collections/PullRequests.ts
644
+ var PullRequests_exports = /* @__PURE__ */ __exportAll({ PullRequestCollection: () => PullRequestCollection });
645
+ var logger$2 = createLogger({ level: "info" });
646
+ var PullRequestCollection = class extends SmrtCollection {
647
+ static _itemClass = PullRequest;
648
+ /**
649
+ * Discover pull requests from a repository and sync to database
650
+ *
651
+ * @param options - Discovery options
652
+ * @returns Array of PullRequest objects
653
+ */
654
+ async discover(options) {
655
+ const { repository, filters } = options;
656
+ const repositoryId = repository.id ?? void 0;
657
+ if (!repositoryId) throw new Error("Repository must be saved before discovering pull requests");
658
+ const repoClient = await repository.getClient();
659
+ const remoteItems = await repoClient.searchIssues("is:pr", filters);
660
+ const pullRequests = [];
661
+ for (const remote of remoteItems) {
662
+ let prData;
663
+ try {
664
+ prData = await repoClient.getPullRequest(remote.number);
665
+ } catch (error) {
666
+ logger$2.warn(`Failed to fetch PR #${remote.number} from ${repository.owner}/${repository.name}`, { error: error instanceof Error ? error.message : error });
667
+ continue;
668
+ }
669
+ let pr = await this.findOne({ where: {
670
+ repositoryId,
671
+ number: remote.number
672
+ } });
673
+ if (!pr) pr = await this.create({
674
+ repositoryId,
675
+ number: prData.number,
676
+ nodeId: prData.id,
677
+ title: prData.title,
678
+ body: prData.body,
679
+ state: prData.state,
680
+ author: prData.author.login,
681
+ labels: prData.labels.map((l) => l.name),
682
+ assignees: prData.assignees.map((a) => a.login),
683
+ commentsCount: prData.commentsCount,
684
+ headRef: prData.headRef,
685
+ baseRef: prData.baseRef,
686
+ merged: prData.merged,
687
+ mergedAt: prData.mergedAt || null,
688
+ mergeable: prData.mergeable,
689
+ draft: prData.draft,
690
+ lastSyncedAt: /* @__PURE__ */ new Date()
691
+ });
692
+ else {
693
+ pr.nodeId = prData.id;
694
+ pr.title = prData.title;
695
+ pr.body = prData.body;
696
+ pr.state = prData.state;
697
+ pr.author = prData.author.login;
698
+ pr.labels = prData.labels.map((l) => l.name);
699
+ pr.assignees = prData.assignees.map((a) => a.login);
700
+ pr.commentsCount = prData.commentsCount;
701
+ pr.headRef = prData.headRef;
702
+ pr.baseRef = prData.baseRef;
703
+ pr.merged = prData.merged;
704
+ pr.mergedAt = prData.mergedAt || null;
705
+ pr.mergeable = prData.mergeable;
706
+ pr.draft = prData.draft;
707
+ pr.lastSyncedAt = /* @__PURE__ */ new Date();
708
+ }
709
+ await pr.save();
710
+ pullRequests.push(pr);
711
+ }
712
+ return pullRequests;
713
+ }
714
+ /**
715
+ * Find PRs by repository
716
+ *
717
+ * @param repositoryId - Repository ID
718
+ * @returns Array of PRs
719
+ */
720
+ async findByRepository(repositoryId) {
721
+ return await this.list({ where: { repositoryId } });
722
+ }
723
+ /**
724
+ * Find open PRs
725
+ *
726
+ * @param repositoryId - Optional repository filter
727
+ * @returns Array of open PRs
728
+ */
729
+ async findOpen(repositoryId) {
730
+ const where = { state: "open" };
731
+ if (repositoryId) where.repositoryId = repositoryId;
732
+ return await this.list({ where });
733
+ }
734
+ /**
735
+ * Find draft PRs
736
+ *
737
+ * @param repositoryId - Optional repository filter
738
+ * @returns Array of draft PRs
739
+ */
740
+ async findDrafts(repositoryId) {
741
+ return (await this.findOpen(repositoryId)).filter((pr) => pr.draft);
742
+ }
743
+ /**
744
+ * Find PRs ready to merge
745
+ *
746
+ * @param repositoryId - Optional repository filter
747
+ * @returns Array of mergeable PRs
748
+ */
749
+ async findReadyToMerge(repositoryId) {
750
+ return (await this.findOpen(repositoryId)).filter((pr) => !pr.draft && pr.mergeable);
751
+ }
752
+ /**
753
+ * Find PRs by branch
754
+ *
755
+ * @param branch - Branch name (head or base)
756
+ * @param repositoryId - Optional repository filter
757
+ * @returns Array of PRs
758
+ */
759
+ async findByBranch(branch, repositoryId) {
760
+ return (await this.list({ where: repositoryId ? { repositoryId } : {} })).filter((pr) => pr.headRef === branch || pr.baseRef === branch);
761
+ }
762
+ /**
763
+ * Find PR by number in a repository
764
+ *
765
+ * @param repositoryId - Repository ID
766
+ * @param number - PR number
767
+ * @returns PullRequest or null
768
+ */
769
+ async findByNumber(repositoryId, number) {
770
+ return (await this.list({
771
+ where: {
772
+ repositoryId,
773
+ number
774
+ },
775
+ limit: 1
776
+ }))[0] || null;
777
+ }
778
+ /**
779
+ * Find PRs ready to merge (AI-powered)
780
+ *
781
+ * @param repositoryId - Optional repository filter
782
+ * @returns Array of PRs that are ready
783
+ */
784
+ async findAIReadyToMerge(repositoryId) {
785
+ const openPRs = await this.findOpen(repositoryId);
786
+ const ready = [];
787
+ for (const pr of openPRs) if (await pr.isReadyToMerge()) ready.push(pr);
788
+ return ready;
789
+ }
790
+ /**
791
+ * Get PRs by change size
792
+ *
793
+ * @param size - Size classification
794
+ * @param repositoryId - Optional repository filter
795
+ * @returns Array of PRs
796
+ */
797
+ async findBySize(size, repositoryId) {
798
+ return (await this.findOpen(repositoryId)).filter((pr) => pr.getChangeSize() === size);
799
+ }
800
+ /**
801
+ * Batch sync PRs from repository
802
+ *
803
+ * @param repository - Repository to sync from
804
+ * @param options - Sync options
805
+ * @returns Array of synced PRs
806
+ */
807
+ async batchSync(repository, options = {}) {
808
+ const prs = await this.findByRepository(repository.id);
809
+ const synced = [];
810
+ for (const pr of prs) {
811
+ await pr.sync(options);
812
+ synced.push(pr);
813
+ }
814
+ return synced;
815
+ }
816
+ /**
817
+ * Find pull requests by tenant ID
818
+ *
819
+ * @param tenantId - Tenant ID to filter by
820
+ * @returns Array of pull requests for the tenant
821
+ */
822
+ async findByTenant(tenantId) {
823
+ return this.list({ where: { tenantId } });
824
+ }
825
+ /**
826
+ * Find all global pull requests (no tenant association).
827
+ *
828
+ * Routes through the shared tenant-global helper so it does not throw under
829
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
830
+ * flagged as an isolation violation). (#1600)
831
+ *
832
+ * @returns Array of global pull requests
833
+ */
834
+ async findGlobal() {
835
+ return queryGlobal(this);
836
+ }
837
+ /**
838
+ * Find pull requests for a tenant plus all global pull requests.
839
+ *
840
+ * Fails closed if an active tenant context requests a different tenant's
841
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
842
+ *
843
+ * @param tenantId - Tenant ID to filter by
844
+ * @returns Array of tenant and global pull requests
845
+ */
846
+ async findWithGlobals(tenantId) {
847
+ return queryWithGlobals(this, tenantId, "PullRequest.findWithGlobals");
848
+ }
1498
849
  };
1499
- PullRequest = __decorateClass$3([
1500
- TenantScoped({ mode: "optional" }),
1501
- smrt({
1502
- api: { include: ["list", "get", "create", "update"] },
1503
- mcp: { include: ["list", "get", "sync", "summarize", "merge"] },
1504
- // sync/summarize/merge/markReady are operator commands invoked in-process
1505
- // via the CLI; they intentionally aren't exposed over HTTP today.
1506
- cli: {
1507
- include: ["list", "get", "sync", "summarize", "merge", "markReady"],
1508
- skipApiCheck: true
1509
- }
1510
- })
1511
- ], PullRequest);
1512
- const PullRequest$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1513
- __proto__: null,
1514
- get PullRequest() {
1515
- return PullRequest;
1516
- }
1517
- }, Symbol.toStringTag, { value: "Module" }));
1518
- const logger$2 = createLogger({ level: "info" });
1519
- class PullRequestCollection extends SmrtCollection {
1520
- static _itemClass = PullRequest;
1521
- /**
1522
- * Discover pull requests from a repository and sync to database
1523
- *
1524
- * @param options - Discovery options
1525
- * @returns Array of PullRequest objects
1526
- */
1527
- async discover(options) {
1528
- const { repository, filters } = options;
1529
- const repositoryId = repository.id ?? void 0;
1530
- if (!repositoryId) {
1531
- throw new Error(
1532
- "Repository must be saved before discovering pull requests"
1533
- );
1534
- }
1535
- const repoClient = await repository.getClient();
1536
- const remoteItems = await repoClient.searchIssues("is:pr", filters);
1537
- const pullRequests = [];
1538
- for (const remote of remoteItems) {
1539
- let prData;
1540
- try {
1541
- prData = await repoClient.getPullRequest(remote.number);
1542
- } catch (error) {
1543
- logger$2.warn(
1544
- `Failed to fetch PR #${remote.number} from ${repository.owner}/${repository.name}`,
1545
- { error: error instanceof Error ? error.message : error }
1546
- );
1547
- continue;
1548
- }
1549
- let pr = await this.findOne({
1550
- where: {
1551
- repositoryId,
1552
- number: remote.number
1553
- }
1554
- });
1555
- if (!pr) {
1556
- pr = await this.create({
1557
- repositoryId,
1558
- number: prData.number,
1559
- nodeId: prData.id,
1560
- title: prData.title,
1561
- body: prData.body,
1562
- state: prData.state,
1563
- author: prData.author.login,
1564
- labels: prData.labels.map((l) => l.name),
1565
- assignees: prData.assignees.map((a) => a.login),
1566
- commentsCount: prData.commentsCount,
1567
- headRef: prData.headRef,
1568
- baseRef: prData.baseRef,
1569
- merged: prData.merged,
1570
- mergedAt: prData.mergedAt || null,
1571
- mergeable: prData.mergeable,
1572
- draft: prData.draft,
1573
- lastSyncedAt: /* @__PURE__ */ new Date()
1574
- });
1575
- } else {
1576
- pr.nodeId = prData.id;
1577
- pr.title = prData.title;
1578
- pr.body = prData.body;
1579
- pr.state = prData.state;
1580
- pr.author = prData.author.login;
1581
- pr.labels = prData.labels.map((l) => l.name);
1582
- pr.assignees = prData.assignees.map((a) => a.login);
1583
- pr.commentsCount = prData.commentsCount;
1584
- pr.headRef = prData.headRef;
1585
- pr.baseRef = prData.baseRef;
1586
- pr.merged = prData.merged;
1587
- pr.mergedAt = prData.mergedAt || null;
1588
- pr.mergeable = prData.mergeable;
1589
- pr.draft = prData.draft;
1590
- pr.lastSyncedAt = /* @__PURE__ */ new Date();
1591
- }
1592
- await pr.save();
1593
- pullRequests.push(pr);
1594
- }
1595
- return pullRequests;
1596
- }
1597
- /**
1598
- * Find PRs by repository
1599
- *
1600
- * @param repositoryId - Repository ID
1601
- * @returns Array of PRs
1602
- */
1603
- async findByRepository(repositoryId) {
1604
- return await this.list({
1605
- where: { repositoryId }
1606
- });
1607
- }
1608
- /**
1609
- * Find open PRs
1610
- *
1611
- * @param repositoryId - Optional repository filter
1612
- * @returns Array of open PRs
1613
- */
1614
- async findOpen(repositoryId) {
1615
- const where = { state: "open" };
1616
- if (repositoryId) {
1617
- where.repositoryId = repositoryId;
1618
- }
1619
- return await this.list({ where });
1620
- }
1621
- /**
1622
- * Find draft PRs
1623
- *
1624
- * @param repositoryId - Optional repository filter
1625
- * @returns Array of draft PRs
1626
- */
1627
- async findDrafts(repositoryId) {
1628
- const openPRs = await this.findOpen(repositoryId);
1629
- return openPRs.filter((pr) => pr.draft);
1630
- }
1631
- /**
1632
- * Find PRs ready to merge
1633
- *
1634
- * @param repositoryId - Optional repository filter
1635
- * @returns Array of mergeable PRs
1636
- */
1637
- async findReadyToMerge(repositoryId) {
1638
- const openPRs = await this.findOpen(repositoryId);
1639
- return openPRs.filter((pr) => !pr.draft && pr.mergeable);
1640
- }
1641
- /**
1642
- * Find PRs by branch
1643
- *
1644
- * @param branch - Branch name (head or base)
1645
- * @param repositoryId - Optional repository filter
1646
- * @returns Array of PRs
1647
- */
1648
- async findByBranch(branch, repositoryId) {
1649
- const prs = await this.list({
1650
- where: repositoryId ? { repositoryId } : {}
1651
- });
1652
- return prs.filter((pr) => pr.headRef === branch || pr.baseRef === branch);
1653
- }
1654
- /**
1655
- * Find PR by number in a repository
1656
- *
1657
- * @param repositoryId - Repository ID
1658
- * @param number - PR number
1659
- * @returns PullRequest or null
1660
- */
1661
- async findByNumber(repositoryId, number) {
1662
- const results = await this.list({
1663
- where: { repositoryId, number },
1664
- limit: 1
1665
- });
1666
- return results[0] || null;
1667
- }
1668
- /**
1669
- * Find PRs ready to merge (AI-powered)
1670
- *
1671
- * @param repositoryId - Optional repository filter
1672
- * @returns Array of PRs that are ready
1673
- */
1674
- async findAIReadyToMerge(repositoryId) {
1675
- const openPRs = await this.findOpen(repositoryId);
1676
- const ready = [];
1677
- for (const pr of openPRs) {
1678
- if (await pr.isReadyToMerge()) {
1679
- ready.push(pr);
1680
- }
1681
- }
1682
- return ready;
1683
- }
1684
- /**
1685
- * Get PRs by change size
1686
- *
1687
- * @param size - Size classification
1688
- * @param repositoryId - Optional repository filter
1689
- * @returns Array of PRs
1690
- */
1691
- async findBySize(size, repositoryId) {
1692
- const prs = await this.findOpen(repositoryId);
1693
- return prs.filter((pr) => pr.getChangeSize() === size);
1694
- }
1695
- /**
1696
- * Batch sync PRs from repository
1697
- *
1698
- * @param repository - Repository to sync from
1699
- * @param options - Sync options
1700
- * @returns Array of synced PRs
1701
- */
1702
- async batchSync(repository, options = {}) {
1703
- const prs = await this.findByRepository(repository.id);
1704
- const synced = [];
1705
- for (const pr of prs) {
1706
- await pr.sync(options);
1707
- synced.push(pr);
1708
- }
1709
- return synced;
1710
- }
1711
- /**
1712
- * Find pull requests by tenant ID
1713
- *
1714
- * @param tenantId - Tenant ID to filter by
1715
- * @returns Array of pull requests for the tenant
1716
- */
1717
- async findByTenant(tenantId2) {
1718
- return this.list({ where: { tenantId: tenantId2 } });
1719
- }
1720
- /**
1721
- * Find all global pull requests (no tenant association).
1722
- *
1723
- * Routes through the shared tenant-global helper so it does not throw under
1724
- * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1725
- * flagged as an isolation violation). (#1600)
1726
- *
1727
- * @returns Array of global pull requests
1728
- */
1729
- async findGlobal() {
1730
- return queryGlobal(this);
1731
- }
1732
- /**
1733
- * Find pull requests for a tenant plus all global pull requests.
1734
- *
1735
- * Fails closed if an active tenant context requests a different tenant's
1736
- * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1737
- *
1738
- * @param tenantId - Tenant ID to filter by
1739
- * @returns Array of tenant and global pull requests
1740
- */
1741
- async findWithGlobals(tenantId2) {
1742
- return queryWithGlobals(
1743
- this,
1744
- tenantId2,
1745
- "PullRequest.findWithGlobals"
1746
- );
1747
- }
1748
- }
1749
- const PullRequests = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1750
- __proto__: null,
1751
- PullRequestCollection
1752
- }, Symbol.toStringTag, { value: "Module" }));
850
+ //#endregion
851
+ //#region src/models/Repository.ts
1753
852
  var __defProp$2 = Object.defineProperty;
1754
853
  var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
1755
854
  var __decorateClass$2 = (decorators, target, key, kind) => {
1756
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
1757
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
1758
- if (decorator = decorators[i])
1759
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1760
- if (kind && result) __defProp$2(target, key, result);
1761
- return result;
855
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
856
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
857
+ if (kind && result) __defProp$2(target, key, result);
858
+ return result;
859
+ };
860
+ var Repository = class extends SmrtObject {
861
+ tenantId = null;
862
+ /**
863
+ * Repository owner (organization or user)
864
+ */
865
+ owner = "";
866
+ /**
867
+ * Repository name
868
+ */
869
+ name = "";
870
+ /**
871
+ * Full name in owner/repo format
872
+ */
873
+ fullName = "";
874
+ /**
875
+ * Repository description
876
+ */
877
+ description = "";
878
+ /**
879
+ * Default branch name
880
+ */
881
+ defaultBranch = "main";
882
+ /**
883
+ * Whether repository is private
884
+ */
885
+ isPrivate = false;
886
+ /**
887
+ * Repository provider type
888
+ */
889
+ providerType = "github";
890
+ /**
891
+ * Base URL for self-hosted instances (GitHub Enterprise, GitLab self-hosted, etc.)
892
+ */
893
+ baseUrl = "";
894
+ /**
895
+ * Environment variable name or config key for token resolution
896
+ * The token is NOT stored - only the key name is persisted
897
+ */
898
+ tokenConfigKey = "GITHUB_TOKEN";
899
+ /**
900
+ * Last sync timestamp
901
+ */
902
+ lastSyncedAt = null;
903
+ /**
904
+ * Transient: Cached repository client (not persisted)
905
+ */
906
+ _client;
907
+ constructor(options = {}) {
908
+ super(options);
909
+ if (options.owner !== void 0) this.owner = options.owner;
910
+ if (options.name !== void 0) this.name = options.name;
911
+ if (options.fullName !== void 0) this.fullName = options.fullName;
912
+ if (options.description !== void 0) this.description = options.description;
913
+ if (options.defaultBranch !== void 0) this.defaultBranch = options.defaultBranch;
914
+ if (options.isPrivate !== void 0) this.isPrivate = options.isPrivate;
915
+ if (options.providerType !== void 0) this.providerType = options.providerType;
916
+ if (options.baseUrl !== void 0) this.baseUrl = options.baseUrl;
917
+ if (options.tokenConfigKey !== void 0) this.tokenConfigKey = options.tokenConfigKey;
918
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
919
+ }
920
+ /**
921
+ * Get the repository client, resolving token from config
922
+ *
923
+ * Token resolution order:
924
+ * 1. Environment variable matching tokenConfigKey
925
+ * 2. Module config value matching tokenConfigKey
926
+ *
927
+ * @returns Repository client for API operations
928
+ * @throws Error if token cannot be resolved
929
+ */
930
+ async getClient() {
931
+ if (this._client) return this._client;
932
+ const token = process.env[this.tokenConfigKey] || getModuleConfig("smrt-projects", {})[this.tokenConfigKey];
933
+ if (!token) throw new Error(`Token not found for key '${this.tokenConfigKey}'. Set the ${this.tokenConfigKey} environment variable or configure it in smrt.config.`);
934
+ this._client = await getRepository({
935
+ type: this.providerType,
936
+ owner: this.owner,
937
+ repo: this.name,
938
+ token,
939
+ baseUrl: this.baseUrl || void 0
940
+ });
941
+ return this._client;
942
+ }
943
+ /**
944
+ * Clear the cached client (useful after token refresh)
945
+ */
946
+ clearClient() {
947
+ this._client = void 0;
948
+ }
949
+ /**
950
+ * Sync repository metadata from the provider
951
+ *
952
+ * @param options - Sync options
953
+ * @returns This repository with updated fields
954
+ */
955
+ async sync(options = {}) {
956
+ if (!options.force && this.lastSyncedAt && Date.now() - this.lastSyncedAt.getTime() < 3e5) return this;
957
+ const repoData = await (await this.getClient()).getRepository();
958
+ this.owner = repoData.owner;
959
+ this.name = repoData.name;
960
+ this.fullName = `${repoData.owner}/${repoData.name}`;
961
+ this.description = repoData.description;
962
+ this.defaultBranch = repoData.defaultBranch;
963
+ this.isPrivate = repoData.isPrivate;
964
+ this.lastSyncedAt = /* @__PURE__ */ new Date();
965
+ await this.save();
966
+ return this;
967
+ }
968
+ /**
969
+ * Get issues from this repository
970
+ *
971
+ * @param filters - Optional search filters
972
+ * @returns Array of Issue objects (SMRT models)
973
+ */
974
+ async getIssues(filters) {
975
+ const { IssueCollection } = await Promise.resolve().then(() => Issues_exports);
976
+ return await (await IssueCollection.create(this.options)).discover({
977
+ repository: this,
978
+ filters
979
+ });
980
+ }
981
+ /**
982
+ * Get pull requests from this repository
983
+ *
984
+ * @param filters - Optional search filters
985
+ * @returns Array of PullRequest objects (SMRT models)
986
+ */
987
+ async getPullRequests(filters) {
988
+ const { PullRequestCollection } = await Promise.resolve().then(() => PullRequests_exports);
989
+ return await (await PullRequestCollection.create(this.options)).discover({
990
+ repository: this,
991
+ filters
992
+ });
993
+ }
994
+ /**
995
+ * Create a new issue in this repository
996
+ *
997
+ * @param data - Issue creation data
998
+ * @returns Created Issue (SMRT model)
999
+ */
1000
+ async createIssue(data) {
1001
+ const repositoryId = this.id ?? void 0;
1002
+ if (!repositoryId) throw new Error("Repository must be saved before creating issues");
1003
+ const created = await (await this.getClient()).createIssue(data);
1004
+ const { Issue } = await import("./chunks/Issue-DITLxBl7.js").then((n) => n.n);
1005
+ const issue = new Issue({
1006
+ ...this.options,
1007
+ repositoryId,
1008
+ number: created.number,
1009
+ nodeId: created.id,
1010
+ title: created.title,
1011
+ body: created.body,
1012
+ state: created.state,
1013
+ author: created.author.login,
1014
+ labels: created.labels.map((l) => l.name),
1015
+ assignees: created.assignees.map((a) => a.login),
1016
+ commentsCount: created.commentsCount,
1017
+ lastSyncedAt: /* @__PURE__ */ new Date()
1018
+ });
1019
+ await issue.initialize();
1020
+ await issue.save();
1021
+ return issue;
1022
+ }
1023
+ /**
1024
+ * Create a new pull request in this repository
1025
+ *
1026
+ * @param data - PR creation data
1027
+ * @returns Created PullRequest (SMRT model)
1028
+ */
1029
+ async createPullRequest(data) {
1030
+ const repositoryId = this.id ?? void 0;
1031
+ if (!repositoryId) throw new Error("Repository must be saved before creating pull requests");
1032
+ const created = await (await this.getClient()).createPullRequest(data);
1033
+ const { PullRequest } = await import("./chunks/PullRequest-C6mck19s.js").then((n) => n.n);
1034
+ const pr = new PullRequest({
1035
+ ...this.options,
1036
+ repositoryId,
1037
+ number: created.number,
1038
+ nodeId: created.id,
1039
+ title: created.title,
1040
+ body: created.body,
1041
+ state: created.state,
1042
+ author: created.author.login,
1043
+ labels: created.labels.map((l) => l.name),
1044
+ assignees: created.assignees.map((a) => a.login),
1045
+ commentsCount: created.commentsCount,
1046
+ headRef: created.headRef,
1047
+ baseRef: created.baseRef,
1048
+ merged: created.merged,
1049
+ draft: created.draft,
1050
+ lastSyncedAt: /* @__PURE__ */ new Date()
1051
+ });
1052
+ await pr.initialize();
1053
+ await pr.save();
1054
+ return pr;
1055
+ }
1056
+ /**
1057
+ * Check if this repository has any open issues matching criteria
1058
+ *
1059
+ * @param criteria - Natural language description of what to check
1060
+ * @returns True if matching issues exist
1061
+ */
1062
+ async hasOpenIssuesMatching(criteria) {
1063
+ const issues = await this.getIssues({ state: "open" });
1064
+ if (issues.length === 0) return false;
1065
+ return await this.is(`This repository has open issues matching: ${criteria}. Current open issues: ${issues.map((i) => `#${i.number}: ${i.title}`).join(", ")}`);
1066
+ }
1067
+ /**
1068
+ * Generate a summary of repository activity
1069
+ *
1070
+ * @returns AI-generated summary
1071
+ */
1072
+ async summarizeActivity() {
1073
+ const issues = await this.getIssues({
1074
+ state: "open",
1075
+ limit: 10
1076
+ });
1077
+ const prs = await this.getPullRequests({
1078
+ state: "open",
1079
+ limit: 10
1080
+ });
1081
+ return await this.do(`Summarize the current activity in this repository. Open issues: ${issues.map((i) => `#${i.number}: ${i.title}`).join(", ")}. Open PRs: ${prs.map((p) => `#${p.number}: ${p.title}`).join(", ")}.`);
1082
+ }
1083
+ /**
1084
+ * Get repository by owner and name
1085
+ *
1086
+ * @param owner - Repository owner
1087
+ * @param name - Repository name
1088
+ * @param options - Additional options
1089
+ * @returns Repository or null if not found
1090
+ */
1091
+ static async getByFullName(owner, name, options = {}) {
1092
+ const { RepositoryCollection } = await Promise.resolve().then(() => Repositories_exports);
1093
+ return await (await RepositoryCollection.create(options)).findByFullName(owner, name);
1094
+ }
1762
1095
  };
1763
- let Repository = class extends SmrtObject {
1764
- tenantId = null;
1765
- /**
1766
- * Repository owner (organization or user)
1767
- */
1768
- owner = "";
1769
- /**
1770
- * Repository name
1771
- */
1772
- name = "";
1773
- /**
1774
- * Full name in owner/repo format
1775
- */
1776
- fullName = "";
1777
- /**
1778
- * Repository description
1779
- */
1780
- description = "";
1781
- /**
1782
- * Default branch name
1783
- */
1784
- defaultBranch = "main";
1785
- /**
1786
- * Whether repository is private
1787
- */
1788
- isPrivate = false;
1789
- /**
1790
- * Repository provider type
1791
- */
1792
- providerType = "github";
1793
- /**
1794
- * Base URL for self-hosted instances (GitHub Enterprise, GitLab self-hosted, etc.)
1795
- */
1796
- baseUrl = "";
1797
- /**
1798
- * Environment variable name or config key for token resolution
1799
- * The token is NOT stored - only the key name is persisted
1800
- */
1801
- tokenConfigKey = "GITHUB_TOKEN";
1802
- /**
1803
- * Last sync timestamp
1804
- */
1805
- lastSyncedAt = null;
1806
- /**
1807
- * Transient: Cached repository client (not persisted)
1808
- */
1809
- _client;
1810
- constructor(options = {}) {
1811
- super(options);
1812
- if (options.owner !== void 0) this.owner = options.owner;
1813
- if (options.name !== void 0) this.name = options.name;
1814
- if (options.fullName !== void 0) this.fullName = options.fullName;
1815
- if (options.description !== void 0)
1816
- this.description = options.description;
1817
- if (options.defaultBranch !== void 0)
1818
- this.defaultBranch = options.defaultBranch;
1819
- if (options.isPrivate !== void 0) this.isPrivate = options.isPrivate;
1820
- if (options.providerType !== void 0)
1821
- this.providerType = options.providerType;
1822
- if (options.baseUrl !== void 0) this.baseUrl = options.baseUrl;
1823
- if (options.tokenConfigKey !== void 0)
1824
- this.tokenConfigKey = options.tokenConfigKey;
1825
- if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1826
- }
1827
- /**
1828
- * Get the repository client, resolving token from config
1829
- *
1830
- * Token resolution order:
1831
- * 1. Environment variable matching tokenConfigKey
1832
- * 2. Module config value matching tokenConfigKey
1833
- *
1834
- * @returns Repository client for API operations
1835
- * @throws Error if token cannot be resolved
1836
- */
1837
- async getClient() {
1838
- if (this._client) {
1839
- return this._client;
1840
- }
1841
- const token = process.env[this.tokenConfigKey] || getModuleConfig("smrt-projects", {})[this.tokenConfigKey];
1842
- if (!token) {
1843
- throw new Error(
1844
- `Token not found for key '${this.tokenConfigKey}'. Set the ${this.tokenConfigKey} environment variable or configure it in smrt.config.`
1845
- );
1846
- }
1847
- this._client = await getRepository({
1848
- type: this.providerType,
1849
- owner: this.owner,
1850
- repo: this.name,
1851
- token,
1852
- baseUrl: this.baseUrl || void 0
1853
- });
1854
- return this._client;
1855
- }
1856
- /**
1857
- * Clear the cached client (useful after token refresh)
1858
- */
1859
- clearClient() {
1860
- this._client = void 0;
1861
- }
1862
- /**
1863
- * Sync repository metadata from the provider
1864
- *
1865
- * @param options - Sync options
1866
- * @returns This repository with updated fields
1867
- */
1868
- async sync(options = {}) {
1869
- if (!options.force && this.lastSyncedAt && Date.now() - this.lastSyncedAt.getTime() < SYNC_THROTTLE_MS) {
1870
- return this;
1871
- }
1872
- const client = await this.getClient();
1873
- const repoData = await client.getRepository();
1874
- this.owner = repoData.owner;
1875
- this.name = repoData.name;
1876
- this.fullName = `${repoData.owner}/${repoData.name}`;
1877
- this.description = repoData.description;
1878
- this.defaultBranch = repoData.defaultBranch;
1879
- this.isPrivate = repoData.isPrivate;
1880
- this.lastSyncedAt = /* @__PURE__ */ new Date();
1881
- await this.save();
1882
- return this;
1883
- }
1884
- /**
1885
- * Get issues from this repository
1886
- *
1887
- * @param filters - Optional search filters
1888
- * @returns Array of Issue objects (SMRT models)
1889
- */
1890
- async getIssues(filters) {
1891
- const { IssueCollection: IssueCollection2 } = await Promise.resolve().then(() => Issues);
1892
- const collection = await IssueCollection2.create(this.options);
1893
- return await collection.discover({ repository: this, filters });
1894
- }
1895
- /**
1896
- * Get pull requests from this repository
1897
- *
1898
- * @param filters - Optional search filters
1899
- * @returns Array of PullRequest objects (SMRT models)
1900
- */
1901
- async getPullRequests(filters) {
1902
- const { PullRequestCollection: PullRequestCollection2 } = await Promise.resolve().then(() => PullRequests);
1903
- const collection = await PullRequestCollection2.create(this.options);
1904
- return await collection.discover({ repository: this, filters });
1905
- }
1906
- /**
1907
- * Create a new issue in this repository
1908
- *
1909
- * @param data - Issue creation data
1910
- * @returns Created Issue (SMRT model)
1911
- */
1912
- async createIssue(data) {
1913
- const repositoryId = this.id ?? void 0;
1914
- if (!repositoryId) {
1915
- throw new Error("Repository must be saved before creating issues");
1916
- }
1917
- const client = await this.getClient();
1918
- const created = await client.createIssue(data);
1919
- const { Issue: Issue2 } = await Promise.resolve().then(() => Issue$1);
1920
- const issue = new Issue2({
1921
- ...this.options,
1922
- repositoryId,
1923
- number: created.number,
1924
- nodeId: created.id,
1925
- title: created.title,
1926
- body: created.body,
1927
- state: created.state,
1928
- author: created.author.login,
1929
- labels: created.labels.map((l) => l.name),
1930
- assignees: created.assignees.map((a) => a.login),
1931
- commentsCount: created.commentsCount,
1932
- lastSyncedAt: /* @__PURE__ */ new Date()
1933
- });
1934
- await issue.initialize();
1935
- await issue.save();
1936
- return issue;
1937
- }
1938
- /**
1939
- * Create a new pull request in this repository
1940
- *
1941
- * @param data - PR creation data
1942
- * @returns Created PullRequest (SMRT model)
1943
- */
1944
- async createPullRequest(data) {
1945
- const repositoryId = this.id ?? void 0;
1946
- if (!repositoryId) {
1947
- throw new Error("Repository must be saved before creating pull requests");
1948
- }
1949
- const client = await this.getClient();
1950
- const created = await client.createPullRequest(data);
1951
- const { PullRequest: PullRequest2 } = await Promise.resolve().then(() => PullRequest$1);
1952
- const pr = new PullRequest2({
1953
- ...this.options,
1954
- repositoryId,
1955
- number: created.number,
1956
- nodeId: created.id,
1957
- title: created.title,
1958
- body: created.body,
1959
- state: created.state,
1960
- author: created.author.login,
1961
- labels: created.labels.map((l) => l.name),
1962
- assignees: created.assignees.map((a) => a.login),
1963
- commentsCount: created.commentsCount,
1964
- headRef: created.headRef,
1965
- baseRef: created.baseRef,
1966
- merged: created.merged,
1967
- draft: created.draft,
1968
- lastSyncedAt: /* @__PURE__ */ new Date()
1969
- });
1970
- await pr.initialize();
1971
- await pr.save();
1972
- return pr;
1973
- }
1974
- /**
1975
- * Check if this repository has any open issues matching criteria
1976
- *
1977
- * @param criteria - Natural language description of what to check
1978
- * @returns True if matching issues exist
1979
- */
1980
- async hasOpenIssuesMatching(criteria) {
1981
- const issues = await this.getIssues({ state: "open" });
1982
- if (issues.length === 0) return false;
1983
- return await this.is(
1984
- `This repository has open issues matching: ${criteria}. Current open issues: ${issues.map((i) => `#${i.number}: ${i.title}`).join(", ")}`
1985
- );
1986
- }
1987
- /**
1988
- * Generate a summary of repository activity
1989
- *
1990
- * @returns AI-generated summary
1991
- */
1992
- async summarizeActivity() {
1993
- const issues = await this.getIssues({ state: "open", limit: 10 });
1994
- const prs = await this.getPullRequests({ state: "open", limit: 10 });
1995
- return await this.do(
1996
- `Summarize the current activity in this repository. Open issues: ${issues.map((i) => `#${i.number}: ${i.title}`).join(", ")}. Open PRs: ${prs.map((p) => `#${p.number}: ${p.title}`).join(", ")}.`
1997
- );
1998
- }
1999
- /**
2000
- * Get repository by owner and name
2001
- *
2002
- * @param owner - Repository owner
2003
- * @param name - Repository name
2004
- * @param options - Additional options
2005
- * @returns Repository or null if not found
2006
- */
2007
- static async getByFullName(owner, name, options = {}) {
2008
- const { RepositoryCollection: RepositoryCollection2 } = await Promise.resolve().then(() => Repositories);
2009
- const collection = await RepositoryCollection2.create(options);
2010
- return await collection.findByFullName(owner, name);
2011
- }
1096
+ __decorateClass$2([tenantId({ nullable: true })], Repository.prototype, "tenantId", 2);
1097
+ Repository = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
1098
+ api: { include: [
1099
+ "list",
1100
+ "get",
1101
+ "create",
1102
+ "update"
1103
+ ] },
1104
+ mcp: { include: [
1105
+ "list",
1106
+ "get",
1107
+ "sync"
1108
+ ] },
1109
+ cli: {
1110
+ include: [
1111
+ "list",
1112
+ "get",
1113
+ "sync",
1114
+ "create"
1115
+ ],
1116
+ skipApiCheck: true
1117
+ }
1118
+ })], Repository);
1119
+ //#endregion
1120
+ //#region src/collections/Repositories.ts
1121
+ var Repositories_exports = /* @__PURE__ */ __exportAll({ RepositoryCollection: () => RepositoryCollection });
1122
+ var logger$1 = createLogger({ level: "info" });
1123
+ var RepositoryCollection = class extends SmrtCollection {
1124
+ static _itemClass = Repository;
1125
+ /**
1126
+ * Find a repository by owner and name
1127
+ *
1128
+ * @param owner - Repository owner
1129
+ * @param name - Repository name
1130
+ * @returns Repository or null
1131
+ */
1132
+ async findByFullName(owner, name) {
1133
+ return (await this.list({
1134
+ where: {
1135
+ owner,
1136
+ name
1137
+ },
1138
+ limit: 1
1139
+ }))[0] || null;
1140
+ }
1141
+ /**
1142
+ * Find repositories by owner
1143
+ *
1144
+ * @param owner - Repository owner
1145
+ * @returns Array of repositories
1146
+ */
1147
+ async findByOwner(owner) {
1148
+ return await this.list({ where: { owner } });
1149
+ }
1150
+ /**
1151
+ * Find repositories by provider type
1152
+ *
1153
+ * @param providerType - Provider type
1154
+ * @returns Array of repositories
1155
+ */
1156
+ async findByProvider(providerType) {
1157
+ return await this.list({ where: { providerType } });
1158
+ }
1159
+ /**
1160
+ * Get or create a repository by owner/name
1161
+ *
1162
+ * @param owner - Repository owner
1163
+ * @param name - Repository name
1164
+ * @param options - Additional options for creation
1165
+ * @returns Repository (existing or newly created)
1166
+ */
1167
+ async getOrCreate(owner, name, options = {}) {
1168
+ let repo = await this.findByFullName(owner, name);
1169
+ if (!repo) {
1170
+ repo = await this.create({
1171
+ owner,
1172
+ name,
1173
+ fullName: `${owner}/${name}`,
1174
+ providerType: options.providerType || "github",
1175
+ tokenConfigKey: options.tokenConfigKey || "GITHUB_TOKEN",
1176
+ baseUrl: options.baseUrl || ""
1177
+ });
1178
+ await repo.save();
1179
+ }
1180
+ return repo;
1181
+ }
1182
+ /**
1183
+ * Sync all repositories
1184
+ *
1185
+ * @param options - Sync options
1186
+ * @returns Array of synced repositories
1187
+ */
1188
+ async syncAll(options = {}) {
1189
+ const repos = await this.list({});
1190
+ const synced = [];
1191
+ for (const repo of repos) {
1192
+ await repo.sync(options);
1193
+ synced.push(repo);
1194
+ }
1195
+ return synced;
1196
+ }
1197
+ /**
1198
+ * Find repositories with open issues
1199
+ *
1200
+ * @returns Array of repositories with at least one open issue
1201
+ */
1202
+ async findWithOpenIssues() {
1203
+ const repos = await this.list({});
1204
+ const withIssues = [];
1205
+ for (const repo of repos) try {
1206
+ if ((await repo.getIssues({
1207
+ state: "open",
1208
+ limit: 1
1209
+ })).length > 0) withIssues.push(repo);
1210
+ } catch (error) {
1211
+ logger$1.warn(`Error accessing issues for repository ${repo.owner}/${repo.name}`, { error: error instanceof Error ? error.message : error });
1212
+ }
1213
+ return withIssues;
1214
+ }
1215
+ /**
1216
+ * Find repositories by tenant ID
1217
+ *
1218
+ * @param tenantId - Tenant ID to filter by
1219
+ * @returns Array of repositories for the tenant
1220
+ */
1221
+ async findByTenant(tenantId) {
1222
+ return this.list({ where: { tenantId } });
1223
+ }
1224
+ /**
1225
+ * Find all global repositories (no tenant association).
1226
+ *
1227
+ * Routes through the shared tenant-global helper so it does not throw under
1228
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1229
+ * flagged as an isolation violation). (#1600)
1230
+ *
1231
+ * @returns Array of global repositories
1232
+ */
1233
+ async findGlobal() {
1234
+ return queryGlobal(this);
1235
+ }
1236
+ /**
1237
+ * Find repositories for a tenant plus all global repositories.
1238
+ *
1239
+ * Fails closed if an active tenant context requests a different tenant's
1240
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1241
+ *
1242
+ * @param tenantId - Tenant ID to filter by
1243
+ * @returns Array of tenant and global repositories
1244
+ */
1245
+ async findWithGlobals(tenantId) {
1246
+ return queryWithGlobals(this, tenantId, "Repository.findWithGlobals");
1247
+ }
2012
1248
  };
2013
- __decorateClass$2([
2014
- tenantId({ nullable: true })
2015
- ], Repository.prototype, "tenantId", 2);
2016
- Repository = __decorateClass$2([
2017
- TenantScoped({ mode: "optional" }),
2018
- smrt({
2019
- api: { include: ["list", "get", "create", "update"] },
2020
- mcp: { include: ["list", "get", "sync"] },
2021
- // sync is an operator command invoked in-process via the CLI;
2022
- // it intentionally isn't exposed over HTTP today.
2023
- cli: { include: ["list", "get", "sync", "create"], skipApiCheck: true }
2024
- })
2025
- ], Repository);
2026
- const logger$1 = createLogger({ level: "info" });
2027
- class RepositoryCollection extends SmrtCollection {
2028
- static _itemClass = Repository;
2029
- /**
2030
- * Find a repository by owner and name
2031
- *
2032
- * @param owner - Repository owner
2033
- * @param name - Repository name
2034
- * @returns Repository or null
2035
- */
2036
- async findByFullName(owner, name) {
2037
- const results = await this.list({
2038
- where: { owner, name },
2039
- limit: 1
2040
- });
2041
- return results[0] || null;
2042
- }
2043
- /**
2044
- * Find repositories by owner
2045
- *
2046
- * @param owner - Repository owner
2047
- * @returns Array of repositories
2048
- */
2049
- async findByOwner(owner) {
2050
- return await this.list({
2051
- where: { owner }
2052
- });
2053
- }
2054
- /**
2055
- * Find repositories by provider type
2056
- *
2057
- * @param providerType - Provider type
2058
- * @returns Array of repositories
2059
- */
2060
- async findByProvider(providerType) {
2061
- return await this.list({
2062
- where: { providerType }
2063
- });
2064
- }
2065
- /**
2066
- * Get or create a repository by owner/name
2067
- *
2068
- * @param owner - Repository owner
2069
- * @param name - Repository name
2070
- * @param options - Additional options for creation
2071
- * @returns Repository (existing or newly created)
2072
- */
2073
- async getOrCreate(owner, name, options = {}) {
2074
- let repo = await this.findByFullName(owner, name);
2075
- if (!repo) {
2076
- repo = await this.create({
2077
- owner,
2078
- name,
2079
- fullName: `${owner}/${name}`,
2080
- providerType: options.providerType || "github",
2081
- tokenConfigKey: options.tokenConfigKey || "GITHUB_TOKEN",
2082
- baseUrl: options.baseUrl || ""
2083
- });
2084
- await repo.save();
2085
- }
2086
- return repo;
2087
- }
2088
- /**
2089
- * Sync all repositories
2090
- *
2091
- * @param options - Sync options
2092
- * @returns Array of synced repositories
2093
- */
2094
- async syncAll(options = {}) {
2095
- const repos = await this.list({});
2096
- const synced = [];
2097
- for (const repo of repos) {
2098
- await repo.sync(options);
2099
- synced.push(repo);
2100
- }
2101
- return synced;
2102
- }
2103
- /**
2104
- * Find repositories with open issues
2105
- *
2106
- * @returns Array of repositories with at least one open issue
2107
- */
2108
- async findWithOpenIssues() {
2109
- const repos = await this.list({});
2110
- const withIssues = [];
2111
- for (const repo of repos) {
2112
- try {
2113
- const issues = await repo.getIssues({ state: "open", limit: 1 });
2114
- if (issues.length > 0) {
2115
- withIssues.push(repo);
2116
- }
2117
- } catch (error) {
2118
- logger$1.warn(
2119
- `Error accessing issues for repository ${repo.owner}/${repo.name}`,
2120
- { error: error instanceof Error ? error.message : error }
2121
- );
2122
- }
2123
- }
2124
- return withIssues;
2125
- }
2126
- /**
2127
- * Find repositories by tenant ID
2128
- *
2129
- * @param tenantId - Tenant ID to filter by
2130
- * @returns Array of repositories for the tenant
2131
- */
2132
- async findByTenant(tenantId2) {
2133
- return this.list({ where: { tenantId: tenantId2 } });
2134
- }
2135
- /**
2136
- * Find all global repositories (no tenant association).
2137
- *
2138
- * Routes through the shared tenant-global helper so it does not throw under
2139
- * an active tenant context (an explicit `tenant_id IS NULL` filter would be
2140
- * flagged as an isolation violation). (#1600)
2141
- *
2142
- * @returns Array of global repositories
2143
- */
2144
- async findGlobal() {
2145
- return queryGlobal(this);
2146
- }
2147
- /**
2148
- * Find repositories for a tenant plus all global repositories.
2149
- *
2150
- * Fails closed if an active tenant context requests a different tenant's
2151
- * rows; the admin/system path keeps the cross-tenant capability. (#1600)
2152
- *
2153
- * @param tenantId - Tenant ID to filter by
2154
- * @returns Array of tenant and global repositories
2155
- */
2156
- async findWithGlobals(tenantId2) {
2157
- return queryWithGlobals(
2158
- this,
2159
- tenantId2,
2160
- "Repository.findWithGlobals"
2161
- );
2162
- }
2163
- }
2164
- const Repositories = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2165
- __proto__: null,
2166
- RepositoryCollection
2167
- }, Symbol.toStringTag, { value: "Module" }));
1249
+ //#endregion
1250
+ //#region src/models/Comment.ts
1251
+ var Comment_exports = /* @__PURE__ */ __exportAll({ Comment: () => Comment });
2168
1252
  var __defProp$1 = Object.defineProperty;
2169
1253
  var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
2170
1254
  var __decorateClass$1 = (decorators, target, key, kind) => {
2171
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
2172
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
2173
- if (decorator = decorators[i])
2174
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
2175
- if (kind && result) __defProp$1(target, key, result);
2176
- return result;
1255
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
1256
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1257
+ if (kind && result) __defProp$1(target, key, result);
1258
+ return result;
2177
1259
  };
2178
- const logger = createLogger({ level: "info" });
2179
- let Comment = class extends SmrtObject {
2180
- tenantId = null;
2181
- issueId;
2182
- /**
2183
- * Provider-specific comment ID
2184
- */
2185
- commentId = "";
2186
- /**
2187
- * Comment body text
2188
- */
2189
- body = "";
2190
- /**
2191
- * Comment author's login
2192
- */
2193
- author = "";
2194
- /**
2195
- * When the comment was created
2196
- */
2197
- createdAt = null;
2198
- /**
2199
- * When the comment was last updated
2200
- */
2201
- updatedAt = null;
2202
- /**
2203
- * Comment URL
2204
- */
2205
- url = "";
2206
- constructor(options = {}) {
2207
- super(options);
2208
- if (options.issueId !== void 0) this.issueId = options.issueId;
2209
- if (options.commentId !== void 0) this.commentId = options.commentId;
2210
- if (options.body !== void 0) this.body = options.body;
2211
- if (options.author !== void 0) this.author = options.author;
2212
- if (options.createdAt !== void 0) this.createdAt = options.createdAt;
2213
- if (options.updatedAt !== void 0) this.updatedAt = options.updatedAt;
2214
- if (options.url !== void 0) this.url = options.url;
2215
- if (options.tenantId !== void 0) this.tenantId = options.tenantId;
2216
- }
2217
- /**
2218
- * AI-powered: Check if this comment contains a question
2219
- */
2220
- async isQuestion() {
2221
- return await this.is(
2222
- "This comment contains a question or request for clarification"
2223
- );
2224
- }
2225
- /**
2226
- * AI-powered: Check if this comment contains approval
2227
- */
2228
- async isApproval() {
2229
- return await this.is(
2230
- "This comment expresses approval, agreement, or a positive response (LGTM, +1, approved, etc.)"
2231
- );
2232
- }
2233
- /**
2234
- * AI-powered: Check if this comment requests changes
2235
- */
2236
- async requestsChanges() {
2237
- return await this.is(
2238
- "This comment requests changes, modifications, or improvements to the issue/PR"
2239
- );
2240
- }
2241
- /**
2242
- * AI-powered: Extract action items from this comment
2243
- *
2244
- * @returns Array of action items
2245
- */
2246
- async extractActionItems() {
2247
- const result = await this.do(
2248
- `Extract any action items, tasks, or requests from this comment.
1260
+ var logger = createLogger({ level: "info" });
1261
+ var Comment = class extends SmrtObject {
1262
+ tenantId = null;
1263
+ issueId;
1264
+ /**
1265
+ * Provider-specific comment ID
1266
+ */
1267
+ commentId = "";
1268
+ /**
1269
+ * Comment body text
1270
+ */
1271
+ body = "";
1272
+ /**
1273
+ * Comment author's login
1274
+ */
1275
+ author = "";
1276
+ /**
1277
+ * When the comment was created
1278
+ */
1279
+ createdAt = null;
1280
+ /**
1281
+ * When the comment was last updated
1282
+ */
1283
+ updatedAt = null;
1284
+ /**
1285
+ * Comment URL
1286
+ */
1287
+ url = "";
1288
+ constructor(options = {}) {
1289
+ super(options);
1290
+ if (options.issueId !== void 0) this.issueId = options.issueId;
1291
+ if (options.commentId !== void 0) this.commentId = options.commentId;
1292
+ if (options.body !== void 0) this.body = options.body;
1293
+ if (options.author !== void 0) this.author = options.author;
1294
+ if (options.createdAt !== void 0) this.createdAt = options.createdAt;
1295
+ if (options.updatedAt !== void 0) this.updatedAt = options.updatedAt;
1296
+ if (options.url !== void 0) this.url = options.url;
1297
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1298
+ }
1299
+ /**
1300
+ * AI-powered: Check if this comment contains a question
1301
+ */
1302
+ async isQuestion() {
1303
+ return await this.is("This comment contains a question or request for clarification");
1304
+ }
1305
+ /**
1306
+ * AI-powered: Check if this comment contains approval
1307
+ */
1308
+ async isApproval() {
1309
+ return await this.is("This comment expresses approval, agreement, or a positive response (LGTM, +1, approved, etc.)");
1310
+ }
1311
+ /**
1312
+ * AI-powered: Check if this comment requests changes
1313
+ */
1314
+ async requestsChanges() {
1315
+ return await this.is("This comment requests changes, modifications, or improvements to the issue/PR");
1316
+ }
1317
+ /**
1318
+ * AI-powered: Extract action items from this comment
1319
+ *
1320
+ * @returns Array of action items
1321
+ */
1322
+ async extractActionItems() {
1323
+ const result = await this.do(`Extract any action items, tasks, or requests from this comment.
2249
1324
  Return a JSON array of strings, each representing one action item.
2250
1325
  If no action items, return an empty array [].
2251
1326
  Only return the JSON array, nothing else.
2252
1327
 
2253
- Comment: ${this.body}`,
2254
- // Body is hand-rolled above; skip do()'s object-data injection (no dup).
2255
- { includeData: false }
2256
- );
2257
- try {
2258
- return JSON.parse(result);
2259
- } catch (error) {
2260
- logger.warn("Failed to parse action items JSON", {
2261
- error: error instanceof Error ? error.message : error,
2262
- response: result
2263
- });
2264
- return [];
2265
- }
2266
- }
2267
- /**
2268
- * AI-powered: Summarize this comment
2269
- *
2270
- * @returns Brief summary
2271
- */
2272
- async summarize() {
2273
- return await this.do(
2274
- `Summarize this comment in one sentence.
2275
- Comment by ${this.author}: ${this.body}`,
2276
- // Author + body hand-rolled above; skip do()'s object-data injection.
2277
- { includeData: false }
2278
- );
2279
- }
2280
- /**
2281
- * Get the sentiment of this comment
2282
- *
2283
- * @returns Sentiment classification
2284
- */
2285
- async getSentiment() {
2286
- const result = await this.do(
2287
- `Classify the sentiment of this comment as exactly one of: positive, negative, neutral
1328
+ Comment: ${this.body}`, { includeData: false });
1329
+ try {
1330
+ return JSON.parse(result);
1331
+ } catch (error) {
1332
+ logger.warn("Failed to parse action items JSON", {
1333
+ error: error instanceof Error ? error.message : error,
1334
+ response: result
1335
+ });
1336
+ return [];
1337
+ }
1338
+ }
1339
+ /**
1340
+ * AI-powered: Summarize this comment
1341
+ *
1342
+ * @returns Brief summary
1343
+ */
1344
+ async summarize() {
1345
+ return await this.do(`Summarize this comment in one sentence.
1346
+ Comment by ${this.author}: ${this.body}`, { includeData: false });
1347
+ }
1348
+ /**
1349
+ * Get the sentiment of this comment
1350
+ *
1351
+ * @returns Sentiment classification
1352
+ */
1353
+ async getSentiment() {
1354
+ const normalized = (await this.do(`Classify the sentiment of this comment as exactly one of: positive, negative, neutral
2288
1355
  Only return one word.
2289
- Comment: ${this.body}`,
2290
- // Body is hand-rolled above; skip do()'s object-data injection (no dup).
2291
- { includeData: false }
2292
- );
2293
- const normalized = result.toLowerCase().trim();
2294
- if (normalized.includes("positive")) return "positive";
2295
- if (normalized.includes("negative")) return "negative";
2296
- return "neutral";
2297
- }
1356
+ Comment: ${this.body}`, { includeData: false })).toLowerCase().trim();
1357
+ if (normalized.includes("positive")) return "positive";
1358
+ if (normalized.includes("negative")) return "negative";
1359
+ return "neutral";
1360
+ }
2298
1361
  };
2299
- __decorateClass$1([
2300
- tenantId({ nullable: true })
2301
- ], Comment.prototype, "tenantId", 2);
2302
- __decorateClass$1([
2303
- foreignKey("Issue")
2304
- ], Comment.prototype, "issueId", 2);
2305
- Comment = __decorateClass$1([
2306
- TenantScoped({ mode: "optional" }),
2307
- smrt({
2308
- api: { include: ["list", "get"] },
2309
- mcp: { include: ["list", "get"] },
2310
- cli: { include: ["list", "get"] }
2311
- })
2312
- ], Comment);
2313
- const Comment$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2314
- __proto__: null,
2315
- get Comment() {
2316
- return Comment;
2317
- }
2318
- }, Symbol.toStringTag, { value: "Module" }));
1362
+ __decorateClass$1([tenantId({ nullable: true })], Comment.prototype, "tenantId", 2);
1363
+ __decorateClass$1([foreignKey("Issue")], Comment.prototype, "issueId", 2);
1364
+ Comment = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
1365
+ api: { include: ["list", "get"] },
1366
+ mcp: { include: ["list", "get"] },
1367
+ cli: { include: ["list", "get"] }
1368
+ })], Comment);
1369
+ //#endregion
1370
+ //#region src/models/Label.ts
2319
1371
  var __defProp = Object.defineProperty;
2320
1372
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
2321
1373
  var __decorateClass = (decorators, target, key, kind) => {
2322
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
2323
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
2324
- if (decorator = decorators[i])
2325
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
2326
- if (kind && result) __defProp(target, key, result);
2327
- return result;
1374
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1375
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1376
+ if (kind && result) __defProp(target, key, result);
1377
+ return result;
2328
1378
  };
2329
- let Label = class extends SmrtObject {
2330
- repositoryId;
2331
- /**
2332
- * Label name
2333
- */
2334
- name = "";
2335
- /**
2336
- * Label color (hex without #)
2337
- */
2338
- color = "";
2339
- /**
2340
- * Label description
2341
- */
2342
- description = "";
2343
- constructor(options = {}) {
2344
- super(options);
2345
- if (options.repositoryId !== void 0)
2346
- this.repositoryId = options.repositoryId;
2347
- if (options.name !== void 0) this.name = options.name;
2348
- if (options.color !== void 0) this.color = options.color;
2349
- if (options.description !== void 0)
2350
- this.description = options.description;
2351
- }
2352
- /**
2353
- * Check if this is a type label (bug, feature, etc.)
2354
- */
2355
- isTypeLabel() {
2356
- const typePatterns = [
2357
- /^type:/i,
2358
- /^kind:/i,
2359
- /^bug$/i,
2360
- /^feature$/i,
2361
- /^enhancement$/i,
2362
- /^docs$/i,
2363
- /^chore$/i
2364
- ];
2365
- return typePatterns.some((p) => p.test(this.name));
2366
- }
2367
- /**
2368
- * Check if this is a priority label
2369
- */
2370
- isPriorityLabel() {
2371
- const priorityPatterns = [
2372
- /^p[0-4]$/i,
2373
- /^priority:/i,
2374
- /^critical$/i,
2375
- /^high$/i,
2376
- /^medium$/i,
2377
- /^low$/i
2378
- ];
2379
- return priorityPatterns.some((p) => p.test(this.name));
2380
- }
2381
- /**
2382
- * Check if this is a status label
2383
- */
2384
- isStatusLabel() {
2385
- const statusPatterns = [
2386
- /^status:/i,
2387
- /^wip$/i,
2388
- /^in.?progress$/i,
2389
- /^blocked$/i,
2390
- /^needs.?review$/i,
2391
- /^ready$/i
2392
- ];
2393
- return statusPatterns.some((p) => p.test(this.name));
2394
- }
2395
- /**
2396
- * Get the label category
2397
- *
2398
- * @returns Category name
2399
- */
2400
- getCategory() {
2401
- if (this.isTypeLabel()) return "type";
2402
- if (this.isPriorityLabel()) return "priority";
2403
- if (this.isStatusLabel()) return "status";
2404
- if (this.name.includes(":")) return "area";
2405
- return "other";
2406
- }
2407
- /**
2408
- * Parse priority level from label name
2409
- *
2410
- * @returns Priority level 0-4 or null
2411
- */
2412
- getPriorityLevel() {
2413
- const match = this.name.match(/p([0-4])/i);
2414
- if (match) {
2415
- return parseInt(match[1], 10);
2416
- }
2417
- if (/critical/i.test(this.name)) return 0;
2418
- if (/high/i.test(this.name)) return 1;
2419
- if (/medium/i.test(this.name)) return 2;
2420
- if (/low/i.test(this.name)) return 3;
2421
- return null;
2422
- }
2423
- /**
2424
- * Get label with # prefix for hex color
2425
- */
2426
- getHexColor() {
2427
- return this.color.startsWith("#") ? this.color : `#${this.color}`;
2428
- }
2429
- /**
2430
- * Create a label in the repository
2431
- */
2432
- async createInRepository() {
2433
- if (!this.repositoryId) {
2434
- throw new Error("Label must have a repositoryId");
2435
- }
2436
- const { RepositoryCollection: RepositoryCollection2 } = await Promise.resolve().then(() => Repositories);
2437
- const collection = await RepositoryCollection2.create(this.options);
2438
- const repo = await collection.get({ id: this.repositoryId });
2439
- if (!repo) {
2440
- throw new Error(`Repository ${this.repositoryId} not found`);
2441
- }
2442
- const client = await repo.getClient();
2443
- await client.createLabel({
2444
- name: this.name,
2445
- color: this.color,
2446
- description: this.description
2447
- });
2448
- await this.save();
2449
- }
2450
- /**
2451
- * Update this label in the repository
2452
- */
2453
- async updateInRepository() {
2454
- if (!this.repositoryId) {
2455
- throw new Error("Label must have a repositoryId");
2456
- }
2457
- const { RepositoryCollection: RepositoryCollection2 } = await Promise.resolve().then(() => Repositories);
2458
- const collection = await RepositoryCollection2.create(this.options);
2459
- const repo = await collection.get({ id: this.repositoryId });
2460
- if (!repo) {
2461
- throw new Error(`Repository ${this.repositoryId} not found`);
2462
- }
2463
- const client = await repo.getClient();
2464
- await client.updateLabel(this.name, {
2465
- name: this.name,
2466
- color: this.color,
2467
- description: this.description
2468
- });
2469
- await this.save();
2470
- }
1379
+ var Label = class extends SmrtObject {
1380
+ repositoryId;
1381
+ /**
1382
+ * Label name
1383
+ */
1384
+ name = "";
1385
+ /**
1386
+ * Label color (hex without #)
1387
+ */
1388
+ color = "";
1389
+ /**
1390
+ * Label description
1391
+ */
1392
+ description = "";
1393
+ constructor(options = {}) {
1394
+ super(options);
1395
+ if (options.repositoryId !== void 0) this.repositoryId = options.repositoryId;
1396
+ if (options.name !== void 0) this.name = options.name;
1397
+ if (options.color !== void 0) this.color = options.color;
1398
+ if (options.description !== void 0) this.description = options.description;
1399
+ }
1400
+ /**
1401
+ * Check if this is a type label (bug, feature, etc.)
1402
+ */
1403
+ isTypeLabel() {
1404
+ return [
1405
+ /^type:/i,
1406
+ /^kind:/i,
1407
+ /^bug$/i,
1408
+ /^feature$/i,
1409
+ /^enhancement$/i,
1410
+ /^docs$/i,
1411
+ /^chore$/i
1412
+ ].some((p) => p.test(this.name));
1413
+ }
1414
+ /**
1415
+ * Check if this is a priority label
1416
+ */
1417
+ isPriorityLabel() {
1418
+ return [
1419
+ /^p[0-4]$/i,
1420
+ /^priority:/i,
1421
+ /^critical$/i,
1422
+ /^high$/i,
1423
+ /^medium$/i,
1424
+ /^low$/i
1425
+ ].some((p) => p.test(this.name));
1426
+ }
1427
+ /**
1428
+ * Check if this is a status label
1429
+ */
1430
+ isStatusLabel() {
1431
+ return [
1432
+ /^status:/i,
1433
+ /^wip$/i,
1434
+ /^in.?progress$/i,
1435
+ /^blocked$/i,
1436
+ /^needs.?review$/i,
1437
+ /^ready$/i
1438
+ ].some((p) => p.test(this.name));
1439
+ }
1440
+ /**
1441
+ * Get the label category
1442
+ *
1443
+ * @returns Category name
1444
+ */
1445
+ getCategory() {
1446
+ if (this.isTypeLabel()) return "type";
1447
+ if (this.isPriorityLabel()) return "priority";
1448
+ if (this.isStatusLabel()) return "status";
1449
+ if (this.name.includes(":")) return "area";
1450
+ return "other";
1451
+ }
1452
+ /**
1453
+ * Parse priority level from label name
1454
+ *
1455
+ * @returns Priority level 0-4 or null
1456
+ */
1457
+ getPriorityLevel() {
1458
+ const match = this.name.match(/p([0-4])/i);
1459
+ if (match) return parseInt(match[1], 10);
1460
+ if (/critical/i.test(this.name)) return 0;
1461
+ if (/high/i.test(this.name)) return 1;
1462
+ if (/medium/i.test(this.name)) return 2;
1463
+ if (/low/i.test(this.name)) return 3;
1464
+ return null;
1465
+ }
1466
+ /**
1467
+ * Get label with # prefix for hex color
1468
+ */
1469
+ getHexColor() {
1470
+ return this.color.startsWith("#") ? this.color : `#${this.color}`;
1471
+ }
1472
+ /**
1473
+ * Create a label in the repository
1474
+ */
1475
+ async createInRepository() {
1476
+ if (!this.repositoryId) throw new Error("Label must have a repositoryId");
1477
+ const { RepositoryCollection } = await Promise.resolve().then(() => Repositories_exports);
1478
+ const repo = await (await RepositoryCollection.create(this.options)).get({ id: this.repositoryId });
1479
+ if (!repo) throw new Error(`Repository ${this.repositoryId} not found`);
1480
+ await (await repo.getClient()).createLabel({
1481
+ name: this.name,
1482
+ color: this.color,
1483
+ description: this.description
1484
+ });
1485
+ await this.save();
1486
+ }
1487
+ /**
1488
+ * Update this label in the repository
1489
+ */
1490
+ async updateInRepository() {
1491
+ if (!this.repositoryId) throw new Error("Label must have a repositoryId");
1492
+ const { RepositoryCollection } = await Promise.resolve().then(() => Repositories_exports);
1493
+ const repo = await (await RepositoryCollection.create(this.options)).get({ id: this.repositoryId });
1494
+ if (!repo) throw new Error(`Repository ${this.repositoryId} not found`);
1495
+ await (await repo.getClient()).updateLabel(this.name, {
1496
+ name: this.name,
1497
+ color: this.color,
1498
+ description: this.description
1499
+ });
1500
+ await this.save();
1501
+ }
2471
1502
  };
2472
- __decorateClass([
2473
- foreignKey("Repository")
2474
- ], Label.prototype, "repositoryId", 2);
2475
- Label = __decorateClass([
2476
- smrt({
2477
- api: { include: ["list", "get", "create", "update", "delete"] },
2478
- mcp: { include: ["list", "get", "create"] },
2479
- cli: { include: ["list", "get", "create", "update", "delete"] }
2480
- })
2481
- ], Label);
2482
- export {
2483
- Comment,
2484
- Issue,
2485
- IssueCollection,
2486
- Label,
2487
- PROJECTS_MODULE_META,
2488
- PROJECTS_UI_SLOTS,
2489
- Project,
2490
- ProjectCollection,
2491
- PullRequest,
2492
- PullRequestCollection,
2493
- Repository,
2494
- RepositoryCollection,
2495
- issueIncorporateFeedbackPrompt
2496
- };
2497
- //# sourceMappingURL=index.js.map
1503
+ __decorateClass([foreignKey("Repository")], Label.prototype, "repositoryId", 2);
1504
+ Label = __decorateClass([smrt({
1505
+ api: { include: [
1506
+ "list",
1507
+ "get",
1508
+ "create",
1509
+ "update",
1510
+ "delete"
1511
+ ] },
1512
+ mcp: { include: [
1513
+ "list",
1514
+ "get",
1515
+ "create"
1516
+ ] },
1517
+ cli: { include: [
1518
+ "list",
1519
+ "get",
1520
+ "create",
1521
+ "update",
1522
+ "delete"
1523
+ ] }
1524
+ })], Label);
1525
+ //#endregion
1526
+ export { Comment, Issue, IssueCollection, Label, PROJECTS_MODULE_META, PROJECTS_UI_SLOTS, Project, ProjectCollection, PullRequest, PullRequestCollection, Repository, RepositoryCollection, issueIncorporateFeedbackPrompt, Repositories_exports as n, Issues_exports as r, Comment_exports as t };
1527
+
1528
+ //# sourceMappingURL=index.js.map