@action-llama/action-llama 0.1.1
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/README.md +448 -0
- package/dist/agents/container-entry.d.ts +2 -0
- package/dist/agents/container-entry.d.ts.map +1 -0
- package/dist/agents/container-entry.js +173 -0
- package/dist/agents/container-entry.js.map +1 -0
- package/dist/agents/container-runner.d.ts +20 -0
- package/dist/agents/container-runner.d.ts.map +1 -0
- package/dist/agents/container-runner.js +208 -0
- package/dist/agents/container-runner.js.map +1 -0
- package/dist/agents/definitions/dev/AGENTS.md +44 -0
- package/dist/agents/definitions/dev/config-definition.json +39 -0
- package/dist/agents/definitions/devops/AGENTS.md +33 -0
- package/dist/agents/definitions/devops/config-definition.json +37 -0
- package/dist/agents/definitions/loader.d.ts +18 -0
- package/dist/agents/definitions/loader.d.ts.map +1 -0
- package/dist/agents/definitions/loader.js +59 -0
- package/dist/agents/definitions/loader.js.map +1 -0
- package/dist/agents/definitions/reviewer/AGENTS.md +37 -0
- package/dist/agents/definitions/reviewer/config-definition.json +24 -0
- package/dist/agents/definitions/schema.d.ts +38 -0
- package/dist/agents/definitions/schema.d.ts.map +1 -0
- package/dist/agents/definitions/schema.js +97 -0
- package/dist/agents/definitions/schema.js.map +1 -0
- package/dist/agents/prompt.d.ts +6 -0
- package/dist/agents/prompt.d.ts.map +1 -0
- package/dist/agents/prompt.js +42 -0
- package/dist/agents/prompt.js.map +1 -0
- package/dist/agents/runner.d.ts +14 -0
- package/dist/agents/runner.d.ts.map +1 -0
- package/dist/agents/runner.js +148 -0
- package/dist/agents/runner.js.map +1 -0
- package/dist/broker/index.d.ts +16 -0
- package/dist/broker/index.d.ts.map +1 -0
- package/dist/broker/index.js +53 -0
- package/dist/broker/index.js.map +1 -0
- package/dist/broker/router.d.ts +13 -0
- package/dist/broker/router.d.ts.map +1 -0
- package/dist/broker/router.js +71 -0
- package/dist/broker/router.js.map +1 -0
- package/dist/broker/routes/shutdown.d.ts +4 -0
- package/dist/broker/routes/shutdown.d.ts.map +1 -0
- package/dist/broker/routes/shutdown.js +37 -0
- package/dist/broker/routes/shutdown.js.map +1 -0
- package/dist/broker/routes/webhooks.d.ts +5 -0
- package/dist/broker/routes/webhooks.d.ts.map +1 -0
- package/dist/broker/routes/webhooks.js +50 -0
- package/dist/broker/routes/webhooks.js.map +1 -0
- package/dist/cli/commands/agent/add.d.ts +5 -0
- package/dist/cli/commands/agent/add.d.ts.map +1 -0
- package/dist/cli/commands/agent/add.js +86 -0
- package/dist/cli/commands/agent/add.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +75 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +7 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +121 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +44 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +4 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +52 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +75 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/docker/container.d.ts +19 -0
- package/dist/docker/container.d.ts.map +1 -0
- package/dist/docker/container.js +73 -0
- package/dist/docker/container.js.map +1 -0
- package/dist/docker/image.d.ts +4 -0
- package/dist/docker/image.d.ts.map +1 -0
- package/dist/docker/image.js +38 -0
- package/dist/docker/image.js.map +1 -0
- package/dist/docker/network.d.ts +5 -0
- package/dist/docker/network.d.ts.map +1 -0
- package/dist/docker/network.js +23 -0
- package/dist/docker/network.js.map +1 -0
- package/dist/scheduler/event-queue.d.ts +12 -0
- package/dist/scheduler/event-queue.d.ts.map +1 -0
- package/dist/scheduler/event-queue.js +12 -0
- package/dist/scheduler/event-queue.js.map +1 -0
- package/dist/scheduler/index.d.ts +19 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +192 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/types.d.ts +14 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +2 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/setup/prompts.d.ts +52 -0
- package/dist/setup/prompts.d.ts.map +1 -0
- package/dist/setup/prompts.js +656 -0
- package/dist/setup/prompts.js.map +1 -0
- package/dist/setup/scaffold.d.ts +11 -0
- package/dist/setup/scaffold.d.ts.map +1 -0
- package/dist/setup/scaffold.js +70 -0
- package/dist/setup/scaffold.js.map +1 -0
- package/dist/setup/validators.d.ts +23 -0
- package/dist/setup/validators.d.ts.map +1 -0
- package/dist/setup/validators.js +61 -0
- package/dist/setup/validators.js.map +1 -0
- package/dist/shared/config.d.ts +40 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +46 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/credentials.d.ts +4 -0
- package/dist/shared/credentials.d.ts.map +1 -0
- package/dist/shared/credentials.js +21 -0
- package/dist/shared/credentials.js.map +1 -0
- package/dist/shared/git.d.ts +3 -0
- package/dist/shared/git.d.ts.map +1 -0
- package/dist/shared/git.js +24 -0
- package/dist/shared/git.js.map +1 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +47 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/paths.d.ts +8 -0
- package/dist/shared/paths.d.ts.map +1 -0
- package/dist/shared/paths.js +20 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/tui/App.d.ts +5 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +85 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/render.d.ts +5 -0
- package/dist/tui/render.d.ts.map +1 -0
- package/dist/tui/render.js +9 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/status-tracker.d.ts +39 -0
- package/dist/tui/status-tracker.d.ts.map +1 -0
- package/dist/tui/status-tracker.js +73 -0
- package/dist/tui/status-tracker.js.map +1 -0
- package/dist/webhooks/providers/github.d.ts +9 -0
- package/dist/webhooks/providers/github.d.ts.map +1 -0
- package/dist/webhooks/providers/github.js +169 -0
- package/dist/webhooks/providers/github.js.map +1 -0
- package/dist/webhooks/registry.d.ts +13 -0
- package/dist/webhooks/registry.d.ts.map +1 -0
- package/dist/webhooks/registry.js +82 -0
- package/dist/webhooks/registry.js.map +1 -0
- package/dist/webhooks/types.d.ts +49 -0
- package/dist/webhooks/types.d.ts.map +1 -0
- package/dist/webhooks/types.js +2 -0
- package/dist/webhooks/types.js.map +1 -0
- package/docker/Dockerfile +29 -0
- package/package.json +67 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { input, select, checkbox, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { validateGitHubToken, validateSentryToken, validateSentryProjects, validateAnthropicApiKey, validateOAuthTokenFormat } from "./validators.js";
|
|
5
|
+
import { loadCredential } from "../shared/credentials.js";
|
|
6
|
+
import { CREDENTIALS_DIR } from "../shared/paths.js";
|
|
7
|
+
import { listBuiltinDefinitions, loadDefinition } from "../agents/definitions/loader.js";
|
|
8
|
+
// --- Shared: configure a single agent from a definition ---
|
|
9
|
+
export async function configureAgent(definition, context) {
|
|
10
|
+
// Agent name
|
|
11
|
+
const name = await input({
|
|
12
|
+
message: "Agent name:",
|
|
13
|
+
default: definition.name,
|
|
14
|
+
validate: (v) => {
|
|
15
|
+
const trimmed = v.trim();
|
|
16
|
+
if (!trimmed)
|
|
17
|
+
return "Name is required";
|
|
18
|
+
if (context.existingAgentNames?.includes(trimmed))
|
|
19
|
+
return `Agent "${trimmed}" already exists`;
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
// Repos
|
|
24
|
+
const repoChoices = context.availableRepos.map((r) => ({
|
|
25
|
+
name: r.fullName,
|
|
26
|
+
value: r.fullName,
|
|
27
|
+
}));
|
|
28
|
+
const repos = await checkbox({
|
|
29
|
+
message: `Repos for ${name}:`,
|
|
30
|
+
choices: repoChoices,
|
|
31
|
+
validate: (v) => (v.length > 0 ? true : "Select at least one repo"),
|
|
32
|
+
});
|
|
33
|
+
// Credentials — handle required and optional
|
|
34
|
+
const credentials = [...definition.credentials.required];
|
|
35
|
+
let sentryToken;
|
|
36
|
+
let sentryOrg;
|
|
37
|
+
let sentryProjectSlugs = [];
|
|
38
|
+
for (const cred of definition.credentials.optional) {
|
|
39
|
+
if (cred === "sentry-token") {
|
|
40
|
+
const result = await promptSentryCredential();
|
|
41
|
+
sentryToken = result.sentryToken;
|
|
42
|
+
sentryOrg = result.sentryOrg;
|
|
43
|
+
sentryProjectSlugs = result.sentryProjectSlugs;
|
|
44
|
+
if (sentryToken) {
|
|
45
|
+
credentials.push("sentry-token");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Params — prompt for each non-credential param; resolve credential params
|
|
50
|
+
const params = {};
|
|
51
|
+
for (const [key, paramDef] of Object.entries(definition.params)) {
|
|
52
|
+
// Credential-linked params are populated by the credential handler above
|
|
53
|
+
if (paramDef.credential === "sentry-token") {
|
|
54
|
+
if (key === "sentryOrg" && sentryOrg) {
|
|
55
|
+
params[key] = sentryOrg;
|
|
56
|
+
}
|
|
57
|
+
else if (key === "sentryProjects" && sentryProjectSlugs.length > 0) {
|
|
58
|
+
params[key] = sentryProjectSlugs;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Resolve special defaults
|
|
63
|
+
let defaultValue = paramDef.default;
|
|
64
|
+
if (defaultValue === "$githubUser") {
|
|
65
|
+
defaultValue = context.githubUser;
|
|
66
|
+
}
|
|
67
|
+
if (paramDef.type === "string") {
|
|
68
|
+
const value = await input({
|
|
69
|
+
message: `${paramDef.description}:`,
|
|
70
|
+
default: defaultValue,
|
|
71
|
+
...(paramDef.required ? { validate: (v) => v.trim().length > 0 ? true : `${key} is required` } : {}),
|
|
72
|
+
});
|
|
73
|
+
if (value.trim()) {
|
|
74
|
+
params[key] = value.trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (paramDef.type === "string[]") {
|
|
78
|
+
const value = await input({
|
|
79
|
+
message: `${paramDef.description} (comma-separated):`,
|
|
80
|
+
default: defaultValue,
|
|
81
|
+
});
|
|
82
|
+
if (value.trim()) {
|
|
83
|
+
params[key] = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Webhook trigger
|
|
88
|
+
const useWebhooks = await confirm({
|
|
89
|
+
message: `Listen for webhooks? (${definition.webhooks.description})`,
|
|
90
|
+
default: true,
|
|
91
|
+
});
|
|
92
|
+
let webhooks;
|
|
93
|
+
if (useWebhooks) {
|
|
94
|
+
const filter = buildWebhookFilter(definition, repos, params);
|
|
95
|
+
webhooks = { filters: [filter] };
|
|
96
|
+
}
|
|
97
|
+
// Schedule trigger
|
|
98
|
+
const useSchedule = await confirm({
|
|
99
|
+
message: "Also run on a schedule (polling)?",
|
|
100
|
+
default: !useWebhooks,
|
|
101
|
+
});
|
|
102
|
+
let schedule;
|
|
103
|
+
if (useSchedule) {
|
|
104
|
+
schedule = await input({
|
|
105
|
+
message: `${name} poll interval (cron):`,
|
|
106
|
+
default: definition.defaultSchedule,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Select prompt based on trigger mode
|
|
110
|
+
const prompt = useWebhooks ? definition.prompts.webhook : definition.prompts.schedule;
|
|
111
|
+
// Webhook secret
|
|
112
|
+
let githubWebhookSecret;
|
|
113
|
+
if (useWebhooks) {
|
|
114
|
+
const existingSecret = loadCredential("github-webhook-secret");
|
|
115
|
+
if (!existingSecret) {
|
|
116
|
+
githubWebhookSecret = (await input({
|
|
117
|
+
message: "GitHub webhook secret (set this same value in your GitHub webhook settings):",
|
|
118
|
+
validate: (v) => (v.trim().length > 0 ? true : "Secret is required to verify webhook payloads"),
|
|
119
|
+
})).trim();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Build agent config
|
|
123
|
+
const config = {
|
|
124
|
+
name,
|
|
125
|
+
credentials,
|
|
126
|
+
model: context.modelConfig,
|
|
127
|
+
prompt,
|
|
128
|
+
repos,
|
|
129
|
+
...(schedule ? { schedule } : {}),
|
|
130
|
+
...(webhooks ? { webhooks } : {}),
|
|
131
|
+
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
agent: { name, template: definition.name, config },
|
|
135
|
+
secrets: { sentryToken, githubWebhookSecret },
|
|
136
|
+
usesWebhooks: useWebhooks,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// --- Build webhook filter from definition + params ---
|
|
140
|
+
function buildWebhookFilter(definition, repos, params) {
|
|
141
|
+
const filter = {
|
|
142
|
+
source: "github",
|
|
143
|
+
repos,
|
|
144
|
+
events: definition.webhooks.events,
|
|
145
|
+
actions: definition.webhooks.actions,
|
|
146
|
+
};
|
|
147
|
+
// Inject param values into filter via webhookFilter mappings
|
|
148
|
+
for (const [key, paramDef] of Object.entries(definition.params)) {
|
|
149
|
+
if (paramDef.webhookFilter && params[key] !== undefined) {
|
|
150
|
+
const value = params[key];
|
|
151
|
+
const field = paramDef.webhookFilter.field;
|
|
152
|
+
if (paramDef.webhookFilter.wrap === "array") {
|
|
153
|
+
filter[field] = [value];
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
filter[field] = value;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return filter;
|
|
161
|
+
}
|
|
162
|
+
// --- Sentry credential handler ---
|
|
163
|
+
async function promptSentryCredential() {
|
|
164
|
+
let sentryToken;
|
|
165
|
+
let sentryOrg;
|
|
166
|
+
let sentryProjectSlugs = [];
|
|
167
|
+
const existingSentryToken = loadCredential("sentry-token");
|
|
168
|
+
if (existingSentryToken) {
|
|
169
|
+
const reuse = await confirm({
|
|
170
|
+
message: `Found existing Sentry token in ${CREDENTIALS_DIR}/sentry-token. Use it?`,
|
|
171
|
+
default: true,
|
|
172
|
+
});
|
|
173
|
+
if (reuse) {
|
|
174
|
+
sentryToken = existingSentryToken;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!sentryToken) {
|
|
178
|
+
const useSentry = await confirm({
|
|
179
|
+
message: "Configure Sentry integration?",
|
|
180
|
+
default: false,
|
|
181
|
+
});
|
|
182
|
+
if (useSentry) {
|
|
183
|
+
sentryToken = (await input({
|
|
184
|
+
message: "Sentry auth token:",
|
|
185
|
+
validate: (v) => (v.trim().length > 0 ? true : "Token is required"),
|
|
186
|
+
})).trim();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (sentryToken) {
|
|
190
|
+
console.log("Validating Sentry token...");
|
|
191
|
+
try {
|
|
192
|
+
const { organizations } = await validateSentryToken(sentryToken);
|
|
193
|
+
if (organizations.length === 0)
|
|
194
|
+
throw new Error("No organizations found");
|
|
195
|
+
if (organizations.length === 1) {
|
|
196
|
+
sentryOrg = organizations[0].slug;
|
|
197
|
+
console.log(`Organization: ${sentryOrg}\n`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
sentryOrg = await select({
|
|
201
|
+
message: "Select Sentry organization:",
|
|
202
|
+
choices: organizations.map((o) => ({ name: `${o.name} (${o.slug})`, value: o.slug })),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const { projects } = await validateSentryProjects(sentryToken, sentryOrg);
|
|
206
|
+
if (projects.length > 0) {
|
|
207
|
+
sentryProjectSlugs = await checkbox({
|
|
208
|
+
message: "Select Sentry projects to monitor:",
|
|
209
|
+
choices: projects.map((p) => ({ name: p.name, value: p.slug })),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
console.log(`Sentry validation failed: ${err.message}. Skipping Sentry.\n`);
|
|
215
|
+
sentryToken = undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { sentryToken, sentryOrg, sentryProjectSlugs };
|
|
219
|
+
}
|
|
220
|
+
// --- Full interactive setup (init command) ---
|
|
221
|
+
export async function runSetup() {
|
|
222
|
+
console.log("\n=== Action Llama — Setup ===\n");
|
|
223
|
+
// Step 1: Agent Selection
|
|
224
|
+
console.log("--- Step 1: Agents ---\n");
|
|
225
|
+
const builtinDefs = listBuiltinDefinitions();
|
|
226
|
+
const selectedDefNames = await checkbox({
|
|
227
|
+
message: "Which agents do you want to create?",
|
|
228
|
+
choices: builtinDefs.map((d) => ({
|
|
229
|
+
name: `${d.name} — ${d.label} (${d.description})`,
|
|
230
|
+
value: d.name,
|
|
231
|
+
})),
|
|
232
|
+
validate: (v) => (v.length > 0 ? true : "Select at least one agent"),
|
|
233
|
+
});
|
|
234
|
+
const selectedDefs = selectedDefNames.map((name) => loadDefinition(name));
|
|
235
|
+
// Collect all required/optional credentials from selected definitions
|
|
236
|
+
const allOptionalCredentials = new Set();
|
|
237
|
+
for (const def of selectedDefs) {
|
|
238
|
+
for (const cred of def.credentials.optional) {
|
|
239
|
+
allOptionalCredentials.add(cred);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Step 2: Credentials
|
|
243
|
+
console.log("\n--- Step 2: Credentials ---\n");
|
|
244
|
+
// GitHub token (always required)
|
|
245
|
+
const existingGithubToken = loadCredential("github-token");
|
|
246
|
+
let githubToken;
|
|
247
|
+
if (existingGithubToken) {
|
|
248
|
+
const reuse = await confirm({
|
|
249
|
+
message: `Found existing GitHub token in ${CREDENTIALS_DIR}/github-token. Use it?`,
|
|
250
|
+
default: true,
|
|
251
|
+
});
|
|
252
|
+
if (reuse) {
|
|
253
|
+
githubToken = existingGithubToken;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
githubToken = (await input({
|
|
257
|
+
message: "GitHub Personal Access Token (needs repo, workflow scopes):",
|
|
258
|
+
validate: (v) => (v.trim().length > 0 ? true : "Token is required"),
|
|
259
|
+
})).trim();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
githubToken = (await input({
|
|
264
|
+
message: "GitHub Personal Access Token (needs repo, workflow scopes):",
|
|
265
|
+
validate: (v) => (v.trim().length > 0 ? true : "Token is required"),
|
|
266
|
+
})).trim();
|
|
267
|
+
}
|
|
268
|
+
console.log("Validating GitHub token...");
|
|
269
|
+
let githubUser;
|
|
270
|
+
let availableRepos;
|
|
271
|
+
try {
|
|
272
|
+
const result = await validateGitHubToken(githubToken);
|
|
273
|
+
githubUser = result.user;
|
|
274
|
+
availableRepos = result.repos;
|
|
275
|
+
console.log(`Authenticated as: ${githubUser} (${availableRepos.length} repos found)\n`);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
throw new Error(`GitHub validation failed: ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
// SSH key
|
|
281
|
+
console.log("--- Git SSH Key ---\n");
|
|
282
|
+
const existingSshKey = existsSync(resolve(CREDENTIALS_DIR, "id_rsa"));
|
|
283
|
+
let sshKey;
|
|
284
|
+
if (existingSshKey) {
|
|
285
|
+
const reuse = await confirm({
|
|
286
|
+
message: `Found existing SSH key in ${CREDENTIALS_DIR}/id_rsa. Use it?`,
|
|
287
|
+
default: true,
|
|
288
|
+
});
|
|
289
|
+
if (!reuse) {
|
|
290
|
+
sshKey = await promptSshKey();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
sshKey = await promptSshKey();
|
|
295
|
+
}
|
|
296
|
+
// Sentry token (only if any selected definition has it as optional)
|
|
297
|
+
let sentryToken;
|
|
298
|
+
let sentryOrg;
|
|
299
|
+
let sentryProjectSlugs = [];
|
|
300
|
+
if (allOptionalCredentials.has("sentry-token")) {
|
|
301
|
+
console.log("\n--- Sentry ---\n");
|
|
302
|
+
const existingSentryToken = loadCredential("sentry-token");
|
|
303
|
+
if (existingSentryToken) {
|
|
304
|
+
const reuse = await confirm({
|
|
305
|
+
message: `Found existing Sentry token in ${CREDENTIALS_DIR}/sentry-token. Use it?`,
|
|
306
|
+
default: true,
|
|
307
|
+
});
|
|
308
|
+
if (reuse) {
|
|
309
|
+
sentryToken = existingSentryToken;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!sentryToken) {
|
|
313
|
+
const useSentry = await confirm({
|
|
314
|
+
message: "Configure Sentry integration?",
|
|
315
|
+
default: false,
|
|
316
|
+
});
|
|
317
|
+
if (useSentry) {
|
|
318
|
+
sentryToken = (await input({
|
|
319
|
+
message: "Sentry auth token:",
|
|
320
|
+
validate: (v) => (v.trim().length > 0 ? true : "Token is required"),
|
|
321
|
+
})).trim();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (sentryToken) {
|
|
325
|
+
console.log("Validating Sentry token...");
|
|
326
|
+
try {
|
|
327
|
+
const { organizations } = await validateSentryToken(sentryToken);
|
|
328
|
+
if (organizations.length === 0) {
|
|
329
|
+
throw new Error("No organizations found");
|
|
330
|
+
}
|
|
331
|
+
if (organizations.length === 1) {
|
|
332
|
+
sentryOrg = organizations[0].slug;
|
|
333
|
+
console.log(`Organization: ${sentryOrg}\n`);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
sentryOrg = await select({
|
|
337
|
+
message: "Select Sentry organization:",
|
|
338
|
+
choices: organizations.map((o) => ({ name: `${o.name} (${o.slug})`, value: o.slug })),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const { projects } = await validateSentryProjects(sentryToken, sentryOrg);
|
|
342
|
+
if (projects.length > 0) {
|
|
343
|
+
sentryProjectSlugs = await checkbox({
|
|
344
|
+
message: "Select Sentry projects to monitor:",
|
|
345
|
+
choices: projects.map((p) => ({ name: p.name, value: p.slug })),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
console.log(`Sentry validation failed: ${err.message}. Skipping Sentry.\n`);
|
|
351
|
+
sentryToken = undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Anthropic auth
|
|
356
|
+
console.log("\n--- Anthropic Auth ---\n");
|
|
357
|
+
const existingAnthropicKey = loadCredential("anthropic-key");
|
|
358
|
+
let authType;
|
|
359
|
+
let anthropicKey;
|
|
360
|
+
if (existingAnthropicKey) {
|
|
361
|
+
const reuse = await confirm({
|
|
362
|
+
message: `Found existing Anthropic credential in ${CREDENTIALS_DIR}/anthropic-key. Use it?`,
|
|
363
|
+
default: true,
|
|
364
|
+
});
|
|
365
|
+
if (reuse) {
|
|
366
|
+
anthropicKey = existingAnthropicKey;
|
|
367
|
+
authType = anthropicKey.includes("sk-ant-oat") ? "oauth_token" : "api_key";
|
|
368
|
+
console.log(`Using existing credential (detected type: ${authType}).\n`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
({ authType, anthropicKey } = await promptAnthropicAuth());
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
({ authType, anthropicKey } = await promptAnthropicAuth());
|
|
376
|
+
}
|
|
377
|
+
// Step 3: LLM defaults
|
|
378
|
+
console.log("\n--- Step 3: LLM Defaults ---\n");
|
|
379
|
+
const modelName = await select({
|
|
380
|
+
message: "Select model:",
|
|
381
|
+
choices: [
|
|
382
|
+
{ name: "claude-sonnet-4-20250514 (recommended)", value: "claude-sonnet-4-20250514" },
|
|
383
|
+
{ name: "claude-opus-4-20250514", value: "claude-opus-4-20250514" },
|
|
384
|
+
{ name: "claude-haiku-3-5-20241022", value: "claude-haiku-3-5-20241022" },
|
|
385
|
+
],
|
|
386
|
+
default: "claude-sonnet-4-20250514",
|
|
387
|
+
});
|
|
388
|
+
const thinkingLevel = await select({
|
|
389
|
+
message: "Thinking level:",
|
|
390
|
+
choices: [
|
|
391
|
+
{ name: "off", value: "off" },
|
|
392
|
+
{ name: "minimal", value: "minimal" },
|
|
393
|
+
{ name: "low", value: "low" },
|
|
394
|
+
{ name: "medium (recommended)", value: "medium" },
|
|
395
|
+
{ name: "high", value: "high" },
|
|
396
|
+
],
|
|
397
|
+
default: "medium",
|
|
398
|
+
});
|
|
399
|
+
const modelConfig = {
|
|
400
|
+
provider: "anthropic",
|
|
401
|
+
model: modelName,
|
|
402
|
+
thinkingLevel,
|
|
403
|
+
authType,
|
|
404
|
+
};
|
|
405
|
+
// Step 4: Configure each agent
|
|
406
|
+
console.log("\n--- Step 4: Configure Agents ---\n");
|
|
407
|
+
const agents = [];
|
|
408
|
+
let anyWebhooks = false;
|
|
409
|
+
let firstGithubWebhookSecret;
|
|
410
|
+
for (const def of selectedDefs) {
|
|
411
|
+
console.log(`\n --- Configure ${def.name} agent ---\n`);
|
|
412
|
+
// For init flow, we pre-populate sentry params from the top-level credential gathering
|
|
413
|
+
// so we skip the per-agent sentry prompt.
|
|
414
|
+
// We accomplish this by building params for credential-linked params here.
|
|
415
|
+
const prePopulatedParams = {};
|
|
416
|
+
for (const [key, paramDef] of Object.entries(def.params)) {
|
|
417
|
+
if (paramDef.credential === "sentry-token") {
|
|
418
|
+
if (key === "sentryOrg" && sentryOrg) {
|
|
419
|
+
prePopulatedParams[key] = sentryOrg;
|
|
420
|
+
}
|
|
421
|
+
else if (key === "sentryProjects" && sentryProjectSlugs.length > 0) {
|
|
422
|
+
prePopulatedParams[key] = sentryProjectSlugs;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Use configureAgentInit for init flow (skips credential prompts done globally)
|
|
427
|
+
const result = await configureAgentInit(def, {
|
|
428
|
+
availableRepos,
|
|
429
|
+
githubUser,
|
|
430
|
+
modelConfig,
|
|
431
|
+
}, prePopulatedParams, sentryToken ? true : false);
|
|
432
|
+
if (result.usesWebhooks) {
|
|
433
|
+
anyWebhooks = true;
|
|
434
|
+
if (!firstGithubWebhookSecret) {
|
|
435
|
+
firstGithubWebhookSecret = result.secrets.githubWebhookSecret;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
agents.push(result.agent);
|
|
439
|
+
}
|
|
440
|
+
// GitHub webhook secret (if any agent uses webhooks and not already prompted)
|
|
441
|
+
let githubWebhookSecret = firstGithubWebhookSecret;
|
|
442
|
+
if (anyWebhooks && !githubWebhookSecret) {
|
|
443
|
+
console.log("\n--- GitHub Webhook Secret ---\n");
|
|
444
|
+
const existingSecret = loadCredential("github-webhook-secret");
|
|
445
|
+
if (existingSecret) {
|
|
446
|
+
const reuse = await confirm({
|
|
447
|
+
message: `Found existing webhook secret in ${CREDENTIALS_DIR}/github-webhook-secret. Use it?`,
|
|
448
|
+
default: true,
|
|
449
|
+
});
|
|
450
|
+
if (reuse) {
|
|
451
|
+
githubWebhookSecret = existingSecret;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (!githubWebhookSecret) {
|
|
455
|
+
githubWebhookSecret = (await input({
|
|
456
|
+
message: "GitHub webhook secret (set this same value in your GitHub webhook settings):",
|
|
457
|
+
validate: (v) => (v.trim().length > 0 ? true : "Secret is required to verify webhook payloads"),
|
|
458
|
+
})).trim();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Build global config
|
|
462
|
+
const globalConfig = {};
|
|
463
|
+
if (githubWebhookSecret) {
|
|
464
|
+
globalConfig.webhooks = { githubSecretCredential: "github-webhook-secret" };
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
globalConfig,
|
|
468
|
+
agents,
|
|
469
|
+
secrets: {
|
|
470
|
+
githubToken,
|
|
471
|
+
sentryToken,
|
|
472
|
+
anthropicKey,
|
|
473
|
+
sshKey,
|
|
474
|
+
githubWebhookSecret,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Init-flow variant: configures an agent from a definition, but skips optional
|
|
480
|
+
* credential prompts (those were handled globally in runSetup).
|
|
481
|
+
*/
|
|
482
|
+
async function configureAgentInit(definition, context, prePopulatedParams, hasSentryToken) {
|
|
483
|
+
const name = await input({
|
|
484
|
+
message: `Agent name:`,
|
|
485
|
+
default: definition.name,
|
|
486
|
+
});
|
|
487
|
+
const repoChoices = context.availableRepos.map((r) => ({
|
|
488
|
+
name: r.fullName,
|
|
489
|
+
value: r.fullName,
|
|
490
|
+
}));
|
|
491
|
+
const repos = await checkbox({
|
|
492
|
+
message: `Repos for ${name}:`,
|
|
493
|
+
choices: repoChoices,
|
|
494
|
+
validate: (v) => (v.length > 0 ? true : "Select at least one repo"),
|
|
495
|
+
});
|
|
496
|
+
// Credentials from definition
|
|
497
|
+
const credentials = [...definition.credentials.required];
|
|
498
|
+
if (hasSentryToken && definition.credentials.optional.includes("sentry-token")) {
|
|
499
|
+
credentials.push("sentry-token");
|
|
500
|
+
}
|
|
501
|
+
// Params — prompt for non-credential params, use pre-populated for credential params
|
|
502
|
+
const params = { ...prePopulatedParams };
|
|
503
|
+
for (const [key, paramDef] of Object.entries(definition.params)) {
|
|
504
|
+
if (paramDef.credential)
|
|
505
|
+
continue; // Already handled
|
|
506
|
+
let defaultValue = paramDef.default;
|
|
507
|
+
if (defaultValue === "$githubUser") {
|
|
508
|
+
defaultValue = context.githubUser;
|
|
509
|
+
}
|
|
510
|
+
if (paramDef.type === "string") {
|
|
511
|
+
const value = await input({
|
|
512
|
+
message: `${paramDef.description}:`,
|
|
513
|
+
default: defaultValue,
|
|
514
|
+
...(paramDef.required ? { validate: (v) => v.trim().length > 0 ? true : `${key} is required` } : {}),
|
|
515
|
+
});
|
|
516
|
+
if (value.trim()) {
|
|
517
|
+
params[key] = value.trim();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else if (paramDef.type === "string[]") {
|
|
521
|
+
const value = await input({
|
|
522
|
+
message: `${paramDef.description} (comma-separated):`,
|
|
523
|
+
default: defaultValue,
|
|
524
|
+
});
|
|
525
|
+
if (value.trim()) {
|
|
526
|
+
params[key] = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Webhook trigger
|
|
531
|
+
const useWebhooks = await confirm({
|
|
532
|
+
message: `Listen for webhooks? (${definition.webhooks.description})`,
|
|
533
|
+
default: true,
|
|
534
|
+
});
|
|
535
|
+
let webhooks;
|
|
536
|
+
if (useWebhooks) {
|
|
537
|
+
const filter = buildWebhookFilter(definition, repos, params);
|
|
538
|
+
webhooks = { filters: [filter] };
|
|
539
|
+
}
|
|
540
|
+
// Schedule trigger
|
|
541
|
+
const useSchedule = await confirm({
|
|
542
|
+
message: `Also run on a schedule (polling)?`,
|
|
543
|
+
default: !useWebhooks,
|
|
544
|
+
});
|
|
545
|
+
let schedule;
|
|
546
|
+
if (useSchedule) {
|
|
547
|
+
schedule = await input({
|
|
548
|
+
message: `${name} poll interval (cron):`,
|
|
549
|
+
default: definition.defaultSchedule,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const prompt = useWebhooks ? definition.prompts.webhook : definition.prompts.schedule;
|
|
553
|
+
// Build agent config
|
|
554
|
+
const config = {
|
|
555
|
+
name,
|
|
556
|
+
credentials,
|
|
557
|
+
model: context.modelConfig,
|
|
558
|
+
prompt,
|
|
559
|
+
repos,
|
|
560
|
+
...(schedule ? { schedule } : {}),
|
|
561
|
+
...(webhooks ? { webhooks } : {}),
|
|
562
|
+
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
563
|
+
};
|
|
564
|
+
return {
|
|
565
|
+
agent: { name, template: definition.name, config },
|
|
566
|
+
secrets: {},
|
|
567
|
+
usesWebhooks: useWebhooks,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
// --- Add agent to existing project ---
|
|
571
|
+
export async function runAddAgent(opts) {
|
|
572
|
+
console.log("\n=== Action Llama — Add Agent ===\n");
|
|
573
|
+
const result = await configureAgent(opts.definition, {
|
|
574
|
+
availableRepos: opts.availableRepos,
|
|
575
|
+
githubUser: opts.githubUser,
|
|
576
|
+
modelConfig: opts.modelConfig,
|
|
577
|
+
existingAgentNames: opts.existingAgentNames,
|
|
578
|
+
});
|
|
579
|
+
return {
|
|
580
|
+
agent: result.agent,
|
|
581
|
+
secrets: result.secrets,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
// --- SSH key prompt ---
|
|
585
|
+
async function promptSshKey() {
|
|
586
|
+
const defaultPath = resolve(process.env.HOME || "~", ".ssh", "id_rsa");
|
|
587
|
+
const keyPath = await input({
|
|
588
|
+
message: `Path to SSH private key for git operations (leave empty to use system default):`,
|
|
589
|
+
default: existsSync(defaultPath) ? defaultPath : "",
|
|
590
|
+
});
|
|
591
|
+
if (!keyPath.trim()) {
|
|
592
|
+
console.log("No SSH key configured — git will use your system SSH config.\n");
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
const resolvedPath = resolve(keyPath.trim());
|
|
596
|
+
if (!existsSync(resolvedPath)) {
|
|
597
|
+
throw new Error(`SSH key not found at ${resolvedPath}`);
|
|
598
|
+
}
|
|
599
|
+
const content = readFileSync(resolvedPath, "utf-8");
|
|
600
|
+
console.log("SSH key loaded.\n");
|
|
601
|
+
return content;
|
|
602
|
+
}
|
|
603
|
+
// --- Anthropic auth prompt ---
|
|
604
|
+
async function promptAnthropicAuth() {
|
|
605
|
+
const authMethod = await select({
|
|
606
|
+
message: "How do you want to authenticate with Anthropic?",
|
|
607
|
+
choices: [
|
|
608
|
+
{ name: "Use existing pi auth (already ran `pi /login` or `claude setup-token`)", value: "pi_auth" },
|
|
609
|
+
{ name: "Enter an API key (sk-ant-api...)", value: "api_key" },
|
|
610
|
+
{ name: "Enter an OAuth token (sk-ant-oat...)", value: "oauth_token" },
|
|
611
|
+
],
|
|
612
|
+
});
|
|
613
|
+
if (authMethod === "pi_auth") {
|
|
614
|
+
const { AuthStorage, ModelRegistry } = await import("@mariozechner/pi-coding-agent");
|
|
615
|
+
const authStorage = AuthStorage.create();
|
|
616
|
+
const registry = new ModelRegistry(authStorage);
|
|
617
|
+
const available = await registry.getAvailable();
|
|
618
|
+
const hasAnthropic = available.some((m) => m.provider === "anthropic");
|
|
619
|
+
if (!hasAnthropic) {
|
|
620
|
+
throw new Error("No Anthropic credentials found in pi auth storage (~/.pi/agent/auth.json). " +
|
|
621
|
+
"Run `pi /login` first, or choose a different auth method.");
|
|
622
|
+
}
|
|
623
|
+
console.log("Found existing Anthropic credentials in pi auth storage.\n");
|
|
624
|
+
return { authType: "pi_auth", anthropicKey: undefined };
|
|
625
|
+
}
|
|
626
|
+
else if (authMethod === "api_key") {
|
|
627
|
+
let anthropicKey = (await input({
|
|
628
|
+
message: "Anthropic API key:",
|
|
629
|
+
validate: (v) => (v.trim().length > 0 ? true : "Key is required"),
|
|
630
|
+
})).trim();
|
|
631
|
+
console.log("Validating API key...");
|
|
632
|
+
try {
|
|
633
|
+
await validateAnthropicApiKey(anthropicKey);
|
|
634
|
+
console.log("API key validated.\n");
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
throw new Error(`Anthropic validation failed: ${err.message}`);
|
|
638
|
+
}
|
|
639
|
+
return { authType: "api_key", anthropicKey };
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
let anthropicKey = (await input({
|
|
643
|
+
message: "Anthropic OAuth token (from `claude setup-token`):",
|
|
644
|
+
validate: (v) => (v.trim().length > 0 ? true : "Token is required"),
|
|
645
|
+
})).trim();
|
|
646
|
+
try {
|
|
647
|
+
validateOAuthTokenFormat(anthropicKey);
|
|
648
|
+
console.log("OAuth token format looks valid. It will be verified on first agent run.\n");
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
throw new Error(err.message);
|
|
652
|
+
}
|
|
653
|
+
return { authType: "oauth_token", anthropicKey };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
//# sourceMappingURL=prompts.js.map
|