@happyvertical/smrt-projects 0.30.0

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