kachow 0.1.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 (48) hide show
  1. package/README.md +77 -0
  2. package/_server/dist/app.js +130 -0
  3. package/_server/dist/db/index.js +50 -0
  4. package/_server/dist/db/schema.js +247 -0
  5. package/_server/dist/queues/ingestQueue.js +49 -0
  6. package/_server/dist/queues/redis.js +58 -0
  7. package/_server/dist/routes/agents.js +162 -0
  8. package/_server/dist/routes/architecture.js +88 -0
  9. package/_server/dist/routes/config.js +24 -0
  10. package/_server/dist/routes/github.js +158 -0
  11. package/_server/dist/routes/graph.js +112 -0
  12. package/_server/dist/routes/healing.js +137 -0
  13. package/_server/dist/routes/impact.js +100 -0
  14. package/_server/dist/routes/ingest.js +182 -0
  15. package/_server/dist/routes/manager.js +179 -0
  16. package/_server/dist/routes/notifications.js +85 -0
  17. package/_server/dist/routes/qa.js +68 -0
  18. package/_server/dist/routes/scanner.js +221 -0
  19. package/_server/dist/routes/stream.js +179 -0
  20. package/_server/dist/routes/webhooks.js +168 -0
  21. package/_server/dist/server.js +46 -0
  22. package/_server/dist/services/agentService.js +715 -0
  23. package/_server/dist/services/architectureService.js +172 -0
  24. package/_server/dist/services/demoSeed.js +181 -0
  25. package/_server/dist/services/graphLayout.js +102 -0
  26. package/_server/dist/services/graphService.js +532 -0
  27. package/_server/dist/services/healingService.js +253 -0
  28. package/_server/dist/services/impactService.js +304 -0
  29. package/_server/dist/services/ingestService.js +129 -0
  30. package/_server/dist/services/managerService.js +260 -0
  31. package/_server/dist/services/notificationService.js +283 -0
  32. package/_server/dist/services/qaService.js +413 -0
  33. package/_server/dist/services/scannerService.js +748 -0
  34. package/_server/dist/services/seedService.js +215 -0
  35. package/_server/dist/sse/sseManager.js +101 -0
  36. package/_server/dist/types/index.js +38 -0
  37. package/_server/dist/workers/ingestWorker.js +274 -0
  38. package/_server/public/assets/index-BTkbB_YF.js +4546 -0
  39. package/_server/public/assets/index-Bmh3jWBm.css +1 -0
  40. package/_server/public/favicon.ico +0 -0
  41. package/_server/public/images/glass-waves-bg.png +0 -0
  42. package/_server/public/index.html +29 -0
  43. package/_server/public/placeholder.svg +1 -0
  44. package/_server/public/robots.txt +14 -0
  45. package/dist/config.js +133 -0
  46. package/dist/index.js +510 -0
  47. package/dist/setup.js +223 -0
  48. package/package.json +62 -0
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ /**
3
+ * Manager intelligence service — Phase 8.
4
+ * Provides VP-level health summaries via OpenAI, team breakdowns,
5
+ * ADR decision history, and role-based onboarding paths.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.getManagerPulse = getManagerPulse;
12
+ exports.getManagerTeams = getManagerTeams;
13
+ exports.getManagerDecisions = getManagerDecisions;
14
+ exports.getOnboardingPath = getOnboardingPath;
15
+ exports.getTeams = getTeams;
16
+ exports.createTeam = createTeam;
17
+ exports.deleteTeam = deleteTeam;
18
+ exports.regenTeamCode = regenTeamCode;
19
+ exports.joinTeamByCode = joinTeamByCode;
20
+ exports.addTeamMember = addTeamMember;
21
+ const openai_1 = __importDefault(require("openai"));
22
+ const crypto_1 = require("crypto");
23
+ const index_js_1 = require("../db/index.js");
24
+ const graphService_js_1 = require("./graphService.js");
25
+ /**
26
+ * Returns a VP-level pulse summary using OpenAI for the plain-English description.
27
+ */
28
+ async function getManagerPulse() {
29
+ const health = (0, graphService_js_1.getSystemHealth)();
30
+ const nodes = (0, graphService_js_1.getNodes)();
31
+ const db = (0, index_js_1.getDb)();
32
+ const criticalNames = nodes.filter(n => n.healthTier === 'critical').map(n => n.name);
33
+ const warningNames = nodes.filter(n => n.healthTier === 'warning').map(n => n.name);
34
+ let plainEnglishSummary = `System health is at ${health.overallScore}/100 with ${health.critical} critical and ${health.warning} warning services.`;
35
+ const apiKey = process.env.OPENAI_API_KEY;
36
+ if (apiKey) {
37
+ try {
38
+ const openai = new openai_1.default({ apiKey });
39
+ const completion = await openai.chat.completions.create({
40
+ model: 'gpt-4o',
41
+ messages: [{
42
+ role: 'user',
43
+ content: `You are briefing a VP of Engineering. In exactly 2-3 sentences, summarize: Overall score ${health.overallScore}/100. Services: ${health.totalServices} total, ${health.critical} critical (${criticalNames.join(', ') || 'none'}), ${health.warning} warning (${warningNames.join(', ') || 'none'}), ${health.healthy} healthy. Be direct and action-oriented.`,
44
+ }],
45
+ temperature: 0.2,
46
+ max_tokens: 150,
47
+ });
48
+ plainEnglishSummary = completion.choices[0]?.message?.content?.trim() ?? plainEnglishSummary;
49
+ }
50
+ catch {
51
+ // Fallback to static summary
52
+ }
53
+ }
54
+ const teams = db.prepare(`
55
+ SELECT team,
56
+ COUNT(*) AS svc_count,
57
+ ROUND(AVG(health_score), 0) AS avg_health,
58
+ SUM(CASE WHEN health_tier = 'critical' THEN 1 ELSE 0 END) AS critical_count
59
+ FROM services WHERE is_external = 0
60
+ GROUP BY team
61
+ ORDER BY avg_health ASC
62
+ `).all();
63
+ const topIssues = nodes
64
+ .filter(n => n.healthTier !== 'healthy')
65
+ .sort((a, b) => a.healthScore - b.healthScore)
66
+ .slice(0, 5)
67
+ .map(n => ({
68
+ serviceId: n.id,
69
+ serviceName: n.name,
70
+ healthScore: n.healthScore,
71
+ healthTier: n.healthTier,
72
+ }));
73
+ return {
74
+ overallScore: health.overallScore,
75
+ plainEnglishSummary,
76
+ topIssues,
77
+ teamBreakdown: teams.map(t => ({
78
+ team: t.team ?? 'unassigned',
79
+ serviceCount: t.svc_count,
80
+ avgHealth: t.avg_health,
81
+ criticalCount: t.critical_count,
82
+ })),
83
+ };
84
+ }
85
+ // ── Teams ─────────────────────────────────────────────────────────────────────
86
+ function getManagerTeams() {
87
+ const db = (0, index_js_1.getDb)();
88
+ return db.prepare(`
89
+ SELECT team,
90
+ COUNT(*) AS svc_count,
91
+ ROUND(AVG(health_score), 0) AS avg_health,
92
+ SUM(CASE WHEN health_tier = 'critical' THEN 1 ELSE 0 END) AS critical_count
93
+ FROM services WHERE is_external = 0
94
+ GROUP BY team ORDER BY avg_health ASC
95
+ `).all()
96
+ .map(t => ({
97
+ team: t.team ?? 'unassigned',
98
+ serviceCount: t.svc_count,
99
+ avgHealth: t.avg_health,
100
+ criticalCount: t.critical_count,
101
+ }));
102
+ }
103
+ // ── Decisions (ADRs) ──────────────────────────────────────────────────────────
104
+ function getManagerDecisions() {
105
+ const db = (0, index_js_1.getDb)();
106
+ return db.prepare(`
107
+ SELECT a.id, a.title, a.status, a.created_at,
108
+ ia.trigger_ref, ia.risk_score, ia.trigger_type
109
+ FROM adrs a
110
+ JOIN impact_analyses ia ON ia.id = a.analysis_id
111
+ ORDER BY a.created_at DESC LIMIT 20
112
+ `).all().map(a => ({
113
+ id: a.id,
114
+ title: a.title,
115
+ status: a.status,
116
+ riskScore: a.risk_score,
117
+ triggerRef: a.trigger_ref,
118
+ triggerType: a.trigger_type,
119
+ createdAt: a.created_at,
120
+ }));
121
+ }
122
+ // ── Onboarding path ───────────────────────────────────────────────────────────
123
+ /**
124
+ * Generates a role-specific onboarding learning path.
125
+ * Orders services based on the engineer's role focus.
126
+ */
127
+ function getOnboardingPath(role) {
128
+ const db = (0, index_js_1.getDb)();
129
+ const nodes = (0, graphService_js_1.getNodes)();
130
+ const edges = (0, graphService_js_1.getEdges)();
131
+ const outDegree = {};
132
+ for (const e of edges) {
133
+ outDegree[e.sourceId] = (outDegree[e.sourceId] ?? 0) + 1;
134
+ }
135
+ let ordered = [...nodes];
136
+ if (role === 'backend-engineer') {
137
+ // High out-degree first (most connected / most impactful)
138
+ ordered = ordered.sort((a, b) => (outDegree[b.id] ?? 0) - (outDegree[a.id] ?? 0));
139
+ }
140
+ else if (role === 'sre') {
141
+ // Worst health first — focus on fires
142
+ ordered = ordered.sort((a, b) => a.healthScore - b.healthScore);
143
+ }
144
+ else if (role === 'engineering-manager') {
145
+ // By team, then by health
146
+ ordered = ordered.sort((a, b) => {
147
+ const teamCmp = (a.team ?? 'zzz').localeCompare(b.team ?? 'zzz');
148
+ return teamCmp !== 0 ? teamCmp : a.healthScore - b.healthScore;
149
+ });
150
+ }
151
+ return {
152
+ role,
153
+ steps: ordered.slice(0, 8).map((n, i) => {
154
+ const svcRow = db.prepare('SELECT repo_url FROM services WHERE id = ?').get(n.id);
155
+ const endpoints = db.prepare('SELECT method, path FROM endpoints WHERE service_id = ? LIMIT 5').all(n.id);
156
+ const deps = edges.filter(e => e.sourceId === n.id).length;
157
+ return {
158
+ step: i + 1,
159
+ serviceId: n.id,
160
+ title: `Understand ${n.name}`,
161
+ description: `${n.name} is a ${n.language ?? 'service'}${n.team ? ` owned by the ${n.team} team` : ''}. Health: ${n.healthScore}/100 (${n.healthTier}). Makes ${deps} downstream call(s).`,
162
+ keyFiles: [
163
+ `${n.name}/src/index.${n.language === 'python' ? 'py' : 'ts'}`,
164
+ `${n.name}/src/routes.${n.language === 'python' ? 'py' : 'ts'}`,
165
+ `${n.name}/README.md`,
166
+ ],
167
+ starterTasks: [
168
+ `Read the ${n.name} README`,
169
+ ...(endpoints.length > 0 ? [`Trace a request to ${endpoints[0].method} ${endpoints[0].path}`] : []),
170
+ `Run the ${n.name} test suite`,
171
+ ...(svcRow?.repo_url ? [`Clone and build ${n.name} locally`] : []),
172
+ ].slice(0, 3),
173
+ repoUrl: svcRow?.repo_url ?? null,
174
+ estimatedMinutes: 30 + deps * 5,
175
+ };
176
+ }),
177
+ };
178
+ }
179
+ function generateJoinCode() {
180
+ return String(Math.floor(100000 + Math.random() * 900000));
181
+ }
182
+ function rowToTeam(row, members) {
183
+ return {
184
+ id: row.id,
185
+ name: row.name,
186
+ repo: row.repo,
187
+ joinCode: row.join_code,
188
+ description: row.description,
189
+ color: row.color,
190
+ createdAt: row.created_at,
191
+ members,
192
+ };
193
+ }
194
+ function membersForTeam(teamId) {
195
+ const db = (0, index_js_1.getDb)();
196
+ return db.prepare(`SELECT * FROM team_members WHERE team_id = ? ORDER BY joined_at ASC`).all(teamId)
197
+ .map(m => ({
198
+ id: m.id,
199
+ name: m.name,
200
+ role: m.role,
201
+ online: m.online === 1,
202
+ progress: m.progress,
203
+ joinedAt: m.joined_at,
204
+ }));
205
+ }
206
+ /** Return all teams with their members. */
207
+ function getTeams() {
208
+ const db = (0, index_js_1.getDb)();
209
+ const rows = db.prepare(`SELECT * FROM teams ORDER BY created_at ASC`).all();
210
+ return rows.map(r => rowToTeam(r, membersForTeam(r.id)));
211
+ }
212
+ /** Create a new team and return it. */
213
+ function createTeam(input) {
214
+ const db = (0, index_js_1.getDb)();
215
+ // Ensure unique join code
216
+ let joinCode = generateJoinCode();
217
+ while (db.prepare(`SELECT 1 FROM teams WHERE join_code = ?`).get(joinCode)) {
218
+ joinCode = generateJoinCode();
219
+ }
220
+ const COLORS = ['#6366F1', '#16A34A', '#D97706', '#0052CC', '#DC2626', '#7C3AED', '#0891B2'];
221
+ const count = db.prepare(`SELECT COUNT(*) as c FROM teams`).get().c;
222
+ const color = input.color ?? COLORS[count % COLORS.length];
223
+ const id = (0, crypto_1.randomUUID)();
224
+ db.prepare(`
225
+ INSERT INTO teams (id, name, repo, join_code, description, color)
226
+ VALUES (?, ?, ?, ?, ?, ?)
227
+ `).run(id, input.name, input.repo ?? '—', joinCode, input.description ?? '', color);
228
+ return rowToTeam(db.prepare(`SELECT * FROM teams WHERE id = ?`).get(id), []);
229
+ }
230
+ /** Delete a team (cascades to members). */
231
+ function deleteTeam(teamId) {
232
+ const db = (0, index_js_1.getDb)();
233
+ db.prepare(`DELETE FROM teams WHERE id = ?`).run(teamId);
234
+ }
235
+ /** Regenerate a team's join code and return the new code. */
236
+ function regenTeamCode(teamId) {
237
+ const db = (0, index_js_1.getDb)();
238
+ let joinCode = generateJoinCode();
239
+ while (db.prepare(`SELECT 1 FROM teams WHERE join_code = ? AND id != ?`).get(joinCode, teamId)) {
240
+ joinCode = generateJoinCode();
241
+ }
242
+ db.prepare(`UPDATE teams SET join_code = ?, updated_at = datetime('now') WHERE id = ?`).run(joinCode, teamId);
243
+ return joinCode;
244
+ }
245
+ /** Validate a 6-digit code and return the matching team (for engineers). */
246
+ function joinTeamByCode(joinCode) {
247
+ const db = (0, index_js_1.getDb)();
248
+ const row = db.prepare(`SELECT * FROM teams WHERE join_code = ?`).get(joinCode);
249
+ if (!row)
250
+ return null;
251
+ return rowToTeam(row, membersForTeam(row.id));
252
+ }
253
+ /** Add a member to a team (called after an engineer enters a valid code). */
254
+ function addTeamMember(teamId, input) {
255
+ const db = (0, index_js_1.getDb)();
256
+ const id = (0, crypto_1.randomUUID)();
257
+ db.prepare(`INSERT INTO team_members (id, team_id, name, role) VALUES (?, ?, ?, ?)`).run(id, teamId, input.name, input.role ?? 'Engineer');
258
+ return db.prepare(`SELECT * FROM team_members WHERE id = ?`).get(id);
259
+ }
260
+ //# sourceMappingURL=managerService.js.map
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ /**
3
+ * Notification service — Slack & Jira integrations.
4
+ *
5
+ * Sends notifications for critical issues:
6
+ * - Slack: Bot Token + chat.postMessage Web API
7
+ * - Jira: REST API v3 → add comment or create issue
8
+ *
9
+ * Env vars:
10
+ * SLACK_BOT_TOKEN — Slack Bot User OAuth Token (xoxb-…)
11
+ * JIRA_BASE_URL — e.g. https://your-org.atlassian.net
12
+ * JIRA_EMAIL — Atlassian account email
13
+ * JIRA_API_TOKEN — Atlassian API token
14
+ * JIRA_PROJECT_KEY — e.g. "KACHOW" (defaults to KACHOW)
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.sendSlackNotification = sendSlackNotification;
18
+ exports.commentOrCreateJira = commentOrCreateJira;
19
+ const index_js_1 = require("../db/index.js");
20
+ const crypto_1 = require("crypto");
21
+ function resolveIssueContext(issueId) {
22
+ const db = (0, index_js_1.getDb)();
23
+ // Agent-sourced issues have IDs like "agent-hi-...", "agent-sh-..."
24
+ // Healing issues have IDs like "heal-<uuid>"
25
+ // Incident issues have IDs like "inc-<uuid>"
26
+ if (issueId.startsWith('heal-')) {
27
+ const hid = issueId.replace('heal-', '');
28
+ const row = db.prepare(`
29
+ SELECT hp.service_id, hp.issue_type, hp.explanation, hp.status,
30
+ s.name AS service_name
31
+ FROM healing_prs hp JOIN services s ON s.id = hp.service_id
32
+ WHERE hp.id = ?
33
+ `).get(hid);
34
+ if (!row)
35
+ return null;
36
+ return {
37
+ serviceId: row.service_id,
38
+ serviceName: row.service_name,
39
+ severity: 'warning',
40
+ title: row.issue_type,
41
+ description: row.explanation,
42
+ category: 'quality',
43
+ file: null, line: null,
44
+ suggestedFix: null,
45
+ source: 'healing',
46
+ };
47
+ }
48
+ if (issueId.startsWith('inc-')) {
49
+ const iid = issueId.replace('inc-', '');
50
+ const row = db.prepare(`
51
+ SELECT i.service_id, i.severity, i.title, i.status, i.occurred_at,
52
+ s.name AS service_name
53
+ FROM incidents i JOIN services s ON s.id = i.service_id
54
+ WHERE i.id = ?
55
+ `).get(iid);
56
+ if (!row)
57
+ return null;
58
+ return {
59
+ serviceId: row.service_id,
60
+ serviceName: row.service_name,
61
+ severity: row.severity,
62
+ title: row.title,
63
+ description: `${row.title} — ${row.status} since ${row.occurred_at}`,
64
+ category: 'reliability',
65
+ file: null, line: null,
66
+ suggestedFix: null,
67
+ source: 'incident',
68
+ };
69
+ }
70
+ // For agent-sourced issues, we need to look them up from the KG
71
+ // These are ephemeral IDs generated by getCriticalIssues(), so we
72
+ // accept the full issue payload from the frontend instead.
73
+ return null;
74
+ }
75
+ // ── Slack ─────────────────────────────────────────────────────────────────────
76
+ async function sendSlackNotification(payload, issueOverride) {
77
+ const botToken = process.env.SLACK_BOT_TOKEN;
78
+ const issue = issueOverride ?? resolveIssueContext(payload.issueId);
79
+ const severityEmoji = {
80
+ critical: '🔴', warning: '🟡', info: '🔵',
81
+ };
82
+ const emoji = issue ? severityEmoji[issue.severity] ?? '⚪' : '⚪';
83
+ const blocks = [
84
+ {
85
+ type: 'header',
86
+ text: { type: 'plain_text', text: `${emoji} KA-CHOW Critical Issue Detected`, emoji: true },
87
+ },
88
+ {
89
+ type: 'section',
90
+ fields: [
91
+ { type: 'mrkdwn', text: `*Service:*\n${issue?.serviceName ?? 'Unknown'}` },
92
+ { type: 'mrkdwn', text: `*Severity:*\n${issue?.severity?.toUpperCase() ?? 'UNKNOWN'}` },
93
+ { type: 'mrkdwn', text: `*Category:*\n${issue?.category ?? 'N/A'}` },
94
+ { type: 'mrkdwn', text: `*Source:*\n${issue?.source ?? 'N/A'}` },
95
+ ],
96
+ },
97
+ {
98
+ type: 'section',
99
+ text: { type: 'mrkdwn', text: `*Issue:*\n${issue?.description ?? payload.issueId}` },
100
+ },
101
+ ];
102
+ if (issue?.file) {
103
+ blocks.push({
104
+ type: 'section',
105
+ text: { type: 'mrkdwn', text: `*File:* \`${issue.file}${issue.line ? `:${issue.line}` : ''}\`` },
106
+ fields: undefined,
107
+ });
108
+ }
109
+ if (issue?.suggestedFix) {
110
+ blocks.push({
111
+ type: 'section',
112
+ text: { type: 'mrkdwn', text: `*🔧 Suggested Fix:*\n${issue.suggestedFix}` },
113
+ fields: undefined,
114
+ });
115
+ }
116
+ if (payload.customMessage) {
117
+ blocks.push({
118
+ type: 'section',
119
+ text: { type: 'mrkdwn', text: payload.customMessage },
120
+ fields: undefined,
121
+ });
122
+ }
123
+ const slackBody = {
124
+ channel: payload.channel,
125
+ text: `${emoji} [KA-CHOW] ${issue?.severity?.toUpperCase()}: ${issue?.title ?? payload.issueId} in ${issue?.serviceName ?? 'unknown service'}`,
126
+ blocks,
127
+ };
128
+ // Log to activity feed regardless of webhook config
129
+ const db = (0, index_js_1.getDb)();
130
+ db.prepare(`
131
+ INSERT INTO activity_feed (id, type, title, detail, severity, service_id)
132
+ VALUES (?, 'SLACK_SENT', ?, ?, ?, ?)
133
+ `).run((0, crypto_1.randomUUID)(), `Slack notification: ${issue?.title ?? payload.issueId}`, `Sent to ${payload.channel ?? 'default channel'}`, issue?.severity ?? 'info', issue?.serviceId ?? null);
134
+ if (!botToken) {
135
+ // No bot token configured — return mock success so the frontend can still work
136
+ return {
137
+ sent: false,
138
+ channel: payload.channel ?? '#general',
139
+ message: slackBody.text,
140
+ webhookConfigured: false,
141
+ };
142
+ }
143
+ try {
144
+ const res = await fetch('https://slack.com/api/chat.postMessage', {
145
+ method: 'POST',
146
+ headers: {
147
+ 'Authorization': `Bearer ${botToken}`,
148
+ 'Content-Type': 'application/json; charset=utf-8',
149
+ },
150
+ body: JSON.stringify({
151
+ channel: payload.channel ?? '#general',
152
+ text: slackBody.text,
153
+ blocks: slackBody.blocks,
154
+ }),
155
+ });
156
+ const data = await res.json();
157
+ if (!data.ok) {
158
+ console.error('[Slack] API error:', data.error);
159
+ }
160
+ return {
161
+ sent: !!data.ok,
162
+ channel: payload.channel ?? '#general',
163
+ message: slackBody.text,
164
+ webhookConfigured: true,
165
+ };
166
+ }
167
+ catch (e) {
168
+ console.error('[Slack] Send failed:', e);
169
+ return {
170
+ sent: false,
171
+ channel: payload.channel ?? '#general',
172
+ message: slackBody.text,
173
+ webhookConfigured: true,
174
+ };
175
+ }
176
+ }
177
+ // ── Jira ──────────────────────────────────────────────────────────────────────
178
+ async function commentOrCreateJira(payload, issueOverride) {
179
+ const baseUrl = process.env.JIRA_BASE_URL;
180
+ const email = process.env.JIRA_EMAIL;
181
+ const token = process.env.JIRA_API_TOKEN;
182
+ const projectKey = process.env.JIRA_PROJECT_KEY ?? 'SCRUM';
183
+ const issue = issueOverride ?? resolveIssueContext(payload.issueId);
184
+ const db = (0, index_js_1.getDb)();
185
+ const title = issue?.title ?? payload.issueId;
186
+ const description = [
187
+ `*Service:* ${issue?.serviceName ?? 'Unknown'}`,
188
+ `*Severity:* ${issue?.severity?.toUpperCase() ?? 'UNKNOWN'}`,
189
+ `*Category:* ${issue?.category ?? 'N/A'}`,
190
+ `*Source:* ${issue?.source ?? 'N/A'}`,
191
+ '',
192
+ `*Description:*`,
193
+ issue?.description ?? 'No description available',
194
+ '',
195
+ issue?.file ? `*File:* ${issue.file}${issue.line ? `:${issue.line}` : ''}` : '',
196
+ issue?.suggestedFix ? `*Suggested Fix:* ${issue.suggestedFix}` : '',
197
+ '',
198
+ '_Auto-generated by KA-CHOW_',
199
+ ].filter(Boolean).join('\n');
200
+ // Log to activity feed regardless of Jira config
201
+ db.prepare(`
202
+ INSERT INTO activity_feed (id, type, title, detail, severity, service_id)
203
+ VALUES (?, 'JIRA_COMMENT', ?, ?, ?, ?)
204
+ `).run((0, crypto_1.randomUUID)(), `Jira ${payload.createNewTicket ? 'ticket' : 'comment'}: ${title}`, payload.jiraIssueKey ?? 'new ticket', issue?.severity ?? 'info', issue?.serviceId ?? null);
205
+ if (!baseUrl || !email || !token) {
206
+ // No Jira configured — return mock
207
+ const mockKey = payload.jiraIssueKey ?? `${projectKey}-${Math.floor(Math.random() * 999)}`;
208
+ return {
209
+ success: false,
210
+ jiraIssueKey: mockKey,
211
+ action: payload.createNewTicket ? 'created' : 'commented',
212
+ summary: `[KA-CHOW] ${issue?.severity?.toUpperCase()}: ${title}`,
213
+ url: null,
214
+ };
215
+ }
216
+ const headers = {
217
+ 'Authorization': `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`,
218
+ 'Content-Type': 'application/json',
219
+ 'Accept': 'application/json',
220
+ };
221
+ try {
222
+ if (payload.createNewTicket || !payload.jiraIssueKey) {
223
+ // Create new issue
224
+ // Team-managed (next-gen) projects use Task; classic projects may have Bug.
225
+ // We try Task as the safe default since it's available in both project types.
226
+ const body = {
227
+ fields: {
228
+ project: { key: projectKey },
229
+ summary: `[KA-CHOW] ${issue?.severity?.toUpperCase()}: ${title} in ${issue?.serviceName ?? 'unknown'}`,
230
+ description: {
231
+ type: 'doc', version: 1,
232
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: description }] }],
233
+ },
234
+ issuetype: { name: 'Task' },
235
+ },
236
+ };
237
+ const res = await fetch(`${baseUrl}/rest/api/3/issue`, {
238
+ method: 'POST', headers, body: JSON.stringify(body),
239
+ });
240
+ const data = await res.json();
241
+ if (!res.ok) {
242
+ console.error('[Jira] Create issue failed:', res.status, JSON.stringify(data));
243
+ }
244
+ return {
245
+ success: res.ok,
246
+ jiraIssueKey: data.key ?? `${projectKey}-?`,
247
+ action: 'created',
248
+ summary: body.fields.summary,
249
+ url: data.key ? `${baseUrl}/browse/${data.key}` : null,
250
+ };
251
+ }
252
+ else {
253
+ // Comment on existing issue
254
+ const commentBody = {
255
+ body: {
256
+ type: 'doc', version: 1,
257
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: description }] }],
258
+ },
259
+ };
260
+ const res = await fetch(`${baseUrl}/rest/api/3/issue/${payload.jiraIssueKey}/comment`, {
261
+ method: 'POST', headers, body: JSON.stringify(commentBody),
262
+ });
263
+ return {
264
+ success: res.ok,
265
+ jiraIssueKey: payload.jiraIssueKey,
266
+ action: 'commented',
267
+ summary: `Comment added to ${payload.jiraIssueKey}`,
268
+ url: `${baseUrl}/browse/${payload.jiraIssueKey}`,
269
+ };
270
+ }
271
+ }
272
+ catch (e) {
273
+ console.error('[Jira] API call failed:', e);
274
+ return {
275
+ success: false,
276
+ jiraIssueKey: payload.jiraIssueKey ?? `${projectKey}-?`,
277
+ action: payload.createNewTicket ? 'created' : 'commented',
278
+ summary: `Failed: ${e.message}`,
279
+ url: null,
280
+ };
281
+ }
282
+ }
283
+ //# sourceMappingURL=notificationService.js.map