@sweny-ai/core 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.
- package/dist/__tests__/claude.test.d.ts +1 -0
- package/dist/__tests__/claude.test.js +328 -0
- package/dist/__tests__/executor.test.d.ts +1 -0
- package/dist/__tests__/executor.test.js +296 -0
- package/dist/__tests__/integration/datadog.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/datadog.integration.test.js +23 -0
- package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/e2e-workflow.integration.test.js +75 -0
- package/dist/__tests__/integration/github.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/github.integration.test.js +37 -0
- package/dist/__tests__/integration/harness.d.ts +24 -0
- package/dist/__tests__/integration/harness.js +34 -0
- package/dist/__tests__/integration/linear.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/linear.integration.test.js +15 -0
- package/dist/__tests__/integration/sentry.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/sentry.integration.test.js +20 -0
- package/dist/__tests__/integration/slack.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/slack.integration.test.js +22 -0
- package/dist/__tests__/schema.test.d.ts +1 -0
- package/dist/__tests__/schema.test.js +239 -0
- package/dist/__tests__/skills-index.test.d.ts +1 -0
- package/dist/__tests__/skills-index.test.js +122 -0
- package/dist/__tests__/skills.test.d.ts +1 -0
- package/dist/__tests__/skills.test.js +296 -0
- package/dist/__tests__/studio.test.d.ts +1 -0
- package/dist/__tests__/studio.test.js +172 -0
- package/dist/__tests__/testing.test.d.ts +1 -0
- package/dist/__tests__/testing.test.js +224 -0
- package/dist/browser.d.ts +17 -0
- package/dist/browser.js +22 -0
- package/dist/claude.d.ts +48 -0
- package/dist/claude.js +293 -0
- package/dist/cli/check.d.ts +11 -0
- package/dist/cli/check.js +237 -0
- package/dist/cli/config-file.d.ts +12 -0
- package/dist/cli/config-file.js +208 -0
- package/dist/cli/config.d.ts +77 -0
- package/dist/cli/config.js +565 -0
- package/dist/cli/main.d.ts +10 -0
- package/dist/cli/main.js +744 -0
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.js +357 -0
- package/dist/cli/renderer.d.ts +33 -0
- package/dist/cli/renderer.js +423 -0
- package/dist/cli/renderer.test.d.ts +1 -0
- package/dist/cli/renderer.test.js +302 -0
- package/dist/cli/setup.d.ts +11 -0
- package/dist/cli/setup.js +310 -0
- package/dist/executor.d.ts +29 -0
- package/dist/executor.js +173 -0
- package/dist/executor.test.d.ts +1 -0
- package/dist/executor.test.js +314 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +36 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +183 -0
- package/dist/mcp.test.d.ts +1 -0
- package/dist/mcp.test.js +334 -0
- package/dist/schema.d.ts +318 -0
- package/dist/schema.js +207 -0
- package/dist/skills/betterstack.d.ts +7 -0
- package/dist/skills/betterstack.js +114 -0
- package/dist/skills/datadog.d.ts +7 -0
- package/dist/skills/datadog.js +107 -0
- package/dist/skills/github.d.ts +8 -0
- package/dist/skills/github.js +155 -0
- package/dist/skills/index.d.ts +68 -0
- package/dist/skills/index.js +134 -0
- package/dist/skills/linear.d.ts +7 -0
- package/dist/skills/linear.js +89 -0
- package/dist/skills/notification.d.ts +11 -0
- package/dist/skills/notification.js +142 -0
- package/dist/skills/sentry.d.ts +7 -0
- package/dist/skills/sentry.js +105 -0
- package/dist/skills/slack.d.ts +8 -0
- package/dist/skills/slack.js +115 -0
- package/dist/studio.d.ts +124 -0
- package/dist/studio.js +174 -0
- package/dist/testing.d.ts +88 -0
- package/dist/testing.js +253 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +11 -0
- package/dist/workflow-builder.d.ts +45 -0
- package/dist/workflow-builder.js +120 -0
- package/dist/workflow-builder.test.d.ts +1 -0
- package/dist/workflow-builder.test.js +117 -0
- package/dist/workflows/implement.d.ts +11 -0
- package/dist/workflows/implement.js +83 -0
- package/dist/workflows/index.d.ts +2 -0
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/triage.d.ts +18 -0
- package/dist/workflows/triage.js +108 -0
- package/package.json +83 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
export function registerTriageCommand(program) {
|
|
4
|
+
// NOTE: File-eligible options omit Commander defaults so parseCliInputs() can
|
|
5
|
+
// distinguish "user passed flag" from "not passed" and fall through to .sweny.yml.
|
|
6
|
+
return program
|
|
7
|
+
.command("triage")
|
|
8
|
+
.description("Run the SWEny triage workflow")
|
|
9
|
+
.option("--agent <provider>", "Coding agent: claude (default), codex, gemini")
|
|
10
|
+
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
11
|
+
.option("--observability-provider <provider>", "Observability provider (default: datadog)")
|
|
12
|
+
.option("--issue-tracker-provider <provider>", "Issue tracker provider (default: github-issues)")
|
|
13
|
+
.option("--source-control-provider <provider>", "Source control provider (default: github)")
|
|
14
|
+
.option("--notification-provider <provider>", "Notification provider (default: console)")
|
|
15
|
+
.option("--time-range <range>", "Time range to analyze (default: 24h)")
|
|
16
|
+
.option("--severity-focus <focus>", "Severity level focus (default: errors)")
|
|
17
|
+
.option("--service-filter <filter>", "Service filter pattern (default: *)")
|
|
18
|
+
.option("--investigation-depth <depth>", "Investigation depth (default: standard)")
|
|
19
|
+
.option("--max-investigate-turns <n>", "Max Claude turns for investigation (default: 50)")
|
|
20
|
+
.option("--max-implement-turns <n>", "Max Claude turns for implementation (default: 30)")
|
|
21
|
+
.option("--base-branch <branch>", "Base branch for PRs (default: main)")
|
|
22
|
+
.option("--pr-labels <labels>", "Comma-separated PR labels (default: agent,triage,needs-review)")
|
|
23
|
+
.option("--dry-run", "Analyze only, do not create issues or PRs", false)
|
|
24
|
+
.option("--review-mode <mode>", "PR merge behavior: auto (merge when CI passes) | review (human approval, default)", "review")
|
|
25
|
+
.option("--no-novelty-mode", "Disable novelty mode (allow +1 on existing issues)")
|
|
26
|
+
.option("--issue-override <issue>", "Work on a specific existing issue")
|
|
27
|
+
.option("--additional-instructions <text>", "Extra instructions for the Claude agent")
|
|
28
|
+
.option("--service-map-path <path>", "Path to service map YAML (default: .github/service-map.yml)")
|
|
29
|
+
.option("--repository <owner/repo>", "Repository (auto-detected from git remote)")
|
|
30
|
+
.option("--linear-team-id <id>", "Linear team ID")
|
|
31
|
+
.option("--linear-bug-label-id <id>", "Linear bug label ID")
|
|
32
|
+
.option("--linear-triage-label-id <id>", "Linear triage label ID")
|
|
33
|
+
.option("--issue-labels <labels>", "Comma-separated extra labels applied to every agent-created issue (e.g. agent UUID for Linear, 'agent' for GitHub Issues)")
|
|
34
|
+
.option("--linear-state-backlog <name>", "Linear backlog state name")
|
|
35
|
+
.option("--linear-state-in-progress <name>", "Linear in-progress state name")
|
|
36
|
+
.option("--linear-state-peer-review <name>", "Linear peer-review state name")
|
|
37
|
+
.option("--log-file <path>", "Path to JSON log file (use with --observability-provider file)")
|
|
38
|
+
.option("--dd-site <site>", "Datadog site (default: datadoghq.com)")
|
|
39
|
+
.option("--sentry-org <org>", "Sentry organization slug")
|
|
40
|
+
.option("--sentry-project <project>", "Sentry project slug")
|
|
41
|
+
.option("--sentry-base-url <url>", "Sentry base URL (default: https://sentry.io)")
|
|
42
|
+
.option("--cloudwatch-region <region>", "AWS CloudWatch region (default: us-east-1)")
|
|
43
|
+
.option("--cloudwatch-log-group-prefix <prefix>", "CloudWatch log group prefix")
|
|
44
|
+
.option("--splunk-index <index>", "Splunk index (default: main)")
|
|
45
|
+
.option("--elastic-index <index>", "Elasticsearch index (default: logs-*)")
|
|
46
|
+
.option("--newrelic-region <region>", "New Relic region (default: us)")
|
|
47
|
+
.option("--prometheus-url <url>", "Prometheus base URL (required for prometheus provider)")
|
|
48
|
+
.option("--prometheus-token <token>", "Prometheus bearer token (optional)")
|
|
49
|
+
.option("--heroku-app-name <name>", "Heroku application name (required for heroku provider)")
|
|
50
|
+
.option("--opsgenie-region <region>", "OpsGenie region (default: us)")
|
|
51
|
+
.option("--honeycomb-dataset <name>", "Honeycomb dataset name (required for honeycomb provider)")
|
|
52
|
+
.option("--axiom-dataset <name>", "Axiom dataset name (required for axiom provider)")
|
|
53
|
+
.option("--axiom-org-id <id>", "Axiom org ID (required for multi-org Axiom accounts)")
|
|
54
|
+
.option("--betterstack-source-id <id>", "Better Stack log source ID (used to auto-discover ClickHouse table)")
|
|
55
|
+
.option("--betterstack-table-name <name>", 'Better Stack ClickHouse table name, e.g. "t273774.my_source"')
|
|
56
|
+
.option("--gitlab-base-url <url>", "GitLab base URL (default: https://gitlab.com)")
|
|
57
|
+
.option("--json", "Output results as JSON", false)
|
|
58
|
+
.option("--bell", "Ring terminal bell on completion", false)
|
|
59
|
+
.option("--cache-dir <path>", "Step cache directory (default: .sweny/cache)")
|
|
60
|
+
.option("--cache-ttl <seconds>", "Cache TTL in seconds, 0 = infinite (default: 86400)")
|
|
61
|
+
.option("--no-cache", "Disable step cache")
|
|
62
|
+
.option("--output-dir <path>", "Output directory for file providers (default: .sweny/output)")
|
|
63
|
+
.option("--workspace-tools <tools>", "Comma-separated workspace tool integrations to enable (slack, notion, pagerduty, monday)");
|
|
64
|
+
}
|
|
65
|
+
export function parseCliInputs(options, fileConfig = {}) {
|
|
66
|
+
const env = process.env;
|
|
67
|
+
// Config file lookup helper: CLI flag > env var > file > default
|
|
68
|
+
const f = (key) => fileConfig[key] || undefined;
|
|
69
|
+
const obsProvider = options.observabilityProvider || f("observability-provider") || "datadog";
|
|
70
|
+
return {
|
|
71
|
+
codingAgentProvider: options.agent || options.codingAgentProvider || f("coding-agent-provider") || "claude",
|
|
72
|
+
// Secrets: env only — never from config file
|
|
73
|
+
anthropicApiKey: env.ANTHROPIC_API_KEY || "",
|
|
74
|
+
claudeOauthToken: env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
|
75
|
+
openaiApiKey: env.OPENAI_API_KEY || "",
|
|
76
|
+
geminiApiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "",
|
|
77
|
+
observabilityProvider: obsProvider,
|
|
78
|
+
observabilityCredentials: parseObservabilityCredentials(obsProvider, options, fileConfig),
|
|
79
|
+
issueTrackerProvider: options.issueTrackerProvider || f("issue-tracker-provider") || "github-issues",
|
|
80
|
+
linearApiKey: env.LINEAR_API_KEY || "",
|
|
81
|
+
linearTeamId: options.linearTeamId || env.LINEAR_TEAM_ID || f("linear-team-id") || "",
|
|
82
|
+
linearBugLabelId: options.linearBugLabelId || env.LINEAR_BUG_LABEL_ID || f("linear-bug-label-id") || "",
|
|
83
|
+
linearTriageLabelId: options.linearTriageLabelId || env.LINEAR_TRIAGE_LABEL_ID || f("linear-triage-label-id") || "",
|
|
84
|
+
linearStateBacklog: options.linearStateBacklog || env.LINEAR_STATE_BACKLOG || f("linear-state-backlog") || "",
|
|
85
|
+
linearStateInProgress: options.linearStateInProgress || env.LINEAR_STATE_IN_PROGRESS || f("linear-state-in-progress") || "",
|
|
86
|
+
linearStatePeerReview: options.linearStatePeerReview || env.LINEAR_STATE_PEER_REVIEW || f("linear-state-peer-review") || "",
|
|
87
|
+
timeRange: options.timeRange || f("time-range") || "24h",
|
|
88
|
+
severityFocus: options.severityFocus || f("severity-focus") || "errors",
|
|
89
|
+
serviceFilter: options.serviceFilter || f("service-filter") || "*",
|
|
90
|
+
investigationDepth: options.investigationDepth || f("investigation-depth") || "standard",
|
|
91
|
+
maxInvestigateTurns: parseInt(String(options.maxInvestigateTurns || f("max-investigate-turns") || "50"), 10),
|
|
92
|
+
maxImplementTurns: parseInt(String(options.maxImplementTurns || f("max-implement-turns") || "30"), 10),
|
|
93
|
+
baseBranch: options.baseBranch || f("base-branch") || "main",
|
|
94
|
+
prLabels: (options.prLabels || f("pr-labels") || "agent,triage,needs-review")
|
|
95
|
+
.split(",")
|
|
96
|
+
.map((l) => l.trim()),
|
|
97
|
+
issueLabels: (options.issueLabels || env.SWENY_ISSUE_LABELS || f("issue-labels") || "")
|
|
98
|
+
.split(",")
|
|
99
|
+
.map((l) => l.trim())
|
|
100
|
+
.filter(Boolean),
|
|
101
|
+
// Per-invocation flags: CLI only — never from config file
|
|
102
|
+
dryRun: Boolean(options.dryRun),
|
|
103
|
+
reviewMode: (options.reviewMode || f("review-mode") || "review"),
|
|
104
|
+
noveltyMode: options.noveltyMode !== false,
|
|
105
|
+
issueOverride: options.issueOverride || "",
|
|
106
|
+
additionalInstructions: options.additionalInstructions || "",
|
|
107
|
+
serviceMapPath: options.serviceMapPath || f("service-map-path") || ".github/service-map.yml",
|
|
108
|
+
githubToken: env.GITHUB_TOKEN || "",
|
|
109
|
+
botToken: env.BOT_TOKEN || "",
|
|
110
|
+
sourceControlProvider: options.sourceControlProvider || f("source-control-provider") || "github",
|
|
111
|
+
// Secrets: env only
|
|
112
|
+
jiraBaseUrl: env.JIRA_BASE_URL || "",
|
|
113
|
+
jiraEmail: env.JIRA_EMAIL || "",
|
|
114
|
+
jiraApiToken: env.JIRA_API_TOKEN || "",
|
|
115
|
+
gitlabToken: env.GITLAB_TOKEN || "",
|
|
116
|
+
gitlabProjectId: env.GITLAB_PROJECT_ID || "",
|
|
117
|
+
gitlabBaseUrl: options.gitlabBaseUrl || env.GITLAB_BASE_URL || f("gitlab-base-url") || "https://gitlab.com",
|
|
118
|
+
notificationProvider: options.notificationProvider || f("notification-provider") || "console",
|
|
119
|
+
// Secrets: env only
|
|
120
|
+
notificationWebhookUrl: env.NOTIFICATION_WEBHOOK_URL || "",
|
|
121
|
+
sendgridApiKey: env.SENDGRID_API_KEY || "",
|
|
122
|
+
emailFrom: env.EMAIL_FROM || "",
|
|
123
|
+
emailTo: env.EMAIL_TO || "",
|
|
124
|
+
webhookSigningSecret: env.WEBHOOK_SIGNING_SECRET || "",
|
|
125
|
+
repository: options.repository || env.GITHUB_REPOSITORY || detectRepository(),
|
|
126
|
+
repositoryOwner: env.GITHUB_REPOSITORY_OWNER || "",
|
|
127
|
+
// Per-invocation flags: CLI only
|
|
128
|
+
json: Boolean(options.json),
|
|
129
|
+
bell: Boolean(options.bell),
|
|
130
|
+
cacheDir: options.cacheDir || env.SWENY_CACHE_DIR || f("cache-dir") || ".sweny/cache",
|
|
131
|
+
cacheTtl: parseInt(String(options.cacheTtl || f("cache-ttl") || "86400"), 10),
|
|
132
|
+
noCache: options.cache === false,
|
|
133
|
+
outputDir: options.outputDir || env.SWENY_OUTPUT_DIR || f("output-dir") || ".sweny/output",
|
|
134
|
+
mcpServers: parseMcpServers(env.SWENY_MCP_SERVERS || f("mcp-servers-json") || ""),
|
|
135
|
+
workspaceTools: (options.workspaceTools || env.SWENY_WORKSPACE_TOOLS || f("workspace-tools") || "")
|
|
136
|
+
.split(",")
|
|
137
|
+
.map((s) => s.trim())
|
|
138
|
+
.filter(Boolean),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* All recognized workspace tool names. Update here when adding a new Category B MCP server.
|
|
143
|
+
*
|
|
144
|
+
* Requirements to add a tool:
|
|
145
|
+
* - Must have an HTTP transport endpoint (preferred) OR a stable pre-installed binary path
|
|
146
|
+
* - npx -y is intentionally NOT used — runtime package downloads bypass lockfiles
|
|
147
|
+
* - Asana is intentionally absent: no stable HTTP MCP endpoint as of current release
|
|
148
|
+
*/
|
|
149
|
+
export const SUPPORTED_WORKSPACE_TOOLS = new Set(["slack", "notion", "pagerduty", "monday"]);
|
|
150
|
+
export function validateInputs(config) {
|
|
151
|
+
const errors = [];
|
|
152
|
+
// Auth: validate per coding agent
|
|
153
|
+
switch (config.codingAgentProvider) {
|
|
154
|
+
case "claude":
|
|
155
|
+
if (!config.anthropicApiKey && !config.claudeOauthToken) {
|
|
156
|
+
errors.push("Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN — get a key at https://console.anthropic.com");
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
case "codex":
|
|
160
|
+
if (!config.openaiApiKey) {
|
|
161
|
+
errors.push("Missing: OPENAI_API_KEY — get a key at https://platform.openai.com/api-keys");
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case "gemini":
|
|
165
|
+
if (!config.geminiApiKey) {
|
|
166
|
+
errors.push("Missing: GEMINI_API_KEY or GOOGLE_API_KEY — get a key at https://aistudio.google.com/app/apikey");
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
errors.push(`Unsupported coding agent provider: ${config.codingAgentProvider} (use claude, codex, or gemini)`);
|
|
171
|
+
}
|
|
172
|
+
// Repository required unless all providers are file-based
|
|
173
|
+
const allLocal = config.issueTrackerProvider === "file" && config.sourceControlProvider === "file";
|
|
174
|
+
if (!config.repository && !allLocal) {
|
|
175
|
+
errors.push("Missing: --repository <owner/repo> or GITHUB_REPOSITORY (could not auto-detect from git remote)");
|
|
176
|
+
}
|
|
177
|
+
// Observability credentials by provider
|
|
178
|
+
switch (config.observabilityProvider) {
|
|
179
|
+
case "datadog":
|
|
180
|
+
if (!config.observabilityCredentials.apiKey)
|
|
181
|
+
errors.push("Missing: DD_API_KEY — find API keys at https://app.datadoghq.com/organization-settings/api-keys");
|
|
182
|
+
if (!config.observabilityCredentials.appKey)
|
|
183
|
+
errors.push("Missing: DD_APP_KEY — find application keys at https://app.datadoghq.com/organization-settings/application-keys");
|
|
184
|
+
break;
|
|
185
|
+
case "sentry":
|
|
186
|
+
if (!config.observabilityCredentials.authToken)
|
|
187
|
+
errors.push("Missing: SENTRY_AUTH_TOKEN — create a token at https://sentry.io/settings/auth-tokens/");
|
|
188
|
+
if (!config.observabilityCredentials.organization)
|
|
189
|
+
errors.push("Missing: --sentry-org is required for sentry provider");
|
|
190
|
+
if (!config.observabilityCredentials.project)
|
|
191
|
+
errors.push("Missing: --sentry-project is required for sentry provider");
|
|
192
|
+
break;
|
|
193
|
+
case "cloudwatch":
|
|
194
|
+
if (!config.observabilityCredentials.logGroupPrefix)
|
|
195
|
+
errors.push("Missing: --cloudwatch-log-group-prefix is required for cloudwatch provider");
|
|
196
|
+
break;
|
|
197
|
+
case "splunk":
|
|
198
|
+
if (!config.observabilityCredentials.baseUrl)
|
|
199
|
+
errors.push("Missing: SPLUNK_URL is required for splunk provider");
|
|
200
|
+
if (!config.observabilityCredentials.token)
|
|
201
|
+
errors.push("Missing: SPLUNK_TOKEN is required for splunk provider");
|
|
202
|
+
break;
|
|
203
|
+
case "elastic":
|
|
204
|
+
if (!config.observabilityCredentials.baseUrl)
|
|
205
|
+
errors.push("Missing: ELASTIC_URL is required for elastic provider");
|
|
206
|
+
if (!config.observabilityCredentials.apiKey)
|
|
207
|
+
errors.push("Missing: ELASTIC_API_KEY is required for elastic provider");
|
|
208
|
+
break;
|
|
209
|
+
case "newrelic":
|
|
210
|
+
if (!config.observabilityCredentials.apiKey)
|
|
211
|
+
errors.push("Missing: NR_API_KEY is required for newrelic provider");
|
|
212
|
+
if (!config.observabilityCredentials.accountId)
|
|
213
|
+
errors.push("Missing: NR_ACCOUNT_ID is required for newrelic provider");
|
|
214
|
+
break;
|
|
215
|
+
case "loki":
|
|
216
|
+
if (!config.observabilityCredentials.baseUrl)
|
|
217
|
+
errors.push("Missing: LOKI_URL is required for loki provider");
|
|
218
|
+
break;
|
|
219
|
+
case "file":
|
|
220
|
+
if (!config.observabilityCredentials.path)
|
|
221
|
+
errors.push("Missing: --log-file <path> is required for file provider");
|
|
222
|
+
break;
|
|
223
|
+
case "prometheus":
|
|
224
|
+
if (!config.observabilityCredentials.url)
|
|
225
|
+
errors.push("Missing: PROMETHEUS_URL or --prometheus-url is required for prometheus provider");
|
|
226
|
+
break;
|
|
227
|
+
case "pagerduty":
|
|
228
|
+
if (!config.observabilityCredentials.apiKey)
|
|
229
|
+
errors.push("Missing: PAGERDUTY_API_KEY is required for pagerduty provider");
|
|
230
|
+
break;
|
|
231
|
+
case "heroku":
|
|
232
|
+
if (!config.observabilityCredentials.apiKey)
|
|
233
|
+
errors.push("Missing: HEROKU_API_KEY is required for heroku provider");
|
|
234
|
+
if (!config.observabilityCredentials.appName)
|
|
235
|
+
errors.push("Missing: HEROKU_APP_NAME or --heroku-app-name is required for heroku provider");
|
|
236
|
+
break;
|
|
237
|
+
case "opsgenie":
|
|
238
|
+
if (!config.observabilityCredentials.apiKey)
|
|
239
|
+
errors.push("Missing: OPSGENIE_API_KEY is required for opsgenie provider");
|
|
240
|
+
break;
|
|
241
|
+
case "honeycomb":
|
|
242
|
+
if (!config.observabilityCredentials.apiKey)
|
|
243
|
+
errors.push("Missing: HONEYCOMB_API_KEY is required for honeycomb provider");
|
|
244
|
+
if (!config.observabilityCredentials.dataset)
|
|
245
|
+
errors.push("Missing: HONEYCOMB_DATASET is required for honeycomb provider");
|
|
246
|
+
break;
|
|
247
|
+
case "axiom":
|
|
248
|
+
if (!config.observabilityCredentials.apiToken)
|
|
249
|
+
errors.push("Missing: AXIOM_TOKEN is required for axiom provider");
|
|
250
|
+
if (!config.observabilityCredentials.dataset)
|
|
251
|
+
errors.push("Missing: AXIOM_DATASET or --axiom-dataset is required for axiom provider");
|
|
252
|
+
break;
|
|
253
|
+
case "betterstack":
|
|
254
|
+
if (!config.observabilityCredentials.apiToken)
|
|
255
|
+
errors.push("Missing: BETTERSTACK_API_TOKEN is required for betterstack provider");
|
|
256
|
+
if (!config.observabilityCredentials.sourceId && !config.observabilityCredentials.tableName)
|
|
257
|
+
errors.push("Missing: either BETTERSTACK_SOURCE_ID (--betterstack-source-id) or BETTERSTACK_TABLE_NAME (--betterstack-table-name) is required for betterstack provider");
|
|
258
|
+
break;
|
|
259
|
+
case "vercel":
|
|
260
|
+
if (!config.observabilityCredentials.token)
|
|
261
|
+
errors.push("Missing: VERCEL_TOKEN — create a token at https://vercel.com/account/tokens");
|
|
262
|
+
if (!config.observabilityCredentials.projectId)
|
|
263
|
+
errors.push("Missing: VERCEL_PROJECT_ID — find it in your Vercel project settings");
|
|
264
|
+
break;
|
|
265
|
+
case "supabase":
|
|
266
|
+
if (!config.observabilityCredentials.managementApiKey)
|
|
267
|
+
errors.push("Missing: SUPABASE_MANAGEMENT_KEY — create a token at https://supabase.com/dashboard/account/tokens");
|
|
268
|
+
if (!config.observabilityCredentials.projectRef)
|
|
269
|
+
errors.push("Missing: SUPABASE_PROJECT_REF — find it in your Supabase project settings > General");
|
|
270
|
+
break;
|
|
271
|
+
case "netlify":
|
|
272
|
+
if (!config.observabilityCredentials.token)
|
|
273
|
+
errors.push("Missing: NETLIFY_TOKEN — create a token at https://app.netlify.com/user/applications#personal-access-tokens");
|
|
274
|
+
if (!config.observabilityCredentials.siteId)
|
|
275
|
+
errors.push("Missing: NETLIFY_SITE_ID — find it in Site Settings > General > Site details");
|
|
276
|
+
break;
|
|
277
|
+
case "fly":
|
|
278
|
+
if (!config.observabilityCredentials.token)
|
|
279
|
+
errors.push("Missing: FLY_TOKEN — create a token at https://fly.io/user/personal_access_tokens");
|
|
280
|
+
if (!config.observabilityCredentials.appName)
|
|
281
|
+
errors.push("Missing: FLY_APP_NAME — the name of your Fly.io application");
|
|
282
|
+
break;
|
|
283
|
+
case "render":
|
|
284
|
+
if (!config.observabilityCredentials.apiKey)
|
|
285
|
+
errors.push("Missing: RENDER_API_KEY — create an API key at https://dashboard.render.com/u/settings");
|
|
286
|
+
if (!config.observabilityCredentials.serviceId)
|
|
287
|
+
errors.push("Missing: RENDER_SERVICE_ID — find it in your service's Settings page (srv-...)");
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
errors.push(`Unknown --observability-provider "${config.observabilityProvider}". Valid values: datadog, sentry, cloudwatch, splunk, elastic, newrelic, loki, prometheus, pagerduty, honeycomb, heroku, opsgenie, axiom, betterstack, vercel, supabase, netlify, fly, render, file`);
|
|
291
|
+
}
|
|
292
|
+
// Issue tracker credentials by provider
|
|
293
|
+
switch (config.issueTrackerProvider) {
|
|
294
|
+
case "linear":
|
|
295
|
+
if (!config.linearApiKey)
|
|
296
|
+
errors.push("Missing: LINEAR_API_KEY — find API keys at https://linear.app/settings/api");
|
|
297
|
+
if (!config.linearTeamId)
|
|
298
|
+
errors.push("Missing: LINEAR_TEAM_ID — find it in Linear > Settings > Workspace > Teams > [your team] > copy the ID from the URL");
|
|
299
|
+
break;
|
|
300
|
+
case "jira":
|
|
301
|
+
if (!config.jiraBaseUrl)
|
|
302
|
+
errors.push("Missing: JIRA_BASE_URL — set to your Atlassian domain, e.g. https://your-org.atlassian.net");
|
|
303
|
+
if (!config.jiraEmail)
|
|
304
|
+
errors.push("Missing: JIRA_EMAIL — your Atlassian account email address");
|
|
305
|
+
if (!config.jiraApiToken)
|
|
306
|
+
errors.push("Missing: JIRA_API_TOKEN — create a token at https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
307
|
+
break;
|
|
308
|
+
case "github-issues":
|
|
309
|
+
case "file":
|
|
310
|
+
// No external credentials needed
|
|
311
|
+
break;
|
|
312
|
+
default:
|
|
313
|
+
errors.push(`Unknown --issue-tracker-provider "${config.issueTrackerProvider}". Valid values: linear, jira, github-issues, file`);
|
|
314
|
+
}
|
|
315
|
+
// Source control credentials by provider
|
|
316
|
+
switch (config.sourceControlProvider) {
|
|
317
|
+
case "github":
|
|
318
|
+
if (!config.githubToken && !config.botToken)
|
|
319
|
+
errors.push("Missing: GITHUB_TOKEN — create a Personal Access Token at https://github.com/settings/tokens");
|
|
320
|
+
break;
|
|
321
|
+
case "gitlab":
|
|
322
|
+
if (!config.gitlabToken)
|
|
323
|
+
errors.push("Missing: GITLAB_TOKEN — create a Personal Access Token at https://gitlab.com/-/user_settings/personal_access_tokens (api + read_repository + write_repository scopes)");
|
|
324
|
+
if (!config.gitlabProjectId)
|
|
325
|
+
errors.push("Missing: GITLAB_PROJECT_ID — find it in GitLab > [your project] > Settings > General (numeric ID at the top)");
|
|
326
|
+
break;
|
|
327
|
+
case "file":
|
|
328
|
+
// No external credentials needed
|
|
329
|
+
break;
|
|
330
|
+
default:
|
|
331
|
+
errors.push(`Unknown --source-control-provider "${config.sourceControlProvider}". Valid values: github, gitlab, file`);
|
|
332
|
+
}
|
|
333
|
+
// Notification credentials by provider
|
|
334
|
+
switch (config.notificationProvider) {
|
|
335
|
+
case "console":
|
|
336
|
+
// No credentials needed
|
|
337
|
+
break;
|
|
338
|
+
case "slack":
|
|
339
|
+
case "teams":
|
|
340
|
+
case "discord":
|
|
341
|
+
case "webhook":
|
|
342
|
+
if (!config.notificationWebhookUrl)
|
|
343
|
+
errors.push(`Missing: NOTIFICATION_WEBHOOK_URL is required for ${config.notificationProvider} notifications`);
|
|
344
|
+
break;
|
|
345
|
+
case "email":
|
|
346
|
+
if (!config.sendgridApiKey)
|
|
347
|
+
errors.push("Missing: SENDGRID_API_KEY is required for email notifications");
|
|
348
|
+
if (!config.emailFrom)
|
|
349
|
+
errors.push("Missing: EMAIL_FROM is required for email notifications");
|
|
350
|
+
if (!config.emailTo)
|
|
351
|
+
errors.push("Missing: EMAIL_TO is required for email notifications");
|
|
352
|
+
break;
|
|
353
|
+
case "file":
|
|
354
|
+
// No external credentials needed
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
errors.push(`Unknown --notification-provider "${config.notificationProvider}". Valid values: console, slack, teams, discord, webhook, email, file`);
|
|
358
|
+
}
|
|
359
|
+
// Workspace tools: reject unknown names early
|
|
360
|
+
for (const tool of config.workspaceTools) {
|
|
361
|
+
if (!SUPPORTED_WORKSPACE_TOOLS.has(tool)) {
|
|
362
|
+
errors.push(`Unknown workspace tool: "${tool}". Supported values: ${[...SUPPORTED_WORKSPACE_TOOLS].join(", ")}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Enum validation
|
|
366
|
+
if (!["auto", "review"].includes(config.reviewMode)) {
|
|
367
|
+
errors.push("--review-mode must be one of: auto, review");
|
|
368
|
+
}
|
|
369
|
+
// Integer bounds
|
|
370
|
+
if (config.maxInvestigateTurns < 1 || config.maxInvestigateTurns > 500) {
|
|
371
|
+
errors.push("--max-investigate-turns must be between 1 and 500");
|
|
372
|
+
}
|
|
373
|
+
if (config.maxImplementTurns < 1 || config.maxImplementTurns > 500) {
|
|
374
|
+
errors.push("--max-implement-turns must be between 1 and 500");
|
|
375
|
+
}
|
|
376
|
+
return errors;
|
|
377
|
+
}
|
|
378
|
+
const DEFAULT_SERVICE_MAP_PATH = ".github/service-map.yml";
|
|
379
|
+
/**
|
|
380
|
+
* Returns non-fatal warnings about the configuration.
|
|
381
|
+
* These are surfaced before the spinner starts but do not abort the run.
|
|
382
|
+
*/
|
|
383
|
+
export function validateWarnings(config) {
|
|
384
|
+
const warnings = [];
|
|
385
|
+
// Warn when an explicitly configured serviceMapPath doesn't exist on disk.
|
|
386
|
+
// We skip the default path to avoid noise for users who never set it up.
|
|
387
|
+
if (config.serviceMapPath && config.serviceMapPath !== DEFAULT_SERVICE_MAP_PATH) {
|
|
388
|
+
if (!fs.existsSync(config.serviceMapPath)) {
|
|
389
|
+
warnings.push(`Service map file not found: "${config.serviceMapPath}". Cross-repo routing will be disabled.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return warnings;
|
|
393
|
+
}
|
|
394
|
+
function parseObservabilityCredentials(provider, options, fileConfig = {}) {
|
|
395
|
+
const env = process.env;
|
|
396
|
+
const f = (key) => fileConfig[key] || undefined;
|
|
397
|
+
switch (provider) {
|
|
398
|
+
case "datadog":
|
|
399
|
+
return {
|
|
400
|
+
apiKey: env.DD_API_KEY || "",
|
|
401
|
+
appKey: env.DD_APP_KEY || "",
|
|
402
|
+
site: options.ddSite || env.DD_SITE || f("dd-site") || "datadoghq.com",
|
|
403
|
+
};
|
|
404
|
+
case "sentry":
|
|
405
|
+
return {
|
|
406
|
+
authToken: env.SENTRY_AUTH_TOKEN || "",
|
|
407
|
+
organization: options.sentryOrg || env.SENTRY_ORG || f("sentry-org") || "",
|
|
408
|
+
project: options.sentryProject || env.SENTRY_PROJECT || f("sentry-project") || "",
|
|
409
|
+
baseUrl: options.sentryBaseUrl || env.SENTRY_BASE_URL || f("sentry-base-url") || "https://sentry.io",
|
|
410
|
+
};
|
|
411
|
+
case "cloudwatch":
|
|
412
|
+
return {
|
|
413
|
+
region: options.cloudwatchRegion || env.AWS_REGION || f("cloudwatch-region") || "us-east-1",
|
|
414
|
+
logGroupPrefix: options.cloudwatchLogGroupPrefix ||
|
|
415
|
+
env.CLOUDWATCH_LOG_GROUP_PREFIX ||
|
|
416
|
+
f("cloudwatch-log-group-prefix") ||
|
|
417
|
+
"",
|
|
418
|
+
};
|
|
419
|
+
case "splunk":
|
|
420
|
+
return {
|
|
421
|
+
baseUrl: env.SPLUNK_URL || "",
|
|
422
|
+
token: env.SPLUNK_TOKEN || "",
|
|
423
|
+
index: options.splunkIndex || env.SPLUNK_INDEX || f("splunk-index") || "main",
|
|
424
|
+
};
|
|
425
|
+
case "elastic":
|
|
426
|
+
return {
|
|
427
|
+
baseUrl: env.ELASTIC_URL || "",
|
|
428
|
+
apiKey: env.ELASTIC_API_KEY || "",
|
|
429
|
+
index: options.elasticIndex || env.ELASTIC_INDEX || f("elastic-index") || "logs-*",
|
|
430
|
+
};
|
|
431
|
+
case "newrelic":
|
|
432
|
+
return {
|
|
433
|
+
apiKey: env.NR_API_KEY || "",
|
|
434
|
+
accountId: env.NR_ACCOUNT_ID || "",
|
|
435
|
+
region: options.newrelicRegion || env.NR_REGION || f("newrelic-region") || "us",
|
|
436
|
+
};
|
|
437
|
+
case "loki":
|
|
438
|
+
return {
|
|
439
|
+
baseUrl: env.LOKI_URL || "",
|
|
440
|
+
apiKey: env.LOKI_API_KEY || "",
|
|
441
|
+
orgId: env.LOKI_ORG_ID || "",
|
|
442
|
+
};
|
|
443
|
+
case "file":
|
|
444
|
+
return {
|
|
445
|
+
path: options.logFile || env.SWENY_LOG_FILE || f("log-file") || "",
|
|
446
|
+
};
|
|
447
|
+
case "prometheus":
|
|
448
|
+
return {
|
|
449
|
+
url: options.prometheusUrl || env.PROMETHEUS_URL || f("prometheus-url") || "",
|
|
450
|
+
token: options.prometheusToken || env.PROMETHEUS_TOKEN || f("prometheus-token") || "",
|
|
451
|
+
};
|
|
452
|
+
case "pagerduty":
|
|
453
|
+
return {
|
|
454
|
+
apiKey: env.PAGERDUTY_API_KEY || "",
|
|
455
|
+
};
|
|
456
|
+
case "heroku":
|
|
457
|
+
return {
|
|
458
|
+
apiKey: env.HEROKU_API_KEY || "",
|
|
459
|
+
appName: options.herokuAppName || env.HEROKU_APP_NAME || f("heroku-app-name") || "",
|
|
460
|
+
};
|
|
461
|
+
case "opsgenie":
|
|
462
|
+
return {
|
|
463
|
+
apiKey: env.OPSGENIE_API_KEY || "",
|
|
464
|
+
region: options.opsgenieRegion || env.OPSGENIE_REGION || f("opsgenie-region") || "us",
|
|
465
|
+
};
|
|
466
|
+
case "honeycomb":
|
|
467
|
+
return {
|
|
468
|
+
apiKey: env.HONEYCOMB_API_KEY || "",
|
|
469
|
+
dataset: options.honeycombDataset || env.HONEYCOMB_DATASET || f("honeycomb-dataset") || "",
|
|
470
|
+
};
|
|
471
|
+
case "axiom":
|
|
472
|
+
return {
|
|
473
|
+
apiToken: env.AXIOM_TOKEN || "",
|
|
474
|
+
dataset: options.axiomDataset || env.AXIOM_DATASET || f("axiom-dataset") || "",
|
|
475
|
+
orgId: options.axiomOrgId || env.AXIOM_ORG_ID || f("axiom-org-id") || "",
|
|
476
|
+
};
|
|
477
|
+
case "betterstack":
|
|
478
|
+
return {
|
|
479
|
+
apiToken: env.BETTERSTACK_API_TOKEN || "",
|
|
480
|
+
sourceId: options.betterstackSourceId || env.BETTERSTACK_SOURCE_ID || f("betterstack-source-id") || "",
|
|
481
|
+
tableName: options.betterstackTableName || env.BETTERSTACK_TABLE_NAME || f("betterstack-table-name") || "",
|
|
482
|
+
};
|
|
483
|
+
case "vercel":
|
|
484
|
+
return {
|
|
485
|
+
token: env.VERCEL_TOKEN || "",
|
|
486
|
+
projectId: env.VERCEL_PROJECT_ID || f("vercel-project-id") || "",
|
|
487
|
+
teamId: env.VERCEL_TEAM_ID || f("vercel-team-id") || "",
|
|
488
|
+
};
|
|
489
|
+
case "supabase":
|
|
490
|
+
return {
|
|
491
|
+
managementApiKey: env.SUPABASE_MANAGEMENT_KEY || "",
|
|
492
|
+
projectRef: env.SUPABASE_PROJECT_REF || f("supabase-project-ref") || "",
|
|
493
|
+
};
|
|
494
|
+
case "netlify":
|
|
495
|
+
return {
|
|
496
|
+
token: env.NETLIFY_TOKEN || "",
|
|
497
|
+
siteId: env.NETLIFY_SITE_ID || f("netlify-site-id") || "",
|
|
498
|
+
};
|
|
499
|
+
case "fly":
|
|
500
|
+
return {
|
|
501
|
+
token: env.FLY_TOKEN || "",
|
|
502
|
+
appName: env.FLY_APP_NAME || f("fly-app-name") || "",
|
|
503
|
+
};
|
|
504
|
+
case "render":
|
|
505
|
+
return {
|
|
506
|
+
apiKey: env.RENDER_API_KEY || "",
|
|
507
|
+
serviceId: env.RENDER_SERVICE_ID || f("render-service-id") || "",
|
|
508
|
+
};
|
|
509
|
+
default:
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
export function registerImplementCommand(program) {
|
|
514
|
+
return program
|
|
515
|
+
.command("implement <issueId>")
|
|
516
|
+
.description("Implement a fix for a specific issue and open a PR")
|
|
517
|
+
.option("--agent <provider>", "Coding agent: claude (default), codex, gemini")
|
|
518
|
+
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
519
|
+
.option("--issue-tracker-provider <provider>", "Issue tracker (linear|jira|github-issues|file)")
|
|
520
|
+
.option("--source-control-provider <provider>", "Source control (github|gitlab|file)")
|
|
521
|
+
.option("--dry-run", "Skip creating PR — report only", false)
|
|
522
|
+
.option("--max-implement-turns <n>", "Max coding agent turns (default: 40)")
|
|
523
|
+
.option("--base-branch <branch>", "Base branch for PRs (default: main)")
|
|
524
|
+
.option("--repository <owner/repo>", "Repository (auto-detected from git remote)")
|
|
525
|
+
.option("--linear-team-id <id>", "Linear team ID")
|
|
526
|
+
.option("--linear-state-in-progress <name>", "Linear in-progress state name")
|
|
527
|
+
.option("--linear-state-peer-review <name>", "Linear peer-review state name")
|
|
528
|
+
.option("--output-dir <path>", "Output directory for file providers (default: .sweny/output)")
|
|
529
|
+
.option("--workspace-tools <tools>", "Comma-separated workspace tool integrations to enable (slack, notion, pagerduty, monday)")
|
|
530
|
+
.option("--review-mode <mode>", "PR merge behavior: auto (GitHub auto-merge when CI passes) | review (human approval, default)", "review")
|
|
531
|
+
.option("--additional-instructions <text>", "Extra instructions for the coding agent");
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Parse a JSON string into MCP server configs.
|
|
535
|
+
* Returns empty object on blank input; throws on malformed JSON.
|
|
536
|
+
*
|
|
537
|
+
* Expected format (JSON):
|
|
538
|
+
* {"datadog":{"type":"http","url":"https://...","headers":{"DD_API_KEY":"..."}}}
|
|
539
|
+
*
|
|
540
|
+
* In .sweny.yml, set as a single-line JSON value:
|
|
541
|
+
* mcp-servers-json: '{"datadog":{"type":"http","url":"..."}}'
|
|
542
|
+
*
|
|
543
|
+
* Or via env var:
|
|
544
|
+
* SWENY_MCP_SERVERS='{"datadog":{"type":"http","url":"..."}}'
|
|
545
|
+
*/
|
|
546
|
+
function parseMcpServers(json) {
|
|
547
|
+
if (!json.trim())
|
|
548
|
+
return {};
|
|
549
|
+
try {
|
|
550
|
+
return JSON.parse(json);
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
throw new Error(`Invalid mcp-servers-json: expected a JSON object mapping server names to McpServerConfig.\n Got: ${json.slice(0, 120)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function detectRepository() {
|
|
557
|
+
try {
|
|
558
|
+
const remote = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
559
|
+
const match = remote.match(/[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
|
|
560
|
+
return match?.[1] ?? "";
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return "";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { Workflow } from "../types.js";
|
|
3
|
+
export declare function loadWorkflowFile(filePath: string): Workflow;
|
|
4
|
+
export declare function workflowRunAction(file: string, options: Record<string, unknown> & {
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
export declare function workflowExportAction(name: string): void;
|
|
8
|
+
export declare function workflowValidateAction(file: string, options: {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}): void;
|