@made-by-moonlight/athene-cli 0.9.2
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/LICENSE +22 -0
- package/dist/assets/plugin-registry.json +67 -0
- package/dist/assets/scripts/athene-doctor.ps1 +352 -0
- package/dist/assets/scripts/athene-doctor.sh +552 -0
- package/dist/assets/scripts/athene-update.ps1 +224 -0
- package/dist/assets/scripts/athene-update.sh +252 -0
- package/dist/commands/completion.d.ts +3 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +26 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +89 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +103 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +329 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.d.ts +3 -0
- package/dist/commands/events.d.ts.map +1 -0
- package/dist/commands/events.js +172 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/migrate-storage.d.ts +3 -0
- package/dist/commands/migrate-storage.d.ts.map +1 -0
- package/dist/commands/migrate-storage.js +78 -0
- package/dist/commands/migrate-storage.js.map +1 -0
- package/dist/commands/notify.d.ts +3 -0
- package/dist/commands/notify.d.ts.map +1 -0
- package/dist/commands/notify.js +143 -0
- package/dist/commands/notify.js.map +1 -0
- package/dist/commands/open.d.ts +3 -0
- package/dist/commands/open.d.ts.map +1 -0
- package/dist/commands/open.js +167 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/plugin.d.ts +3 -0
- package/dist/commands/plugin.d.ts.map +1 -0
- package/dist/commands/plugin.js +462 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +143 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/report.d.ts +19 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +114 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/review-check.d.ts +3 -0
- package/dist/commands/review-check.d.ts.map +1 -0
- package/dist/commands/review-check.js +122 -0
- package/dist/commands/review-check.js.map +1 -0
- package/dist/commands/review.d.ts +3 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +215 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/send.d.ts +3 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +187 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +439 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +297 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/spawn.d.ts +4 -0
- package/dist/commands/spawn.d.ts.map +1 -0
- package/dist/commands/spawn.js +436 -0
- package/dist/commands/spawn.js.map +1 -0
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +1836 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +556 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +15 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +652 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +131 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/bun-tmp-janitor.d.ts +18 -0
- package/dist/lib/bun-tmp-janitor.d.ts.map +1 -0
- package/dist/lib/bun-tmp-janitor.js +127 -0
- package/dist/lib/bun-tmp-janitor.js.map +1 -0
- package/dist/lib/caller-context.d.ts +13 -0
- package/dist/lib/caller-context.d.ts.map +1 -0
- package/dist/lib/caller-context.js +20 -0
- package/dist/lib/caller-context.js.map +1 -0
- package/dist/lib/cli-errors.d.ts +8 -0
- package/dist/lib/cli-errors.d.ts.map +1 -0
- package/dist/lib/cli-errors.js +20 -0
- package/dist/lib/cli-errors.js.map +1 -0
- package/dist/lib/completion.d.ts +13 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +428 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/composio-setup.d.ts +65 -0
- package/dist/lib/composio-setup.d.ts.map +1 -0
- package/dist/lib/composio-setup.js +3255 -0
- package/dist/lib/composio-setup.js.map +1 -0
- package/dist/lib/config-instruction.d.ts +2 -0
- package/dist/lib/config-instruction.d.ts.map +1 -0
- package/dist/lib/config-instruction.js +193 -0
- package/dist/lib/config-instruction.js.map +1 -0
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +3 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/create-session-manager.d.ts +26 -0
- package/dist/lib/create-session-manager.d.ts.map +1 -0
- package/dist/lib/create-session-manager.js +55 -0
- package/dist/lib/create-session-manager.js.map +1 -0
- package/dist/lib/credential-resolver.d.ts +37 -0
- package/dist/lib/credential-resolver.d.ts.map +1 -0
- package/dist/lib/credential-resolver.js +105 -0
- package/dist/lib/credential-resolver.js.map +1 -0
- package/dist/lib/daemon.d.ts +69 -0
- package/dist/lib/daemon.d.ts.map +1 -0
- package/dist/lib/daemon.js +77 -0
- package/dist/lib/daemon.js.map +1 -0
- package/dist/lib/dashboard-rebuild.d.ts +53 -0
- package/dist/lib/dashboard-rebuild.d.ts.map +1 -0
- package/dist/lib/dashboard-rebuild.js +188 -0
- package/dist/lib/dashboard-rebuild.js.map +1 -0
- package/dist/lib/dashboard-setup.d.ts +14 -0
- package/dist/lib/dashboard-setup.d.ts.map +1 -0
- package/dist/lib/dashboard-setup.js +192 -0
- package/dist/lib/dashboard-setup.js.map +1 -0
- package/dist/lib/dashboard-url.d.ts +19 -0
- package/dist/lib/dashboard-url.d.ts.map +1 -0
- package/dist/lib/dashboard-url.js +25 -0
- package/dist/lib/dashboard-url.js.map +1 -0
- package/dist/lib/desktop-setup.d.ts +21 -0
- package/dist/lib/desktop-setup.d.ts.map +1 -0
- package/dist/lib/desktop-setup.js +556 -0
- package/dist/lib/desktop-setup.js.map +1 -0
- package/dist/lib/detect-agent.d.ts +24 -0
- package/dist/lib/detect-agent.d.ts.map +1 -0
- package/dist/lib/detect-agent.js +69 -0
- package/dist/lib/detect-agent.js.map +1 -0
- package/dist/lib/detect-env.d.ts +14 -0
- package/dist/lib/detect-env.d.ts.map +1 -0
- package/dist/lib/detect-env.js +46 -0
- package/dist/lib/detect-env.js.map +1 -0
- package/dist/lib/discord-setup.d.ts +20 -0
- package/dist/lib/discord-setup.d.ts.map +1 -0
- package/dist/lib/discord-setup.js +584 -0
- package/dist/lib/discord-setup.js.map +1 -0
- package/dist/lib/format.d.ts +11 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +116 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/git-utils.d.ts +14 -0
- package/dist/lib/git-utils.d.ts.map +1 -0
- package/dist/lib/git-utils.js +45 -0
- package/dist/lib/git-utils.js.map +1 -0
- package/dist/lib/install-helpers.d.ts +24 -0
- package/dist/lib/install-helpers.d.ts.map +1 -0
- package/dist/lib/install-helpers.js +76 -0
- package/dist/lib/install-helpers.js.map +1 -0
- package/dist/lib/lifecycle-service.d.ts +11 -0
- package/dist/lib/lifecycle-service.d.ts.map +1 -0
- package/dist/lib/lifecycle-service.js +65 -0
- package/dist/lib/lifecycle-service.js.map +1 -0
- package/dist/lib/notifier-routing.d.ts +35 -0
- package/dist/lib/notifier-routing.d.ts.map +1 -0
- package/dist/lib/notifier-routing.js +133 -0
- package/dist/lib/notifier-routing.js.map +1 -0
- package/dist/lib/notify-test.d.ts +72 -0
- package/dist/lib/notify-test.d.ts.map +1 -0
- package/dist/lib/notify-test.js +674 -0
- package/dist/lib/notify-test.js.map +1 -0
- package/dist/lib/openclaw-probe.d.ts +38 -0
- package/dist/lib/openclaw-probe.d.ts.map +1 -0
- package/dist/lib/openclaw-probe.js +146 -0
- package/dist/lib/openclaw-probe.js.map +1 -0
- package/dist/lib/openclaw-setup.d.ts +19 -0
- package/dist/lib/openclaw-setup.d.ts.map +1 -0
- package/dist/lib/openclaw-setup.js +684 -0
- package/dist/lib/openclaw-setup.js.map +1 -0
- package/dist/lib/path-equality.d.ts +29 -0
- package/dist/lib/path-equality.d.ts.map +1 -0
- package/dist/lib/path-equality.js +52 -0
- package/dist/lib/path-equality.js.map +1 -0
- package/dist/lib/plugin-marketplace.d.ts +24 -0
- package/dist/lib/plugin-marketplace.d.ts.map +1 -0
- package/dist/lib/plugin-marketplace.js +175 -0
- package/dist/lib/plugin-marketplace.js.map +1 -0
- package/dist/lib/plugin-scaffold.d.ts +14 -0
- package/dist/lib/plugin-scaffold.d.ts.map +1 -0
- package/dist/lib/plugin-scaffold.js +174 -0
- package/dist/lib/plugin-scaffold.js.map +1 -0
- package/dist/lib/plugin-store.d.ts +9 -0
- package/dist/lib/plugin-store.d.ts.map +1 -0
- package/dist/lib/plugin-store.js +121 -0
- package/dist/lib/plugin-store.js.map +1 -0
- package/dist/lib/plugins.d.ts +17 -0
- package/dist/lib/plugins.d.ts.map +1 -0
- package/dist/lib/plugins.js +65 -0
- package/dist/lib/plugins.js.map +1 -0
- package/dist/lib/portfolio-display.d.ts +10 -0
- package/dist/lib/portfolio-display.d.ts.map +1 -0
- package/dist/lib/portfolio-display.js +17 -0
- package/dist/lib/portfolio-display.js.map +1 -0
- package/dist/lib/preflight.d.ts +27 -0
- package/dist/lib/preflight.d.ts.map +1 -0
- package/dist/lib/preflight.js +77 -0
- package/dist/lib/preflight.js.map +1 -0
- package/dist/lib/prevent-sleep.d.ts +34 -0
- package/dist/lib/prevent-sleep.d.ts.map +1 -0
- package/dist/lib/prevent-sleep.js +65 -0
- package/dist/lib/prevent-sleep.js.map +1 -0
- package/dist/lib/project-detection.d.ts +11 -0
- package/dist/lib/project-detection.d.ts.map +1 -0
- package/dist/lib/project-detection.js +206 -0
- package/dist/lib/project-detection.js.map +1 -0
- package/dist/lib/project-resolution.d.ts +10 -0
- package/dist/lib/project-resolution.d.ts.map +1 -0
- package/dist/lib/project-resolution.js +17 -0
- package/dist/lib/project-resolution.js.map +1 -0
- package/dist/lib/project-supervisor.d.ts +28 -0
- package/dist/lib/project-supervisor.d.ts.map +1 -0
- package/dist/lib/project-supervisor.js +167 -0
- package/dist/lib/project-supervisor.js.map +1 -0
- package/dist/lib/prompts.d.ts +7 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +37 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/repo-utils.d.ts +16 -0
- package/dist/lib/repo-utils.d.ts.map +1 -0
- package/dist/lib/repo-utils.js +26 -0
- package/dist/lib/repo-utils.js.map +1 -0
- package/dist/lib/resolve-project.d.ts +113 -0
- package/dist/lib/resolve-project.d.ts.map +1 -0
- package/dist/lib/resolve-project.js +433 -0
- package/dist/lib/resolve-project.js.map +1 -0
- package/dist/lib/routes.d.ts +2 -0
- package/dist/lib/routes.d.ts.map +1 -0
- package/dist/lib/routes.js +5 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/running-state.d.ts +76 -0
- package/dist/lib/running-state.d.ts.map +1 -0
- package/dist/lib/running-state.js +338 -0
- package/dist/lib/running-state.js.map +1 -0
- package/dist/lib/script-runner.d.ts +10 -0
- package/dist/lib/script-runner.d.ts.map +1 -0
- package/dist/lib/script-runner.js +189 -0
- package/dist/lib/script-runner.js.map +1 -0
- package/dist/lib/session-utils.d.ts +14 -0
- package/dist/lib/session-utils.d.ts.map +1 -0
- package/dist/lib/session-utils.js +58 -0
- package/dist/lib/session-utils.js.map +1 -0
- package/dist/lib/shell.d.ts +17 -0
- package/dist/lib/shell.d.ts.map +1 -0
- package/dist/lib/shell.js +90 -0
- package/dist/lib/shell.js.map +1 -0
- package/dist/lib/shutdown.d.ts +30 -0
- package/dist/lib/shutdown.d.ts.map +1 -0
- package/dist/lib/shutdown.js +177 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/slack-setup.d.ts +17 -0
- package/dist/lib/slack-setup.d.ts.map +1 -0
- package/dist/lib/slack-setup.js +485 -0
- package/dist/lib/slack-setup.js.map +1 -0
- package/dist/lib/startup-preflight.d.ts +36 -0
- package/dist/lib/startup-preflight.d.ts.map +1 -0
- package/dist/lib/startup-preflight.js +273 -0
- package/dist/lib/startup-preflight.js.map +1 -0
- package/dist/lib/update-channel-onboarding.d.ts +52 -0
- package/dist/lib/update-channel-onboarding.d.ts.map +1 -0
- package/dist/lib/update-channel-onboarding.js +107 -0
- package/dist/lib/update-channel-onboarding.js.map +1 -0
- package/dist/lib/update-check.d.ts +161 -0
- package/dist/lib/update-check.d.ts.map +1 -0
- package/dist/lib/update-check.js +504 -0
- package/dist/lib/update-check.js.map +1 -0
- package/dist/lib/web-dir.d.ts +47 -0
- package/dist/lib/web-dir.d.ts.map +1 -0
- package/dist/lib/web-dir.js +179 -0
- package/dist/lib/web-dir.js.map +1 -0
- package/dist/lib/webhook-setup.d.ts +16 -0
- package/dist/lib/webhook-setup.d.ts.map +1 -0
- package/dist/lib/webhook-setup.js +383 -0
- package/dist/lib/webhook-setup.js.map +1 -0
- package/dist/options/version.d.ts +2 -0
- package/dist/options/version.d.ts.map +1 -0
- package/dist/options/version.js +7 -0
- package/dist/options/version.js.map +1 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +63 -0
- package/dist/program.js.map +1 -0
- package/package.json +80 -0
- package/templates/rules/base.md +4 -0
- package/templates/rules/go.md +8 -0
- package/templates/rules/javascript.md +4 -0
- package/templates/rules/nextjs.md +7 -0
- package/templates/rules/pnpm-workspaces.md +4 -0
- package/templates/rules/python.md +8 -0
- package/templates/rules/react.md +8 -0
- package/templates/rules/typescript.md +6 -0
|
@@ -0,0 +1,3255 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { parseDocument } from "yaml";
|
|
4
|
+
import { CONFIG_SCHEMA_URL, findConfigFile, isCanonicalGlobalConfigPath } from "@made-by-moonlight/athene-core";
|
|
5
|
+
import { applyNotifierRoutingPreset, ensureNotifierDefault, getNotifierRoutingState, promptNotifierRoutingPreset, resolveRoutingPresetOption, routingLabel, } from "./notifier-routing.js";
|
|
6
|
+
const SLACK_TOOLKIT = "slack";
|
|
7
|
+
const DISCORD_TOOLKIT = "discordbot";
|
|
8
|
+
const GMAIL_TOOLKIT = "gmail";
|
|
9
|
+
const DISCORD_TOOL_VERSION = "20260429_01";
|
|
10
|
+
const GMAIL_TOOL_VERSION = "20260506_01";
|
|
11
|
+
const COMPOSIO_NOTIFIER = "composio";
|
|
12
|
+
const COMPOSIO_SLACK_NOTIFIER = "composio-slack";
|
|
13
|
+
const COMPOSIO_DISCORD_WEBHOOK_NOTIFIER = "composio-discord";
|
|
14
|
+
const COMPOSIO_DISCORD_BOT_NOTIFIER = "composio-discord-bot";
|
|
15
|
+
const COMPOSIO_MAIL_NOTIFIER = "composio-mail";
|
|
16
|
+
const DEFAULT_COMPOSIO_USER_ID = "aoagent";
|
|
17
|
+
const GMAIL_SEND_TOOL = "GMAIL_SEND_EMAIL";
|
|
18
|
+
const COMPOSIO_DASHBOARD_URL = "https://app.composio.dev";
|
|
19
|
+
const DISCORD_APP_URL = "https://discord.com/app";
|
|
20
|
+
const DISCORD_DEVELOPER_PORTAL_URL = "https://discord.com/developers/applications";
|
|
21
|
+
const DISCORD_WEBHOOK_DOCS_URL = "https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks";
|
|
22
|
+
export class ComposioSetupError extends Error {
|
|
23
|
+
exitCode;
|
|
24
|
+
constructor(message, exitCode = 1) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.exitCode = exitCode;
|
|
27
|
+
this.name = "ComposioSetupError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isRecord(value) {
|
|
31
|
+
return value !== null && typeof value === "object";
|
|
32
|
+
}
|
|
33
|
+
function stringValue(value) {
|
|
34
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
35
|
+
}
|
|
36
|
+
function asStringArray(value) {
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.filter((entry) => typeof entry === "string");
|
|
39
|
+
if (typeof value === "string")
|
|
40
|
+
return [value];
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
function resolveComposioRoutingPreset(value) {
|
|
44
|
+
try {
|
|
45
|
+
return resolveRoutingPresetOption(value, "Composio");
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new ComposioSetupError(error instanceof Error ? error.message : String(error));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function routingReviewLabel(preset) {
|
|
52
|
+
return preset ? routingLabel(preset) : "unchanged";
|
|
53
|
+
}
|
|
54
|
+
function scopeArray(value) {
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value.filter((entry) => typeof entry === "string");
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return value
|
|
60
|
+
.split(/\s+/)
|
|
61
|
+
.map((entry) => entry.trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function getExistingComposioConfig(rawConfig) {
|
|
67
|
+
return getExistingNotifierConfig(rawConfig, COMPOSIO_NOTIFIER);
|
|
68
|
+
}
|
|
69
|
+
function getExistingNotifierConfig(rawConfig, notifierName) {
|
|
70
|
+
const notifiers = isRecord(rawConfig["notifiers"]) ? rawConfig["notifiers"] : {};
|
|
71
|
+
const existing = isRecord(notifiers[notifierName]) ? notifiers[notifierName] : {};
|
|
72
|
+
return existing;
|
|
73
|
+
}
|
|
74
|
+
function resolveApiKeyCandidate(opts, existing) {
|
|
75
|
+
const optionKey = stringValue(opts.apiKey);
|
|
76
|
+
if (optionKey) {
|
|
77
|
+
return {
|
|
78
|
+
apiKey: optionKey,
|
|
79
|
+
shouldWriteApiKey: true,
|
|
80
|
+
sourceLabel: "command option",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const envKey = stringValue(process.env.COMPOSIO_API_KEY);
|
|
84
|
+
if (envKey) {
|
|
85
|
+
return {
|
|
86
|
+
apiKey: envKey,
|
|
87
|
+
shouldWriteApiKey: false,
|
|
88
|
+
sourceLabel: "COMPOSIO_API_KEY",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const existingKey = stringValue(existing["composioApiKey"]);
|
|
92
|
+
if (existingKey && !existingKey.includes("${")) {
|
|
93
|
+
return {
|
|
94
|
+
apiKey: existingKey,
|
|
95
|
+
shouldWriteApiKey: true,
|
|
96
|
+
sourceLabel: "agent-orchestrator.yaml",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
function resolveApiKey(opts, existing) {
|
|
102
|
+
const candidate = resolveApiKeyCandidate(opts, existing);
|
|
103
|
+
return {
|
|
104
|
+
apiKey: candidate?.apiKey,
|
|
105
|
+
shouldWriteApiKey: candidate?.shouldWriteApiKey ?? false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function resolveUserId(opts, existing) {
|
|
109
|
+
return (stringValue(opts.userId) ??
|
|
110
|
+
stringValue(existing["userId"]) ??
|
|
111
|
+
stringValue(existing["entityId"]) ??
|
|
112
|
+
stringValue(process.env.COMPOSIO_USER_ID) ??
|
|
113
|
+
stringValue(process.env.COMPOSIO_ENTITY_ID) ??
|
|
114
|
+
DEFAULT_COMPOSIO_USER_ID);
|
|
115
|
+
}
|
|
116
|
+
function isComposioSetupClient(value) {
|
|
117
|
+
return (isRecord(value) &&
|
|
118
|
+
isRecord(value["connectedAccounts"]) &&
|
|
119
|
+
typeof value["connectedAccounts"]["list"] === "function");
|
|
120
|
+
}
|
|
121
|
+
async function loadComposioClient(apiKey) {
|
|
122
|
+
const mod = (await import("@composio/core"));
|
|
123
|
+
const ComposioClass = (mod.Composio ??
|
|
124
|
+
mod.default?.Composio ??
|
|
125
|
+
mod.default);
|
|
126
|
+
if (typeof ComposioClass !== "function") {
|
|
127
|
+
throw new ComposioSetupError("Could not find Composio class in @composio/core module.");
|
|
128
|
+
}
|
|
129
|
+
const client = new ComposioClass({ apiKey });
|
|
130
|
+
if (!isComposioSetupClient(client)) {
|
|
131
|
+
throw new ComposioSetupError("Composio SDK client does not expose connectedAccounts.list().");
|
|
132
|
+
}
|
|
133
|
+
return client;
|
|
134
|
+
}
|
|
135
|
+
function toConnectedAccount(value) {
|
|
136
|
+
if (!isRecord(value))
|
|
137
|
+
return null;
|
|
138
|
+
const id = stringValue(value["id"]);
|
|
139
|
+
if (!id)
|
|
140
|
+
return null;
|
|
141
|
+
const data = isRecord(value["data"]) ? value["data"] : {};
|
|
142
|
+
const params = isRecord(value["params"]) ? value["params"] : {};
|
|
143
|
+
const state = isRecord(value["state"]) ? value["state"] : {};
|
|
144
|
+
const stateVal = isRecord(state["val"]) ? state["val"] : {};
|
|
145
|
+
return {
|
|
146
|
+
id,
|
|
147
|
+
status: stringValue(value["status"]),
|
|
148
|
+
statusReason: stringValue(value["statusReason"]) ?? stringValue(value["status_reason"]) ?? null,
|
|
149
|
+
toolkit: isRecord(value["toolkit"])
|
|
150
|
+
? { slug: stringValue(value["toolkit"]["slug"]) }
|
|
151
|
+
: undefined,
|
|
152
|
+
authConfig: isRecord(value["authConfig"])
|
|
153
|
+
? {
|
|
154
|
+
id: stringValue(value["authConfig"]["id"]),
|
|
155
|
+
name: stringValue(value["authConfig"]["name"]),
|
|
156
|
+
}
|
|
157
|
+
: undefined,
|
|
158
|
+
alias: stringValue(value["alias"]) ?? null,
|
|
159
|
+
isDisabled: value["isDisabled"] === true || value["is_disabled"] === true,
|
|
160
|
+
scopes: [
|
|
161
|
+
...scopeArray(data["scope"]),
|
|
162
|
+
...scopeArray(data["scopes"]),
|
|
163
|
+
...scopeArray(params["scope"]),
|
|
164
|
+
...scopeArray(params["scopes"]),
|
|
165
|
+
...scopeArray(stateVal["scope"]),
|
|
166
|
+
...scopeArray(stateVal["scopes"]),
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function toAuthConfigSummary(value) {
|
|
171
|
+
if (!isRecord(value))
|
|
172
|
+
return null;
|
|
173
|
+
const id = stringValue(value["id"]);
|
|
174
|
+
if (!id)
|
|
175
|
+
return null;
|
|
176
|
+
const toolkit = isRecord(value["toolkit"]) ? value["toolkit"] : {};
|
|
177
|
+
const toolAccessConfig = isRecord(value["toolAccessConfig"])
|
|
178
|
+
? value["toolAccessConfig"]
|
|
179
|
+
: isRecord(value["tool_access_config"])
|
|
180
|
+
? value["tool_access_config"]
|
|
181
|
+
: {};
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
toolkit: isRecord(toolkit) ? { slug: stringValue(toolkit["slug"]) } : undefined,
|
|
185
|
+
toolAccessConfig: {
|
|
186
|
+
toolsAvailableForExecution: asStringArray(toolAccessConfig["toolsAvailableForExecution"] ??
|
|
187
|
+
toolAccessConfig["tools_available_for_execution"]),
|
|
188
|
+
toolsForConnectedAccountCreation: asStringArray(toolAccessConfig["toolsForConnectedAccountCreation"] ??
|
|
189
|
+
toolAccessConfig["tools_for_connected_account_creation"]),
|
|
190
|
+
},
|
|
191
|
+
restrictToFollowingTools: asStringArray(value["restrictToFollowingTools"] ?? value["restrict_to_following_tools"]),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function accountsFromListResult(result) {
|
|
195
|
+
if (Array.isArray(result))
|
|
196
|
+
return result.map(toConnectedAccount).filter((a) => a !== null);
|
|
197
|
+
if (isRecord(result) && Array.isArray(result["items"])) {
|
|
198
|
+
return result["items"].map(toConnectedAccount).filter((a) => a !== null);
|
|
199
|
+
}
|
|
200
|
+
if (isRecord(result) && Array.isArray(result["data"])) {
|
|
201
|
+
return result["data"].map(toConnectedAccount).filter((a) => a !== null);
|
|
202
|
+
}
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
function authConfigsFromListResult(result) {
|
|
206
|
+
if (Array.isArray(result))
|
|
207
|
+
return result.map(toAuthConfigSummary).filter((a) => a !== null);
|
|
208
|
+
if (isRecord(result) && Array.isArray(result["items"])) {
|
|
209
|
+
return result["items"]
|
|
210
|
+
.map(toAuthConfigSummary)
|
|
211
|
+
.filter((a) => a !== null);
|
|
212
|
+
}
|
|
213
|
+
if (isRecord(result) && Array.isArray(result["data"])) {
|
|
214
|
+
return result["data"]
|
|
215
|
+
.map(toAuthConfigSummary)
|
|
216
|
+
.filter((a) => a !== null);
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
function isActive(account) {
|
|
221
|
+
if (account.isDisabled)
|
|
222
|
+
return false;
|
|
223
|
+
return !account.status || account.status.toUpperCase() === "ACTIVE";
|
|
224
|
+
}
|
|
225
|
+
function isToolkit(account, toolkit) {
|
|
226
|
+
return !account.toolkit?.slug || account.toolkit.slug.toLowerCase() === toolkit;
|
|
227
|
+
}
|
|
228
|
+
function hasGmailNotifyScopes(account) {
|
|
229
|
+
const scopes = new Set(account.scopes ?? []);
|
|
230
|
+
if (scopes.has("https://mail.google.com/"))
|
|
231
|
+
return true;
|
|
232
|
+
const canSend = scopes.has("https://www.googleapis.com/auth/gmail.send");
|
|
233
|
+
const canReadProfile = scopes.has("https://www.googleapis.com/auth/gmail.metadata") ||
|
|
234
|
+
scopes.has("https://www.googleapis.com/auth/gmail.readonly") ||
|
|
235
|
+
scopes.has("https://www.googleapis.com/auth/gmail.modify");
|
|
236
|
+
return canSend && canReadProfile;
|
|
237
|
+
}
|
|
238
|
+
function authConfigAllowsGmailSend(config) {
|
|
239
|
+
const tools = [
|
|
240
|
+
...(config.toolAccessConfig?.toolsForConnectedAccountCreation ?? []),
|
|
241
|
+
...(config.toolAccessConfig?.toolsAvailableForExecution ?? []),
|
|
242
|
+
...(config.restrictToFollowingTools ?? []),
|
|
243
|
+
];
|
|
244
|
+
return tools.includes(GMAIL_SEND_TOOL);
|
|
245
|
+
}
|
|
246
|
+
async function withConnectedAccountDetails(client, account) {
|
|
247
|
+
if (!client.connectedAccounts.get)
|
|
248
|
+
return account;
|
|
249
|
+
const detailed = toConnectedAccount(await client.connectedAccounts.get(account.id));
|
|
250
|
+
return detailed ?? account;
|
|
251
|
+
}
|
|
252
|
+
async function listActiveSlackAccounts(client, userId) {
|
|
253
|
+
return listActiveToolkitAccounts(client, userId, SLACK_TOOLKIT);
|
|
254
|
+
}
|
|
255
|
+
async function listActiveGmailAccounts(client, userId) {
|
|
256
|
+
return listActiveToolkitAccounts(client, userId, GMAIL_TOOLKIT);
|
|
257
|
+
}
|
|
258
|
+
async function listUsableGmailAccounts(client, userId) {
|
|
259
|
+
const accounts = await listActiveGmailAccounts(client, userId);
|
|
260
|
+
const detailed = await Promise.all(accounts.map((account) => withConnectedAccountDetails(client, account)));
|
|
261
|
+
const usable = [];
|
|
262
|
+
for (const account of detailed) {
|
|
263
|
+
if (await accountCanSendGmail(client, account)) {
|
|
264
|
+
usable.push(account);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return usable;
|
|
268
|
+
}
|
|
269
|
+
async function listActiveToolkitAccounts(client, userId, toolkit) {
|
|
270
|
+
const result = await client.connectedAccounts.list({
|
|
271
|
+
userIds: [userId],
|
|
272
|
+
toolkitSlugs: [toolkit],
|
|
273
|
+
statuses: ["ACTIVE"],
|
|
274
|
+
limit: 25,
|
|
275
|
+
});
|
|
276
|
+
return accountsFromListResult(result).filter((account) => isActive(account) && isToolkit(account, toolkit));
|
|
277
|
+
}
|
|
278
|
+
async function verifyConnectedAccount(client, userId, connectedAccountId) {
|
|
279
|
+
return verifyConnectedAccountForToolkit(client, userId, connectedAccountId, SLACK_TOOLKIT, "Slack", () => listActiveSlackAccounts(client, userId));
|
|
280
|
+
}
|
|
281
|
+
async function verifyConnectedAccountForToolkit(client, userId, connectedAccountId, toolkit, label, fallbackList) {
|
|
282
|
+
const account = client.connectedAccounts.get
|
|
283
|
+
? toConnectedAccount(await client.connectedAccounts.get(connectedAccountId))
|
|
284
|
+
: ((await fallbackList?.())?.find((candidate) => candidate.id === connectedAccountId) ?? null);
|
|
285
|
+
if (!account) {
|
|
286
|
+
throw new ComposioSetupError(`Could not find Composio connected account ${connectedAccountId} for user ${userId}.`);
|
|
287
|
+
}
|
|
288
|
+
if (!isToolkit(account, toolkit)) {
|
|
289
|
+
throw new ComposioSetupError(`Connected account ${connectedAccountId} is not a ${label} account.`);
|
|
290
|
+
}
|
|
291
|
+
if (!isActive(account)) {
|
|
292
|
+
throw new ComposioSetupError(`Connected account ${connectedAccountId} is not ACTIVE (status: ${account.status ?? "unknown"}).`);
|
|
293
|
+
}
|
|
294
|
+
return account;
|
|
295
|
+
}
|
|
296
|
+
async function getAuthConfig(client, authConfigId) {
|
|
297
|
+
const result = client.authConfigs?.get
|
|
298
|
+
? await client.authConfigs.get(authConfigId)
|
|
299
|
+
: client.authConfigs?.retrieve
|
|
300
|
+
? await client.authConfigs.retrieve(authConfigId)
|
|
301
|
+
: null;
|
|
302
|
+
return toAuthConfigSummary(result);
|
|
303
|
+
}
|
|
304
|
+
async function accountCanSendGmail(client, account) {
|
|
305
|
+
if (hasGmailNotifyScopes(account))
|
|
306
|
+
return true;
|
|
307
|
+
const authConfigId = account.authConfig?.id;
|
|
308
|
+
if (!authConfigId)
|
|
309
|
+
return false;
|
|
310
|
+
const authConfig = await getAuthConfig(client, authConfigId);
|
|
311
|
+
return authConfig ? authConfigAllowsGmailSend(authConfig) : false;
|
|
312
|
+
}
|
|
313
|
+
async function resolveGmailConnectAuthConfigId(client, explicitAuthConfigId, nonInteractive) {
|
|
314
|
+
if (explicitAuthConfigId) {
|
|
315
|
+
const config = await getAuthConfig(client, explicitAuthConfigId);
|
|
316
|
+
if (config?.toolkit?.slug && config.toolkit.slug.toLowerCase() !== GMAIL_TOOLKIT) {
|
|
317
|
+
throw new ComposioSetupError(`Auth config ${explicitAuthConfigId} is not a Gmail config.`);
|
|
318
|
+
}
|
|
319
|
+
if (config && !authConfigAllowsGmailSend(config)) {
|
|
320
|
+
console.log(chalk.yellow(`Auth config ${explicitAuthConfigId} does not explicitly list ${GMAIL_SEND_TOOL}; creating the link anyway.`));
|
|
321
|
+
}
|
|
322
|
+
return explicitAuthConfigId;
|
|
323
|
+
}
|
|
324
|
+
if (!client.authConfigs?.list) {
|
|
325
|
+
throw new ComposioSetupError("Composio SDK client does not expose authConfigs.list(); pass --auth-config-id.");
|
|
326
|
+
}
|
|
327
|
+
const configs = authConfigsFromListResult(await client.authConfigs.list({ toolkit: GMAIL_TOOLKIT })).filter((config) => !config.toolkit?.slug || config.toolkit.slug.toLowerCase() === GMAIL_TOOLKIT);
|
|
328
|
+
const sendConfigs = configs.filter(authConfigAllowsGmailSend);
|
|
329
|
+
const candidates = sendConfigs.length > 0 ? sendConfigs : configs;
|
|
330
|
+
if (candidates.length === 0) {
|
|
331
|
+
throw new ComposioSetupError("No Composio Gmail auth config found. Create/connect Gmail in Composio, or rerun with --auth-config-id ac_...");
|
|
332
|
+
}
|
|
333
|
+
if (sendConfigs.length === 0) {
|
|
334
|
+
console.log(chalk.yellow(`No Gmail auth config explicitly lists ${GMAIL_SEND_TOOL}; using an existing Gmail auth config anyway.`));
|
|
335
|
+
}
|
|
336
|
+
return (await chooseAuthConfig(candidates, nonInteractive, "Gmail")).id;
|
|
337
|
+
}
|
|
338
|
+
async function chooseAccount(accounts, nonInteractive, label = "Slack") {
|
|
339
|
+
if (accounts.length === 1)
|
|
340
|
+
return accounts[0];
|
|
341
|
+
if (nonInteractive) {
|
|
342
|
+
throw new ComposioSetupError(`Multiple active ${label} connected accounts found. Re-run with --connected-account-id.\n` +
|
|
343
|
+
accounts.map((account) => ` - ${account.id}`).join("\n"));
|
|
344
|
+
}
|
|
345
|
+
const clack = await import("@clack/prompts");
|
|
346
|
+
const selected = await clack.select({
|
|
347
|
+
message: `Select the ${label} connected account AO should use:`,
|
|
348
|
+
options: accounts.map((account) => ({
|
|
349
|
+
value: account.id,
|
|
350
|
+
label: account.alias ? `${account.alias} (${account.id})` : account.id,
|
|
351
|
+
})),
|
|
352
|
+
});
|
|
353
|
+
if (clack.isCancel(selected)) {
|
|
354
|
+
throw new ComposioSetupError("Setup cancelled.", 0);
|
|
355
|
+
}
|
|
356
|
+
return accounts.find((account) => account.id === selected);
|
|
357
|
+
}
|
|
358
|
+
async function chooseAuthConfig(configs, nonInteractive, label) {
|
|
359
|
+
if (configs.length === 1)
|
|
360
|
+
return configs[0];
|
|
361
|
+
if (nonInteractive) {
|
|
362
|
+
throw new ComposioSetupError(`Multiple ${label} auth configs found. Re-run with --auth-config-id.\n` +
|
|
363
|
+
configs.map((config) => ` - ${config.id}`).join("\n"));
|
|
364
|
+
}
|
|
365
|
+
const clack = await import("@clack/prompts");
|
|
366
|
+
const selected = await clack.select({
|
|
367
|
+
message: `Select the ${label} auth config AO should use:`,
|
|
368
|
+
options: configs.map((config) => ({
|
|
369
|
+
value: config.id,
|
|
370
|
+
label: config.id,
|
|
371
|
+
})),
|
|
372
|
+
});
|
|
373
|
+
if (clack.isCancel(selected)) {
|
|
374
|
+
throw new ComposioSetupError("Setup cancelled.", 0);
|
|
375
|
+
}
|
|
376
|
+
return configs.find((config) => config.id === selected);
|
|
377
|
+
}
|
|
378
|
+
function toConnectionRequest(value) {
|
|
379
|
+
if (!isRecord(value))
|
|
380
|
+
return {};
|
|
381
|
+
return {
|
|
382
|
+
id: stringValue(value["id"]),
|
|
383
|
+
redirectUrl: stringValue(value["redirectUrl"]),
|
|
384
|
+
waitForConnection: typeof value["waitForConnection"] === "function"
|
|
385
|
+
? value["waitForConnection"]
|
|
386
|
+
: undefined,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async function resolveManagedAuthConfigId(client, toolkit, label, name, options = {}) {
|
|
390
|
+
if (!options.forceCreate) {
|
|
391
|
+
const existing = client.authConfigs?.list
|
|
392
|
+
? authConfigsFromListResult(await client.authConfigs.list({ toolkit })).find((config) => (!config.toolkit?.slug || config.toolkit.slug.toLowerCase() === toolkit) &&
|
|
393
|
+
(!options.existingAuthConfigPredicate || options.existingAuthConfigPredicate(config)))?.id
|
|
394
|
+
: undefined;
|
|
395
|
+
if (existing)
|
|
396
|
+
return existing;
|
|
397
|
+
}
|
|
398
|
+
if (!client.authConfigs?.create) {
|
|
399
|
+
throw new ComposioSetupError(`Composio SDK client does not expose authConfigs.create(); connect ${label} in Composio and pass --connected-account-id.`);
|
|
400
|
+
}
|
|
401
|
+
const created = await client.authConfigs.create(toolkit, {
|
|
402
|
+
type: "use_composio_managed_auth",
|
|
403
|
+
name,
|
|
404
|
+
...(options.scopes ? { credentials: { scopes: [...options.scopes] } } : {}),
|
|
405
|
+
...(options.toolsForConnectedAccountCreation
|
|
406
|
+
? {
|
|
407
|
+
toolAccessConfig: {
|
|
408
|
+
toolsForConnectedAccountCreation: options.toolsForConnectedAccountCreation,
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
: {}),
|
|
412
|
+
});
|
|
413
|
+
const createdId = isRecord(created) ? stringValue(created["id"]) : undefined;
|
|
414
|
+
if (!createdId) {
|
|
415
|
+
throw new ComposioSetupError(`Could not create a Composio ${label} auth config.`);
|
|
416
|
+
}
|
|
417
|
+
return createdId;
|
|
418
|
+
}
|
|
419
|
+
async function createConnectionRequest(client, userId, waitMs) {
|
|
420
|
+
return createManagedOAuthConnectionRequest(client, userId, SLACK_TOOLKIT, "Slack", "Slack Auth Config", waitMs);
|
|
421
|
+
}
|
|
422
|
+
async function createManagedOAuthConnectionRequest(client, userId, toolkit, label, authConfigName, waitMs, options = {}) {
|
|
423
|
+
let request;
|
|
424
|
+
if (client.connectedAccounts.link) {
|
|
425
|
+
const authConfigId = options.authConfigId ??
|
|
426
|
+
(await resolveManagedAuthConfigId(client, toolkit, label, authConfigName, {
|
|
427
|
+
scopes: options.scopes,
|
|
428
|
+
toolsForConnectedAccountCreation: options.toolsForConnectedAccountCreation,
|
|
429
|
+
existingAuthConfigPredicate: options.existingAuthConfigPredicate,
|
|
430
|
+
forceCreate: options.forceCreateAuthConfig,
|
|
431
|
+
}));
|
|
432
|
+
request = toConnectionRequest(await client.connectedAccounts.link(userId, authConfigId, { allowMultiple: true }));
|
|
433
|
+
}
|
|
434
|
+
else if (client.toolkits?.authorize) {
|
|
435
|
+
request = toConnectionRequest(await client.toolkits.authorize(userId, toolkit, options.authConfigId));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
throw new ComposioSetupError(`Composio SDK client does not expose connectedAccounts.link(); connect ${label} in Composio and pass --connected-account-id.`);
|
|
439
|
+
}
|
|
440
|
+
if (request.redirectUrl) {
|
|
441
|
+
console.log(chalk.cyan(`Open this Composio ${label} connect URL: ${request.redirectUrl}`));
|
|
442
|
+
}
|
|
443
|
+
if (!request.id && !request.waitForConnection) {
|
|
444
|
+
return { url: request.redirectUrl };
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const connected = request.waitForConnection
|
|
448
|
+
? await request.waitForConnection(waitMs)
|
|
449
|
+
: await client.connectedAccounts.waitForConnection?.(request.id, waitMs);
|
|
450
|
+
const account = toConnectedAccount(connected);
|
|
451
|
+
return account ? { account, url: request.redirectUrl } : { url: request.redirectUrl };
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
455
|
+
console.log(chalk.yellow(`Connection did not complete yet: ${message}`));
|
|
456
|
+
return { url: request.redirectUrl };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function parseWaitMs(value) {
|
|
460
|
+
if (!value)
|
|
461
|
+
return 60_000;
|
|
462
|
+
const parsed = Number(value);
|
|
463
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
464
|
+
throw new ComposioSetupError("--wait-ms must be a non-negative number.");
|
|
465
|
+
}
|
|
466
|
+
return parsed;
|
|
467
|
+
}
|
|
468
|
+
function channelConfig(channel) {
|
|
469
|
+
const value = stringValue(channel);
|
|
470
|
+
if (!value)
|
|
471
|
+
return {};
|
|
472
|
+
if (/^[CGD][A-Z0-9]{8,}$/.test(value)) {
|
|
473
|
+
return { channelId: value };
|
|
474
|
+
}
|
|
475
|
+
return { channelName: value };
|
|
476
|
+
}
|
|
477
|
+
function shouldUseInteractiveComposioHub(opts, nonInteractive) {
|
|
478
|
+
if (nonInteractive || opts.status)
|
|
479
|
+
return false;
|
|
480
|
+
if (getDirectComposioAppChoice(opts))
|
|
481
|
+
return true;
|
|
482
|
+
return !(stringValue(opts.apiKey) ||
|
|
483
|
+
stringValue(opts.userId) ||
|
|
484
|
+
stringValue(opts.channel) ||
|
|
485
|
+
stringValue(opts.connectedAccountId) ||
|
|
486
|
+
(stringValue(opts.waitMs) && stringValue(opts.waitMs) !== "60000"));
|
|
487
|
+
}
|
|
488
|
+
function getDirectComposioAppChoice(opts) {
|
|
489
|
+
const choices = [];
|
|
490
|
+
if (opts.slack)
|
|
491
|
+
choices.push("slack");
|
|
492
|
+
if (opts.discordWebhook)
|
|
493
|
+
choices.push("discord-webhook");
|
|
494
|
+
if (opts.discordBot)
|
|
495
|
+
choices.push("discord-bot");
|
|
496
|
+
if (opts.gmail)
|
|
497
|
+
choices.push("gmail");
|
|
498
|
+
if (choices.length > 1) {
|
|
499
|
+
throw new ComposioSetupError("Choose only one Composio app flag: --slack, --discord-webhook, --discord-bot, or --gmail.");
|
|
500
|
+
}
|
|
501
|
+
return choices[0];
|
|
502
|
+
}
|
|
503
|
+
function shouldUseInteractiveDedicatedSetup(opts, nonInteractive) {
|
|
504
|
+
return !nonInteractive && !opts.status;
|
|
505
|
+
}
|
|
506
|
+
function cancelInteractiveComposioSetup(clack) {
|
|
507
|
+
clack.cancel("Setup cancelled.");
|
|
508
|
+
throw new ComposioSetupError("Setup cancelled.", 0);
|
|
509
|
+
}
|
|
510
|
+
function printComposioApiKeyInstructions() {
|
|
511
|
+
console.log("");
|
|
512
|
+
console.log(chalk.bold("Find your Composio API key"));
|
|
513
|
+
console.log(` 1. Open ${COMPOSIO_DASHBOARD_URL}`);
|
|
514
|
+
console.log(" 2. Open your project settings or developer settings.");
|
|
515
|
+
console.log(" 3. Create or copy an API key that can execute tools.");
|
|
516
|
+
console.log("");
|
|
517
|
+
}
|
|
518
|
+
function printComposioSlackAccountInfo() {
|
|
519
|
+
console.log(chalk.dim("AO uses a Composio Slack connected account to execute SLACK_SEND_MESSAGE. The userId groups connected accounts inside your Composio project."));
|
|
520
|
+
}
|
|
521
|
+
function printComposioSlackChannelInfo() {
|
|
522
|
+
console.log(chalk.dim("Slack channel is optional for Composio. If set, AO passes it to SLACK_SEND_MESSAGE; use the channel name without # or a Slack channel id."));
|
|
523
|
+
}
|
|
524
|
+
function printComposioSlackReview(resolved, apiKeySource) {
|
|
525
|
+
console.log("");
|
|
526
|
+
console.log(chalk.bold("Review Composio Slack setup"));
|
|
527
|
+
console.log(" app: Slack");
|
|
528
|
+
console.log(` notifier: ${resolved.targetName ?? COMPOSIO_NOTIFIER}`);
|
|
529
|
+
console.log(` api key: configured from ${apiKeySource}`);
|
|
530
|
+
console.log(` userId: ${resolved.userId}`);
|
|
531
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "not configured"}`);
|
|
532
|
+
console.log(` channel: ${resolved.channel ?? "not set"}`);
|
|
533
|
+
console.log(` routing: ${routingReviewLabel(resolved.routingPreset)}`);
|
|
534
|
+
console.log("");
|
|
535
|
+
}
|
|
536
|
+
function redactDiscordWebhookUrl(webhookUrl) {
|
|
537
|
+
if (!webhookUrl)
|
|
538
|
+
return "not configured";
|
|
539
|
+
try {
|
|
540
|
+
const parsed = new URL(webhookUrl);
|
|
541
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
542
|
+
const webhookIndex = segments.findIndex((segment) => segment === "webhooks");
|
|
543
|
+
if (webhookIndex >= 0 && segments[webhookIndex + 1]) {
|
|
544
|
+
return `${parsed.origin}/api/webhooks/${segments[webhookIndex + 1]}/...`;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// Fall through to generic redaction.
|
|
549
|
+
}
|
|
550
|
+
return "configured";
|
|
551
|
+
}
|
|
552
|
+
function printComposioDiscordWebhookInfo() {
|
|
553
|
+
console.log(chalk.dim("AO uses Composio's discordbot toolkit with DISCORDBOT_EXECUTE_WEBHOOK. Webhook mode stores the Discord webhook token as a Composio bearer connected account; no Discord bot invite is required."));
|
|
554
|
+
}
|
|
555
|
+
function printDiscordWebhookInstructions() {
|
|
556
|
+
console.log("");
|
|
557
|
+
console.log(chalk.bold("Create a Discord webhook URL"));
|
|
558
|
+
console.log(` 1. Open ${DISCORD_APP_URL}`);
|
|
559
|
+
console.log(" 2. Open the target server and channel.");
|
|
560
|
+
console.log(" 3. Open Edit Channel > Integrations > Webhooks.");
|
|
561
|
+
console.log(" 4. Create a webhook and copy its URL.");
|
|
562
|
+
console.log(" 5. Paste the URL here.");
|
|
563
|
+
console.log(chalk.dim(`Discord help: ${DISCORD_WEBHOOK_DOCS_URL}`));
|
|
564
|
+
console.log("");
|
|
565
|
+
}
|
|
566
|
+
function printComposioDiscordWebhookReview(resolved, apiKeySource) {
|
|
567
|
+
console.log("");
|
|
568
|
+
console.log(chalk.bold("Review Composio Discord webhook setup"));
|
|
569
|
+
console.log(" app: Discord webhook");
|
|
570
|
+
console.log(` notifier: ${resolved.targetName}`);
|
|
571
|
+
console.log(` api key: configured from ${apiKeySource}`);
|
|
572
|
+
console.log(` userId: ${resolved.userId}`);
|
|
573
|
+
console.log(` webhookUrl: ${redactDiscordWebhookUrl(resolved.webhookUrl)}`);
|
|
574
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "will be created from webhook URL"}`);
|
|
575
|
+
console.log(` toolVersion: ${DISCORD_TOOL_VERSION}`);
|
|
576
|
+
console.log(` routing: ${routingReviewLabel(resolved.routingPreset)}`);
|
|
577
|
+
console.log("");
|
|
578
|
+
}
|
|
579
|
+
function printComposioDiscordBotInfo() {
|
|
580
|
+
console.log(chalk.dim("AO uses Composio's discordbot toolkit with DISCORDBOT_CREATE_MESSAGE. Bot mode requires a Discord channel id and a Composio Discord bot connected account."));
|
|
581
|
+
}
|
|
582
|
+
function printDiscordBotInstructions() {
|
|
583
|
+
console.log("");
|
|
584
|
+
console.log(chalk.bold("Create and invite a Discord bot"));
|
|
585
|
+
console.log(` 1. Open ${DISCORD_DEVELOPER_PORTAL_URL}`);
|
|
586
|
+
console.log(" 2. Create or select an application, then open Bot.");
|
|
587
|
+
console.log(" 3. Create/reset the bot token and keep it available for this setup.");
|
|
588
|
+
console.log(" 4. Open OAuth2 > URL Generator.");
|
|
589
|
+
console.log(" 5. Select the bot scope and grant View Channel + Send Messages.");
|
|
590
|
+
console.log(" 6. Open the generated URL and invite the bot to the target server.");
|
|
591
|
+
console.log(" 7. In Discord, enable Developer Mode and copy the target channel ID.");
|
|
592
|
+
console.log("");
|
|
593
|
+
}
|
|
594
|
+
function printDiscordChannelIdInstructions() {
|
|
595
|
+
console.log("");
|
|
596
|
+
console.log(chalk.bold("Find a Discord channel ID"));
|
|
597
|
+
console.log(" 1. Open Discord User Settings > Advanced.");
|
|
598
|
+
console.log(" 2. Enable Developer Mode.");
|
|
599
|
+
console.log(" 3. Right-click the target channel.");
|
|
600
|
+
console.log(" 4. Click Copy Channel ID and paste it here.");
|
|
601
|
+
console.log("");
|
|
602
|
+
}
|
|
603
|
+
function printComposioDiscordBotReview(resolved, apiKeySource) {
|
|
604
|
+
console.log("");
|
|
605
|
+
console.log(chalk.bold("Review Composio Discord bot setup"));
|
|
606
|
+
console.log(" app: Discord bot");
|
|
607
|
+
console.log(` notifier: ${resolved.targetName}`);
|
|
608
|
+
console.log(` api key: configured from ${apiKeySource}`);
|
|
609
|
+
console.log(` userId: ${resolved.userId}`);
|
|
610
|
+
console.log(` channelId: ${resolved.channelId ?? "not configured"}`);
|
|
611
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "not configured"}`);
|
|
612
|
+
console.log(` toolVersion: ${DISCORD_TOOL_VERSION}`);
|
|
613
|
+
console.log(` routing: ${routingReviewLabel(resolved.routingPreset)}`);
|
|
614
|
+
console.log("");
|
|
615
|
+
}
|
|
616
|
+
function printComposioGmailInfo() {
|
|
617
|
+
console.log(chalk.dim("AO uses Composio's Gmail toolkit with GMAIL_SEND_EMAIL. Gmail mode requires a recipient email and a Gmail connected account with send/profile access."));
|
|
618
|
+
}
|
|
619
|
+
function printComposioGmailConnectInfo() {
|
|
620
|
+
console.log("");
|
|
621
|
+
console.log(chalk.bold("Connect Gmail in Composio"));
|
|
622
|
+
console.log(` 1. Open ${COMPOSIO_DASHBOARD_URL}`);
|
|
623
|
+
console.log(" 2. Make sure your project has a Gmail auth config with send access.");
|
|
624
|
+
console.log(" 3. AO can create a Composio connect link from that existing auth config.");
|
|
625
|
+
console.log(" 4. Complete the Google OAuth flow, then return here.");
|
|
626
|
+
console.log(chalk.dim("AO does not create Gmail OAuth/auth configs because Google may block unverified or invalid OAuth apps."));
|
|
627
|
+
console.log("");
|
|
628
|
+
}
|
|
629
|
+
function printComposioGmailReview(resolved, apiKeySource) {
|
|
630
|
+
console.log("");
|
|
631
|
+
console.log(chalk.bold("Review Composio Gmail setup"));
|
|
632
|
+
console.log(" app: Gmail");
|
|
633
|
+
console.log(` notifier: ${resolved.targetName ?? COMPOSIO_NOTIFIER}`);
|
|
634
|
+
console.log(` api key: configured from ${apiKeySource}`);
|
|
635
|
+
console.log(` userId: ${resolved.userId}`);
|
|
636
|
+
console.log(` emailTo: ${resolved.emailTo ?? "not configured"}`);
|
|
637
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "not configured"}`);
|
|
638
|
+
console.log(` toolVersion: ${GMAIL_TOOL_VERSION}`);
|
|
639
|
+
console.log(` routing: ${routingReviewLabel(resolved.routingPreset)}`);
|
|
640
|
+
console.log("");
|
|
641
|
+
}
|
|
642
|
+
function printComposioAppRequirements(choice) {
|
|
643
|
+
console.log("");
|
|
644
|
+
if (choice === "discord-webhook") {
|
|
645
|
+
console.log(chalk.bold("Composio Discord webhook setup"));
|
|
646
|
+
console.log(" Required: Composio API key, userId, Discord webhook URL.");
|
|
647
|
+
console.log(" AO creates/stores a Composio connected account from the webhook token.");
|
|
648
|
+
console.log(" No Discord bot invite is required for webhook mode.");
|
|
649
|
+
console.log(" Current command: athene setup composio-discord --webhook-url <url>");
|
|
650
|
+
}
|
|
651
|
+
else if (choice === "discord-bot") {
|
|
652
|
+
console.log(chalk.bold("Composio Discord bot setup"));
|
|
653
|
+
console.log(" Required: Composio API key, userId, Discord channel id.");
|
|
654
|
+
console.log(" Also required once: bot token, unless you already have connectedAccountId.");
|
|
655
|
+
console.log(" Current command: athene setup composio-discord-bot --channel-id <id>");
|
|
656
|
+
}
|
|
657
|
+
else if (choice === "gmail") {
|
|
658
|
+
console.log(chalk.bold("Composio Gmail setup"));
|
|
659
|
+
console.log(" Required: Composio API key, userId, recipient email, Gmail connectedAccountId.");
|
|
660
|
+
console.log(" Gmail OAuth/auth config must be usable in Composio with send/profile access.");
|
|
661
|
+
console.log(" Current command: athene setup composio-mail --email-to <email>");
|
|
662
|
+
}
|
|
663
|
+
console.log(chalk.dim("This interactive hub currently implements Slack, Discord webhook/bot, and Gmail."));
|
|
664
|
+
console.log("");
|
|
665
|
+
}
|
|
666
|
+
async function promptApiKeyInput(clack) {
|
|
667
|
+
const apiKeyInput = await clack.password({
|
|
668
|
+
message: "Composio API key:",
|
|
669
|
+
validate: (value) => {
|
|
670
|
+
if (!String(value ?? "").trim())
|
|
671
|
+
return "Composio API key is required.";
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
if (clack.isCancel(apiKeyInput)) {
|
|
675
|
+
cancelInteractiveComposioSetup(clack);
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
apiKey: String(apiKeyInput).trim(),
|
|
679
|
+
shouldWriteApiKey: true,
|
|
680
|
+
sourceLabel: "prompt",
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async function promptInteractiveComposioApiKey(clack, opts, existing) {
|
|
684
|
+
const existingKey = resolveApiKeyCandidate(opts, existing);
|
|
685
|
+
while (true) {
|
|
686
|
+
const choice = existingKey
|
|
687
|
+
? await clack.select({
|
|
688
|
+
message: `Composio API key is already available from ${existingKey.sourceLabel}.`,
|
|
689
|
+
options: [
|
|
690
|
+
{
|
|
691
|
+
value: "use-existing",
|
|
692
|
+
label: "Use existing API key",
|
|
693
|
+
hint: "Keep the configured key",
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
value: "enter-new",
|
|
697
|
+
label: "Enter a new API key",
|
|
698
|
+
hint: "Store it in this config",
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
value: "show-steps",
|
|
702
|
+
label: "Show where to find it",
|
|
703
|
+
hint: "Print Composio dashboard steps",
|
|
704
|
+
},
|
|
705
|
+
{ value: "back", label: "Back", hint: "Return to app choices" },
|
|
706
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
707
|
+
],
|
|
708
|
+
})
|
|
709
|
+
: await clack.select({
|
|
710
|
+
message: "Composio API key is required to list and create connected accounts.",
|
|
711
|
+
options: [
|
|
712
|
+
{
|
|
713
|
+
value: "enter-new",
|
|
714
|
+
label: "Enter API key",
|
|
715
|
+
hint: "Store it in this config",
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
value: "show-steps",
|
|
719
|
+
label: "Show where to find it",
|
|
720
|
+
hint: "Print Composio dashboard steps",
|
|
721
|
+
},
|
|
722
|
+
{ value: "back", label: "Back", hint: "Return to app choices" },
|
|
723
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
724
|
+
],
|
|
725
|
+
});
|
|
726
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
727
|
+
cancelInteractiveComposioSetup(clack);
|
|
728
|
+
}
|
|
729
|
+
if (choice === "back")
|
|
730
|
+
return "back";
|
|
731
|
+
if (choice === "use-existing" && existingKey)
|
|
732
|
+
return existingKey;
|
|
733
|
+
if (choice === "show-steps") {
|
|
734
|
+
printComposioApiKeyInstructions();
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (choice === "enter-new") {
|
|
738
|
+
return promptApiKeyInput(clack);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function promptInteractiveComposioUserId(clack, opts, existing) {
|
|
743
|
+
const currentUserId = resolveUserId(opts, existing);
|
|
744
|
+
console.log(chalk.dim(`userId is the Composio user namespace AO uses for tool execution and connected-account lookup. For AO-managed setups, ${DEFAULT_COMPOSIO_USER_ID} is the recommended default.`));
|
|
745
|
+
while (true) {
|
|
746
|
+
const choice = await clack.select({
|
|
747
|
+
message: `Composio userId: ${currentUserId}`,
|
|
748
|
+
options: [
|
|
749
|
+
{ value: "use-current", label: `Use ${currentUserId}`, hint: "Recommended" },
|
|
750
|
+
{ value: "change", label: "Change userId", hint: "Use a different Composio user id" },
|
|
751
|
+
{ value: "back", label: "Back", hint: "Return to API key" },
|
|
752
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
753
|
+
],
|
|
754
|
+
});
|
|
755
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
756
|
+
cancelInteractiveComposioSetup(clack);
|
|
757
|
+
}
|
|
758
|
+
if (choice === "back")
|
|
759
|
+
return "back";
|
|
760
|
+
if (choice === "use-current")
|
|
761
|
+
return currentUserId;
|
|
762
|
+
if (choice === "change") {
|
|
763
|
+
const nextUserId = await clack.text({
|
|
764
|
+
message: "Composio userId:",
|
|
765
|
+
initialValue: currentUserId,
|
|
766
|
+
validate: (value) => {
|
|
767
|
+
if (!String(value ?? "").trim())
|
|
768
|
+
return "Composio userId is required.";
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
if (clack.isCancel(nextUserId)) {
|
|
772
|
+
cancelInteractiveComposioSetup(clack);
|
|
773
|
+
}
|
|
774
|
+
return String(nextUserId).trim();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function promptManualSlackConnectedAccountId(clack, client, userId, initialValue) {
|
|
779
|
+
const accountId = await clack.text({
|
|
780
|
+
message: "Composio Slack connectedAccountId:",
|
|
781
|
+
placeholder: "ca_...",
|
|
782
|
+
initialValue,
|
|
783
|
+
validate: (value) => {
|
|
784
|
+
if (!String(value ?? "").trim())
|
|
785
|
+
return "connectedAccountId is required.";
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
if (clack.isCancel(accountId)) {
|
|
789
|
+
cancelInteractiveComposioSetup(clack);
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
const account = await verifyConnectedAccount(client, userId, String(accountId).trim());
|
|
793
|
+
return account.id;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
797
|
+
return "back";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function promptChooseSlackConnectedAccount(clack, accounts) {
|
|
801
|
+
if (accounts.length === 0) {
|
|
802
|
+
console.log(chalk.yellow("No active Slack connected accounts were found for this Composio userId."));
|
|
803
|
+
return "back";
|
|
804
|
+
}
|
|
805
|
+
const selected = await clack.select({
|
|
806
|
+
message: "Select the Slack connected account AO should use:",
|
|
807
|
+
options: [
|
|
808
|
+
...accounts.map((account) => ({
|
|
809
|
+
value: account.id,
|
|
810
|
+
label: account.alias ? `${account.alias} (${account.id})` : account.id,
|
|
811
|
+
})),
|
|
812
|
+
{ value: "back", label: "Back", hint: "Return to Slack account options" },
|
|
813
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
814
|
+
],
|
|
815
|
+
});
|
|
816
|
+
if (clack.isCancel(selected) || selected === "cancel") {
|
|
817
|
+
cancelInteractiveComposioSetup(clack);
|
|
818
|
+
}
|
|
819
|
+
if (selected === "back")
|
|
820
|
+
return "back";
|
|
821
|
+
return accounts.find((account) => account.id === selected) ?? "back";
|
|
822
|
+
}
|
|
823
|
+
async function promptAfterSlackConnectLink(clack, client, userId, existingConnectedAccountId) {
|
|
824
|
+
console.log(chalk.yellow("Slack connection did not complete yet. Open the connect URL above and finish the Composio flow."));
|
|
825
|
+
while (true) {
|
|
826
|
+
const next = await clack.select({
|
|
827
|
+
message: "After opening the Composio Slack connect link, what do you want to do?",
|
|
828
|
+
options: [
|
|
829
|
+
{
|
|
830
|
+
value: "check-active",
|
|
831
|
+
label: "I completed the connection",
|
|
832
|
+
hint: "Check Composio for active Slack accounts",
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
value: "retry-link",
|
|
836
|
+
label: "Generate link again",
|
|
837
|
+
hint: "Create a fresh Composio Slack connect URL",
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
value: "enter-id",
|
|
841
|
+
label: "Enter connectedAccountId",
|
|
842
|
+
hint: "Use an existing ca_... value",
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
value: "back",
|
|
846
|
+
label: "Back",
|
|
847
|
+
hint: "Return to Slack account options",
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
value: "cancel",
|
|
851
|
+
label: "Cancel setup",
|
|
852
|
+
hint: "Do not change config",
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
});
|
|
856
|
+
if (clack.isCancel(next) || next === "cancel") {
|
|
857
|
+
cancelInteractiveComposioSetup(clack);
|
|
858
|
+
}
|
|
859
|
+
if (next === "back")
|
|
860
|
+
return "back";
|
|
861
|
+
if (next === "retry-link")
|
|
862
|
+
return "retry-link";
|
|
863
|
+
if (next === "enter-id") {
|
|
864
|
+
const accountId = await promptManualSlackConnectedAccountId(clack, client, userId, existingConnectedAccountId);
|
|
865
|
+
if (accountId !== "back")
|
|
866
|
+
return accountId;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (next === "check-active") {
|
|
870
|
+
const account = await promptChooseSlackConnectedAccount(clack, await listActiveSlackAccounts(client, userId));
|
|
871
|
+
if (account !== "back")
|
|
872
|
+
return account.id;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function promptInteractiveSlackAccount(clack, client, userId, opts, existing) {
|
|
877
|
+
const existingConnectedAccountId = stringValue(opts.connectedAccountId) ?? stringValue(existing["connectedAccountId"]);
|
|
878
|
+
printComposioSlackAccountInfo();
|
|
879
|
+
while (true) {
|
|
880
|
+
const options = [
|
|
881
|
+
...(existingConnectedAccountId
|
|
882
|
+
? [
|
|
883
|
+
{
|
|
884
|
+
value: "use-existing",
|
|
885
|
+
label: "Use existing connected account",
|
|
886
|
+
hint: existingConnectedAccountId,
|
|
887
|
+
},
|
|
888
|
+
]
|
|
889
|
+
: []),
|
|
890
|
+
{
|
|
891
|
+
value: "choose-active",
|
|
892
|
+
label: "Choose active Slack account",
|
|
893
|
+
hint: "List accounts already connected in Composio",
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
value: "create-link",
|
|
897
|
+
label: "Generate Slack connect link",
|
|
898
|
+
hint: "Open Composio OAuth link and wait for completion",
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
value: "enter-id",
|
|
902
|
+
label: "Enter connectedAccountId",
|
|
903
|
+
hint: "Use an existing ca_... value",
|
|
904
|
+
},
|
|
905
|
+
{ value: "back", label: "Back", hint: "Return to userId" },
|
|
906
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
907
|
+
];
|
|
908
|
+
const choice = await clack.select({
|
|
909
|
+
message: "How do you want to choose the Slack connected account?",
|
|
910
|
+
options,
|
|
911
|
+
});
|
|
912
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
913
|
+
cancelInteractiveComposioSetup(clack);
|
|
914
|
+
}
|
|
915
|
+
if (choice === "back")
|
|
916
|
+
return "back";
|
|
917
|
+
if (choice === "use-existing" && existingConnectedAccountId) {
|
|
918
|
+
try {
|
|
919
|
+
const account = await verifyConnectedAccount(client, userId, existingConnectedAccountId);
|
|
920
|
+
return account.id;
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (choice === "choose-active") {
|
|
928
|
+
const account = await promptChooseSlackConnectedAccount(clack, await listActiveSlackAccounts(client, userId));
|
|
929
|
+
if (account !== "back")
|
|
930
|
+
return account.id;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (choice === "create-link") {
|
|
934
|
+
while (true) {
|
|
935
|
+
const connection = await createConnectionRequest(client, userId, parseWaitMs(opts.waitMs));
|
|
936
|
+
if (connection.account)
|
|
937
|
+
return connection.account.id;
|
|
938
|
+
const next = await promptAfterSlackConnectLink(clack, client, userId, existingConnectedAccountId);
|
|
939
|
+
if (next === "retry-link")
|
|
940
|
+
continue;
|
|
941
|
+
if (next !== "back")
|
|
942
|
+
return next;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (choice === "enter-id") {
|
|
948
|
+
const accountId = await promptManualSlackConnectedAccountId(clack, client, userId, existingConnectedAccountId);
|
|
949
|
+
if (accountId !== "back")
|
|
950
|
+
return accountId;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function promptInteractiveSlackChannel(clack, opts, existing) {
|
|
955
|
+
const existingChannel = stringValue(opts.channel) ??
|
|
956
|
+
stringValue(existing["channelName"]) ??
|
|
957
|
+
stringValue(existing["channelId"]);
|
|
958
|
+
printComposioSlackChannelInfo();
|
|
959
|
+
while (true) {
|
|
960
|
+
const choice = await clack.select({
|
|
961
|
+
message: `Slack channel: ${existingChannel ?? "not set"}`,
|
|
962
|
+
options: [
|
|
963
|
+
{
|
|
964
|
+
value: "use-current",
|
|
965
|
+
label: existingChannel ? `Use ${existingChannel}` : "Leave unset",
|
|
966
|
+
hint: existingChannel ? "Keep current Slack target override" : "Do not pass channel",
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
value: "change",
|
|
970
|
+
label: "Set channel",
|
|
971
|
+
hint: "Channel name without #, or a Slack channel id",
|
|
972
|
+
},
|
|
973
|
+
...(existingChannel
|
|
974
|
+
? [{ value: "clear", label: "Clear channel", hint: "Do not pass channel" }]
|
|
975
|
+
: []),
|
|
976
|
+
{ value: "back", label: "Back", hint: "Return to Slack account" },
|
|
977
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
978
|
+
],
|
|
979
|
+
});
|
|
980
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
981
|
+
cancelInteractiveComposioSetup(clack);
|
|
982
|
+
}
|
|
983
|
+
if (choice === "back")
|
|
984
|
+
return "back";
|
|
985
|
+
if (choice === "use-current")
|
|
986
|
+
return existingChannel;
|
|
987
|
+
if (choice === "clear")
|
|
988
|
+
return undefined;
|
|
989
|
+
if (choice === "change") {
|
|
990
|
+
const channel = await clack.text({
|
|
991
|
+
message: "Slack channel name or id:",
|
|
992
|
+
placeholder: "iamasx",
|
|
993
|
+
initialValue: existingChannel,
|
|
994
|
+
});
|
|
995
|
+
if (clack.isCancel(channel)) {
|
|
996
|
+
cancelInteractiveComposioSetup(clack);
|
|
997
|
+
}
|
|
998
|
+
return stringValue(channel);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function promptInteractiveComposioSlackReview(clack, resolved, apiKeySource) {
|
|
1003
|
+
printComposioSlackReview(resolved, apiKeySource);
|
|
1004
|
+
const choice = await clack.select({
|
|
1005
|
+
message: "Write this Composio Slack config?",
|
|
1006
|
+
options: [
|
|
1007
|
+
{ value: "write", label: "Write config", hint: "Update agent-orchestrator.yaml" },
|
|
1008
|
+
{ value: "channel", label: "Change channel", hint: "Return to channel step" },
|
|
1009
|
+
{ value: "account", label: "Change Slack account", hint: "Return to account step" },
|
|
1010
|
+
{ value: "routing", label: "Change routing", hint: "Choose notification priorities" },
|
|
1011
|
+
{ value: "app", label: "Back to app choices", hint: "Choose another Composio app" },
|
|
1012
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1013
|
+
],
|
|
1014
|
+
});
|
|
1015
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1016
|
+
cancelInteractiveComposioSetup(clack);
|
|
1017
|
+
}
|
|
1018
|
+
return choice;
|
|
1019
|
+
}
|
|
1020
|
+
async function promptDiscordWebhookUrlInput(clack, initialValue) {
|
|
1021
|
+
const webhookUrlInput = await clack.text({
|
|
1022
|
+
message: "Discord webhook URL:",
|
|
1023
|
+
placeholder: "https://discord.com/api/webhooks/...",
|
|
1024
|
+
initialValue,
|
|
1025
|
+
validate: (value) => {
|
|
1026
|
+
if (!String(value ?? "").trim())
|
|
1027
|
+
return "Discord webhook URL is required.";
|
|
1028
|
+
try {
|
|
1029
|
+
parseDiscordWebhookUrl(String(value).trim());
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
return error instanceof Error ? error.message : String(error);
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
if (clack.isCancel(webhookUrlInput)) {
|
|
1037
|
+
cancelInteractiveComposioSetup(clack);
|
|
1038
|
+
}
|
|
1039
|
+
return String(webhookUrlInput).trim();
|
|
1040
|
+
}
|
|
1041
|
+
async function promptAfterDiscordWebhookInstructions(clack, initialValue) {
|
|
1042
|
+
printDiscordWebhookInstructions();
|
|
1043
|
+
while (true) {
|
|
1044
|
+
const next = await clack.select({
|
|
1045
|
+
message: "After creating the Discord webhook, what do you want to do?",
|
|
1046
|
+
options: [
|
|
1047
|
+
{ value: "enter-url", label: "Paste webhook URL", hint: "Continue setup" },
|
|
1048
|
+
{
|
|
1049
|
+
value: "show-steps",
|
|
1050
|
+
label: "Show steps again",
|
|
1051
|
+
hint: "Reprint the Discord app URL and steps",
|
|
1052
|
+
},
|
|
1053
|
+
{ value: "back", label: "Back", hint: "Return to webhook URL options" },
|
|
1054
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1055
|
+
],
|
|
1056
|
+
});
|
|
1057
|
+
if (clack.isCancel(next) || next === "cancel") {
|
|
1058
|
+
cancelInteractiveComposioSetup(clack);
|
|
1059
|
+
}
|
|
1060
|
+
if (next === "back")
|
|
1061
|
+
return "back";
|
|
1062
|
+
if (next === "show-steps") {
|
|
1063
|
+
printDiscordWebhookInstructions();
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (next === "enter-url") {
|
|
1067
|
+
return promptDiscordWebhookUrlInput(clack, initialValue);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function promptInteractiveDiscordWebhookUrl(clack, existingWebhookUrl) {
|
|
1072
|
+
printComposioDiscordWebhookInfo();
|
|
1073
|
+
while (true) {
|
|
1074
|
+
const choice = await clack.select({
|
|
1075
|
+
message: `Discord webhook URL: ${redactDiscordWebhookUrl(existingWebhookUrl)}`,
|
|
1076
|
+
options: [
|
|
1077
|
+
...(existingWebhookUrl
|
|
1078
|
+
? [
|
|
1079
|
+
{
|
|
1080
|
+
value: "use-existing",
|
|
1081
|
+
label: "Use existing webhook URL",
|
|
1082
|
+
hint: redactDiscordWebhookUrl(existingWebhookUrl),
|
|
1083
|
+
},
|
|
1084
|
+
]
|
|
1085
|
+
: []),
|
|
1086
|
+
{
|
|
1087
|
+
value: "enter-url",
|
|
1088
|
+
label: "Paste webhook URL",
|
|
1089
|
+
hint: "Use a Discord incoming webhook URL",
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
value: "show-steps",
|
|
1093
|
+
label: "Show me how to create one",
|
|
1094
|
+
hint: "Print Discord webhook creation steps",
|
|
1095
|
+
},
|
|
1096
|
+
{ value: "back", label: "Back", hint: "Return to userId" },
|
|
1097
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1098
|
+
],
|
|
1099
|
+
});
|
|
1100
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1101
|
+
cancelInteractiveComposioSetup(clack);
|
|
1102
|
+
}
|
|
1103
|
+
if (choice === "back")
|
|
1104
|
+
return "back";
|
|
1105
|
+
if (choice === "use-existing" && existingWebhookUrl)
|
|
1106
|
+
return existingWebhookUrl;
|
|
1107
|
+
if (choice === "enter-url") {
|
|
1108
|
+
return promptDiscordWebhookUrlInput(clack, existingWebhookUrl);
|
|
1109
|
+
}
|
|
1110
|
+
if (choice === "show-steps") {
|
|
1111
|
+
const result = await promptAfterDiscordWebhookInstructions(clack, existingWebhookUrl);
|
|
1112
|
+
if (result !== "back")
|
|
1113
|
+
return result;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
async function promptInteractiveComposioDiscordWebhookReview(clack, resolved, apiKeySource) {
|
|
1118
|
+
printComposioDiscordWebhookReview(resolved, apiKeySource);
|
|
1119
|
+
const choice = await clack.select({
|
|
1120
|
+
message: "Write this Composio Discord webhook config?",
|
|
1121
|
+
options: [
|
|
1122
|
+
{ value: "write", label: "Write config", hint: "Update agent-orchestrator.yaml" },
|
|
1123
|
+
{ value: "webhook", label: "Change webhook URL", hint: "Return to webhook URL step" },
|
|
1124
|
+
{
|
|
1125
|
+
value: "account",
|
|
1126
|
+
label: "Change connected account",
|
|
1127
|
+
hint: "Create, choose, or enter a Composio connected account",
|
|
1128
|
+
},
|
|
1129
|
+
{ value: "routing", label: "Change routing", hint: "Choose notification priorities" },
|
|
1130
|
+
{ value: "app", label: "Back to app choices", hint: "Choose another Composio app" },
|
|
1131
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1132
|
+
],
|
|
1133
|
+
});
|
|
1134
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1135
|
+
cancelInteractiveComposioSetup(clack);
|
|
1136
|
+
}
|
|
1137
|
+
return choice;
|
|
1138
|
+
}
|
|
1139
|
+
async function promptManualDiscordWebhookConnectedAccountId(clack, client, userId, initialValue) {
|
|
1140
|
+
const accountId = await clack.text({
|
|
1141
|
+
message: "Composio Discord webhook connectedAccountId:",
|
|
1142
|
+
placeholder: "ca_...",
|
|
1143
|
+
initialValue,
|
|
1144
|
+
validate: (value) => {
|
|
1145
|
+
if (!String(value ?? "").trim())
|
|
1146
|
+
return "connectedAccountId is required.";
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
if (clack.isCancel(accountId)) {
|
|
1150
|
+
cancelInteractiveComposioSetup(clack);
|
|
1151
|
+
}
|
|
1152
|
+
try {
|
|
1153
|
+
const account = await verifyConnectedAccountForToolkit(client, userId, String(accountId).trim(), DISCORD_TOOLKIT, "Discord webhook");
|
|
1154
|
+
return account.id;
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1158
|
+
return "back";
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function promptChooseDiscordWebhookConnectedAccount(clack, accounts) {
|
|
1162
|
+
if (accounts.length === 0) {
|
|
1163
|
+
console.log(chalk.yellow("No active Discord webhook connected accounts were found for this Composio userId."));
|
|
1164
|
+
return "back";
|
|
1165
|
+
}
|
|
1166
|
+
const selected = await clack.select({
|
|
1167
|
+
message: "Select the Discord webhook connected account AO should use:",
|
|
1168
|
+
options: [
|
|
1169
|
+
...accounts.map((account) => ({
|
|
1170
|
+
value: account.id,
|
|
1171
|
+
label: account.alias ? `${account.alias} (${account.id})` : account.id,
|
|
1172
|
+
})),
|
|
1173
|
+
{ value: "back", label: "Back", hint: "Return to Discord webhook account options" },
|
|
1174
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1175
|
+
],
|
|
1176
|
+
});
|
|
1177
|
+
if (clack.isCancel(selected) || selected === "cancel") {
|
|
1178
|
+
cancelInteractiveComposioSetup(clack);
|
|
1179
|
+
}
|
|
1180
|
+
if (selected === "back")
|
|
1181
|
+
return "back";
|
|
1182
|
+
return accounts.find((account) => account.id === selected) ?? "back";
|
|
1183
|
+
}
|
|
1184
|
+
async function promptInteractiveDiscordWebhookAccount(clack, client, userId, webhookUrl, existingConnectedAccountId) {
|
|
1185
|
+
while (true) {
|
|
1186
|
+
const choice = await clack.select({
|
|
1187
|
+
message: "How do you want to configure the Composio Discord webhook connected account?",
|
|
1188
|
+
options: [
|
|
1189
|
+
...(existingConnectedAccountId
|
|
1190
|
+
? [
|
|
1191
|
+
{
|
|
1192
|
+
value: "use-existing",
|
|
1193
|
+
label: "Use existing connected account",
|
|
1194
|
+
hint: existingConnectedAccountId,
|
|
1195
|
+
},
|
|
1196
|
+
]
|
|
1197
|
+
: []),
|
|
1198
|
+
{
|
|
1199
|
+
value: "create-account",
|
|
1200
|
+
label: "Create from webhook URL",
|
|
1201
|
+
hint: "Store the webhook token in Composio for this userId",
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
value: "choose-active",
|
|
1205
|
+
label: "Choose active Discord account",
|
|
1206
|
+
hint: "List discordbot accounts already connected in Composio",
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
value: "enter-id",
|
|
1210
|
+
label: "Enter connectedAccountId",
|
|
1211
|
+
hint: "Use an existing ca_... value",
|
|
1212
|
+
},
|
|
1213
|
+
{ value: "back", label: "Back", hint: "Return to webhook URL" },
|
|
1214
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1215
|
+
],
|
|
1216
|
+
});
|
|
1217
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1218
|
+
cancelInteractiveComposioSetup(clack);
|
|
1219
|
+
}
|
|
1220
|
+
if (choice === "back")
|
|
1221
|
+
return "back";
|
|
1222
|
+
if (choice === "use-existing" && existingConnectedAccountId) {
|
|
1223
|
+
try {
|
|
1224
|
+
const account = await verifyConnectedAccountForToolkit(client, userId, existingConnectedAccountId, DISCORD_TOOLKIT, "Discord webhook");
|
|
1225
|
+
return account.id;
|
|
1226
|
+
}
|
|
1227
|
+
catch (error) {
|
|
1228
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (choice === "create-account") {
|
|
1233
|
+
return resolveDiscordWebhookConnectedAccountId(client, userId, webhookUrl);
|
|
1234
|
+
}
|
|
1235
|
+
if (choice === "choose-active") {
|
|
1236
|
+
const account = await promptChooseDiscordWebhookConnectedAccount(clack, await listActiveToolkitAccounts(client, userId, DISCORD_TOOLKIT));
|
|
1237
|
+
if (account !== "back")
|
|
1238
|
+
return account.id;
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (choice === "enter-id") {
|
|
1242
|
+
const accountId = await promptManualDiscordWebhookConnectedAccountId(clack, client, userId, existingConnectedAccountId);
|
|
1243
|
+
if (accountId !== "back")
|
|
1244
|
+
return accountId;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function validateDiscordChannelIdInput(value) {
|
|
1249
|
+
if (!value.trim())
|
|
1250
|
+
return "Discord channel id is required.";
|
|
1251
|
+
if (!/^\d{8,}$/.test(value.trim()))
|
|
1252
|
+
return "Discord channel id must be numeric.";
|
|
1253
|
+
}
|
|
1254
|
+
async function promptDiscordBotChannelIdInput(clack, initialValue) {
|
|
1255
|
+
const channelIdInput = await clack.text({
|
|
1256
|
+
message: "Discord channel ID:",
|
|
1257
|
+
placeholder: "1234567890",
|
|
1258
|
+
initialValue,
|
|
1259
|
+
validate: (value) => validateDiscordChannelIdInput(String(value ?? "")),
|
|
1260
|
+
});
|
|
1261
|
+
if (clack.isCancel(channelIdInput)) {
|
|
1262
|
+
cancelInteractiveComposioSetup(clack);
|
|
1263
|
+
}
|
|
1264
|
+
return String(channelIdInput).trim();
|
|
1265
|
+
}
|
|
1266
|
+
async function promptAfterDiscordChannelIdInstructions(clack, initialValue) {
|
|
1267
|
+
printDiscordChannelIdInstructions();
|
|
1268
|
+
while (true) {
|
|
1269
|
+
const next = await clack.select({
|
|
1270
|
+
message: "After copying the Discord channel ID, what do you want to do?",
|
|
1271
|
+
options: [
|
|
1272
|
+
{ value: "enter-id", label: "Paste channel ID", hint: "Continue setup" },
|
|
1273
|
+
{
|
|
1274
|
+
value: "show-steps",
|
|
1275
|
+
label: "Show steps again",
|
|
1276
|
+
hint: "Reprint the Developer Mode steps",
|
|
1277
|
+
},
|
|
1278
|
+
{ value: "back", label: "Back", hint: "Return to channel options" },
|
|
1279
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1280
|
+
],
|
|
1281
|
+
});
|
|
1282
|
+
if (clack.isCancel(next) || next === "cancel") {
|
|
1283
|
+
cancelInteractiveComposioSetup(clack);
|
|
1284
|
+
}
|
|
1285
|
+
if (next === "back")
|
|
1286
|
+
return "back";
|
|
1287
|
+
if (next === "show-steps") {
|
|
1288
|
+
printDiscordChannelIdInstructions();
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (next === "enter-id") {
|
|
1292
|
+
return promptDiscordBotChannelIdInput(clack, initialValue);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
async function promptInteractiveDiscordBotChannel(clack, existingChannelId) {
|
|
1297
|
+
printComposioDiscordBotInfo();
|
|
1298
|
+
while (true) {
|
|
1299
|
+
const choice = await clack.select({
|
|
1300
|
+
message: `Discord channel ID: ${existingChannelId ?? "not configured"}`,
|
|
1301
|
+
options: [
|
|
1302
|
+
...(existingChannelId
|
|
1303
|
+
? [
|
|
1304
|
+
{
|
|
1305
|
+
value: "use-existing",
|
|
1306
|
+
label: "Use existing channel ID",
|
|
1307
|
+
hint: existingChannelId,
|
|
1308
|
+
},
|
|
1309
|
+
]
|
|
1310
|
+
: []),
|
|
1311
|
+
{ value: "enter-id", label: "Paste channel ID", hint: "Use a Discord channel id" },
|
|
1312
|
+
{
|
|
1313
|
+
value: "show-steps",
|
|
1314
|
+
label: "Show me how to find it",
|
|
1315
|
+
hint: "Print Developer Mode channel-id steps",
|
|
1316
|
+
},
|
|
1317
|
+
{ value: "back", label: "Back", hint: "Return to userId" },
|
|
1318
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1319
|
+
],
|
|
1320
|
+
});
|
|
1321
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1322
|
+
cancelInteractiveComposioSetup(clack);
|
|
1323
|
+
}
|
|
1324
|
+
if (choice === "back")
|
|
1325
|
+
return "back";
|
|
1326
|
+
if (choice === "use-existing" && existingChannelId)
|
|
1327
|
+
return existingChannelId;
|
|
1328
|
+
if (choice === "enter-id")
|
|
1329
|
+
return promptDiscordBotChannelIdInput(clack, existingChannelId);
|
|
1330
|
+
if (choice === "show-steps") {
|
|
1331
|
+
const result = await promptAfterDiscordChannelIdInstructions(clack, existingChannelId);
|
|
1332
|
+
if (result !== "back")
|
|
1333
|
+
return result;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async function promptManualDiscordBotConnectedAccountId(clack, client, userId, initialValue) {
|
|
1338
|
+
const accountId = await clack.text({
|
|
1339
|
+
message: "Composio Discord bot connectedAccountId:",
|
|
1340
|
+
placeholder: "ca_...",
|
|
1341
|
+
initialValue,
|
|
1342
|
+
validate: (value) => {
|
|
1343
|
+
if (!String(value ?? "").trim())
|
|
1344
|
+
return "connectedAccountId is required.";
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
if (clack.isCancel(accountId)) {
|
|
1348
|
+
cancelInteractiveComposioSetup(clack);
|
|
1349
|
+
}
|
|
1350
|
+
try {
|
|
1351
|
+
const account = await verifyConnectedAccountForToolkit(client, userId, String(accountId).trim(), DISCORD_TOOLKIT, "Discord Bot");
|
|
1352
|
+
return account.id;
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1356
|
+
return "back";
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async function promptChooseDiscordBotConnectedAccount(clack, accounts) {
|
|
1360
|
+
if (accounts.length === 0) {
|
|
1361
|
+
console.log(chalk.yellow("No active Discord bot connected accounts were found for this Composio userId."));
|
|
1362
|
+
return "back";
|
|
1363
|
+
}
|
|
1364
|
+
const selected = await clack.select({
|
|
1365
|
+
message: "Select the Discord bot connected account AO should use:",
|
|
1366
|
+
options: [
|
|
1367
|
+
...accounts.map((account) => ({
|
|
1368
|
+
value: account.id,
|
|
1369
|
+
label: account.alias ? `${account.alias} (${account.id})` : account.id,
|
|
1370
|
+
})),
|
|
1371
|
+
{ value: "back", label: "Back", hint: "Return to Discord bot account options" },
|
|
1372
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1373
|
+
],
|
|
1374
|
+
});
|
|
1375
|
+
if (clack.isCancel(selected) || selected === "cancel") {
|
|
1376
|
+
cancelInteractiveComposioSetup(clack);
|
|
1377
|
+
}
|
|
1378
|
+
if (selected === "back")
|
|
1379
|
+
return "back";
|
|
1380
|
+
return accounts.find((account) => account.id === selected) ?? "back";
|
|
1381
|
+
}
|
|
1382
|
+
async function promptDiscordBotTokenInput(clack, optionToken) {
|
|
1383
|
+
const envToken = stringValue(process.env.DISCORD_BOT_TOKEN);
|
|
1384
|
+
if (optionToken || envToken) {
|
|
1385
|
+
const choice = await clack.select({
|
|
1386
|
+
message: "Discord bot token:",
|
|
1387
|
+
options: [
|
|
1388
|
+
...(optionToken
|
|
1389
|
+
? [
|
|
1390
|
+
{
|
|
1391
|
+
value: "use-option",
|
|
1392
|
+
label: "Use provided bot token",
|
|
1393
|
+
hint: "Used once; not written to config",
|
|
1394
|
+
},
|
|
1395
|
+
]
|
|
1396
|
+
: []),
|
|
1397
|
+
...(envToken
|
|
1398
|
+
? [
|
|
1399
|
+
{
|
|
1400
|
+
value: "use-env",
|
|
1401
|
+
label: "Use DISCORD_BOT_TOKEN",
|
|
1402
|
+
hint: "Use the current environment",
|
|
1403
|
+
},
|
|
1404
|
+
]
|
|
1405
|
+
: []),
|
|
1406
|
+
{ value: "paste", label: "Paste bot token", hint: "Used once; not written to config" },
|
|
1407
|
+
{ value: "back", label: "Back", hint: "Return to account options" },
|
|
1408
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1409
|
+
],
|
|
1410
|
+
});
|
|
1411
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1412
|
+
cancelInteractiveComposioSetup(clack);
|
|
1413
|
+
}
|
|
1414
|
+
if (choice === "back")
|
|
1415
|
+
return "back";
|
|
1416
|
+
if (choice === "use-option" && optionToken)
|
|
1417
|
+
return optionToken;
|
|
1418
|
+
if (choice === "use-env" && envToken)
|
|
1419
|
+
return envToken;
|
|
1420
|
+
}
|
|
1421
|
+
printDiscordBotInstructions();
|
|
1422
|
+
const token = await clack.password({
|
|
1423
|
+
message: "Discord bot token:",
|
|
1424
|
+
validate: (value) => {
|
|
1425
|
+
if (!String(value ?? "").trim())
|
|
1426
|
+
return "Discord bot token is required.";
|
|
1427
|
+
},
|
|
1428
|
+
});
|
|
1429
|
+
if (clack.isCancel(token)) {
|
|
1430
|
+
cancelInteractiveComposioSetup(clack);
|
|
1431
|
+
}
|
|
1432
|
+
return String(token).trim();
|
|
1433
|
+
}
|
|
1434
|
+
async function promptInteractiveDiscordBotAccount(clack, client, userId, channelId, existingConnectedAccountId, optionToken) {
|
|
1435
|
+
while (true) {
|
|
1436
|
+
const choice = await clack.select({
|
|
1437
|
+
message: "How do you want to configure the Composio Discord bot account?",
|
|
1438
|
+
options: [
|
|
1439
|
+
...(existingConnectedAccountId
|
|
1440
|
+
? [
|
|
1441
|
+
{
|
|
1442
|
+
value: "use-existing",
|
|
1443
|
+
label: "Use existing connected account",
|
|
1444
|
+
hint: existingConnectedAccountId,
|
|
1445
|
+
},
|
|
1446
|
+
]
|
|
1447
|
+
: []),
|
|
1448
|
+
{
|
|
1449
|
+
value: "choose-active",
|
|
1450
|
+
label: "Choose active Discord bot account",
|
|
1451
|
+
hint: "List active discordbot accounts for this userId",
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
value: "enter-id",
|
|
1455
|
+
label: "Enter connectedAccountId",
|
|
1456
|
+
hint: "Use an existing ca_... value",
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
value: "create-account",
|
|
1460
|
+
label: "Create from bot token",
|
|
1461
|
+
hint: "Validate channel access and store a Composio connected account",
|
|
1462
|
+
},
|
|
1463
|
+
{ value: "back", label: "Back", hint: "Return to channel ID" },
|
|
1464
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1465
|
+
],
|
|
1466
|
+
});
|
|
1467
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1468
|
+
cancelInteractiveComposioSetup(clack);
|
|
1469
|
+
}
|
|
1470
|
+
if (choice === "back")
|
|
1471
|
+
return "back";
|
|
1472
|
+
if (choice === "use-existing" && existingConnectedAccountId) {
|
|
1473
|
+
try {
|
|
1474
|
+
const account = await verifyConnectedAccountForToolkit(client, userId, existingConnectedAccountId, DISCORD_TOOLKIT, "Discord Bot");
|
|
1475
|
+
return account.id;
|
|
1476
|
+
}
|
|
1477
|
+
catch (error) {
|
|
1478
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (choice === "choose-active") {
|
|
1483
|
+
const account = await promptChooseDiscordBotConnectedAccount(clack, await listActiveToolkitAccounts(client, userId, DISCORD_TOOLKIT));
|
|
1484
|
+
if (account !== "back")
|
|
1485
|
+
return account.id;
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (choice === "enter-id") {
|
|
1489
|
+
const accountId = await promptManualDiscordBotConnectedAccountId(clack, client, userId, existingConnectedAccountId);
|
|
1490
|
+
if (accountId !== "back")
|
|
1491
|
+
return accountId;
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (choice === "create-account") {
|
|
1495
|
+
const token = await promptDiscordBotTokenInput(clack, optionToken);
|
|
1496
|
+
if (token === "back")
|
|
1497
|
+
continue;
|
|
1498
|
+
try {
|
|
1499
|
+
await validateDiscordBotChannelAccess(token, channelId);
|
|
1500
|
+
return await createDiscordBearerConnectedAccount(client, userId, token, "Discord Bot Auth Config");
|
|
1501
|
+
}
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function promptInteractiveComposioDiscordBotReview(clack, resolved, apiKeySource) {
|
|
1509
|
+
printComposioDiscordBotReview(resolved, apiKeySource);
|
|
1510
|
+
const choice = await clack.select({
|
|
1511
|
+
message: "Write this Composio Discord bot config?",
|
|
1512
|
+
options: [
|
|
1513
|
+
{ value: "write", label: "Write config", hint: "Update agent-orchestrator.yaml" },
|
|
1514
|
+
{ value: "channel", label: "Change channel ID", hint: "Return to channel step" },
|
|
1515
|
+
{ value: "account", label: "Change bot account", hint: "Return to account step" },
|
|
1516
|
+
{ value: "routing", label: "Change routing", hint: "Choose notification priorities" },
|
|
1517
|
+
{ value: "app", label: "Back to app choices", hint: "Choose another Composio app" },
|
|
1518
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1519
|
+
],
|
|
1520
|
+
});
|
|
1521
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1522
|
+
cancelInteractiveComposioSetup(clack);
|
|
1523
|
+
}
|
|
1524
|
+
return choice;
|
|
1525
|
+
}
|
|
1526
|
+
function validateEmailInput(value) {
|
|
1527
|
+
const trimmed = value.trim();
|
|
1528
|
+
if (!trimmed)
|
|
1529
|
+
return "Recipient email is required.";
|
|
1530
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed))
|
|
1531
|
+
return "Enter a valid email address.";
|
|
1532
|
+
}
|
|
1533
|
+
async function promptGmailEmailInput(clack, initialValue) {
|
|
1534
|
+
const email = await clack.text({
|
|
1535
|
+
message: "Recipient email:",
|
|
1536
|
+
placeholder: "alerts@example.com",
|
|
1537
|
+
initialValue,
|
|
1538
|
+
validate: (value) => validateEmailInput(String(value ?? "")),
|
|
1539
|
+
});
|
|
1540
|
+
if (clack.isCancel(email)) {
|
|
1541
|
+
cancelInteractiveComposioSetup(clack);
|
|
1542
|
+
}
|
|
1543
|
+
return String(email).trim();
|
|
1544
|
+
}
|
|
1545
|
+
async function promptInteractiveGmailEmail(clack, existingEmailTo) {
|
|
1546
|
+
printComposioGmailInfo();
|
|
1547
|
+
while (true) {
|
|
1548
|
+
const choice = await clack.select({
|
|
1549
|
+
message: `Gmail recipient email: ${existingEmailTo ?? "not configured"}`,
|
|
1550
|
+
options: [
|
|
1551
|
+
...(existingEmailTo
|
|
1552
|
+
? [
|
|
1553
|
+
{
|
|
1554
|
+
value: "use-existing",
|
|
1555
|
+
label: "Use existing recipient",
|
|
1556
|
+
hint: existingEmailTo,
|
|
1557
|
+
},
|
|
1558
|
+
]
|
|
1559
|
+
: []),
|
|
1560
|
+
{
|
|
1561
|
+
value: "enter-email",
|
|
1562
|
+
label: "Enter recipient email",
|
|
1563
|
+
hint: "AO sends notification emails to this address",
|
|
1564
|
+
},
|
|
1565
|
+
{ value: "back", label: "Back", hint: "Return to userId" },
|
|
1566
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1567
|
+
],
|
|
1568
|
+
});
|
|
1569
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1570
|
+
cancelInteractiveComposioSetup(clack);
|
|
1571
|
+
}
|
|
1572
|
+
if (choice === "back")
|
|
1573
|
+
return "back";
|
|
1574
|
+
if (choice === "use-existing" && existingEmailTo)
|
|
1575
|
+
return existingEmailTo;
|
|
1576
|
+
if (choice === "enter-email")
|
|
1577
|
+
return promptGmailEmailInput(clack, existingEmailTo);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async function verifyUsableGmailConnectedAccount(client, userId, connectedAccountId) {
|
|
1581
|
+
const account = await withConnectedAccountDetails(client, await verifyConnectedAccountForToolkit(client, userId, connectedAccountId, GMAIL_TOOLKIT, "Gmail", () => listActiveGmailAccounts(client, userId)));
|
|
1582
|
+
if (!(await accountCanSendGmail(client, account))) {
|
|
1583
|
+
throw new ComposioSetupError(`Connected account ${connectedAccountId} is missing Gmail send/profile access. Reconnect Gmail in Composio with send access, or use a different Gmail connected account.`);
|
|
1584
|
+
}
|
|
1585
|
+
return account;
|
|
1586
|
+
}
|
|
1587
|
+
async function promptManualGmailConnectedAccountId(clack, client, userId, initialValue) {
|
|
1588
|
+
const accountId = await clack.text({
|
|
1589
|
+
message: "Composio Gmail connectedAccountId:",
|
|
1590
|
+
placeholder: "ca_...",
|
|
1591
|
+
initialValue,
|
|
1592
|
+
validate: (value) => {
|
|
1593
|
+
if (!String(value ?? "").trim())
|
|
1594
|
+
return "connectedAccountId is required.";
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
1597
|
+
if (clack.isCancel(accountId)) {
|
|
1598
|
+
cancelInteractiveComposioSetup(clack);
|
|
1599
|
+
}
|
|
1600
|
+
try {
|
|
1601
|
+
const account = await verifyUsableGmailConnectedAccount(client, userId, String(accountId).trim());
|
|
1602
|
+
return account.id;
|
|
1603
|
+
}
|
|
1604
|
+
catch (error) {
|
|
1605
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1606
|
+
return "back";
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async function promptChooseGmailConnectedAccount(clack, accounts) {
|
|
1610
|
+
if (accounts.length === 0) {
|
|
1611
|
+
console.log(chalk.yellow("No active Gmail connected accounts with send/profile access were found for this Composio userId."));
|
|
1612
|
+
return "back";
|
|
1613
|
+
}
|
|
1614
|
+
const selected = await clack.select({
|
|
1615
|
+
message: "Select the Gmail connected account AO should use:",
|
|
1616
|
+
options: [
|
|
1617
|
+
...accounts.map((account) => ({
|
|
1618
|
+
value: account.id,
|
|
1619
|
+
label: account.alias ? `${account.alias} (${account.id})` : account.id,
|
|
1620
|
+
})),
|
|
1621
|
+
{ value: "back", label: "Back", hint: "Return to Gmail account options" },
|
|
1622
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1623
|
+
],
|
|
1624
|
+
});
|
|
1625
|
+
if (clack.isCancel(selected) || selected === "cancel") {
|
|
1626
|
+
cancelInteractiveComposioSetup(clack);
|
|
1627
|
+
}
|
|
1628
|
+
if (selected === "back")
|
|
1629
|
+
return "back";
|
|
1630
|
+
return accounts.find((account) => account.id === selected) ?? "back";
|
|
1631
|
+
}
|
|
1632
|
+
async function promptManualGmailAuthConfigId(clack, client, initialValue) {
|
|
1633
|
+
const authConfigId = await clack.text({
|
|
1634
|
+
message: "Composio Gmail authConfigId:",
|
|
1635
|
+
placeholder: "ac_...",
|
|
1636
|
+
initialValue,
|
|
1637
|
+
validate: (value) => {
|
|
1638
|
+
if (!String(value ?? "").trim())
|
|
1639
|
+
return "authConfigId is required.";
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
if (clack.isCancel(authConfigId)) {
|
|
1643
|
+
cancelInteractiveComposioSetup(clack);
|
|
1644
|
+
}
|
|
1645
|
+
const id = String(authConfigId).trim();
|
|
1646
|
+
const config = await getAuthConfig(client, id);
|
|
1647
|
+
if (config?.toolkit?.slug && config.toolkit.slug.toLowerCase() !== GMAIL_TOOLKIT) {
|
|
1648
|
+
console.log(chalk.yellow(`Auth config ${id} is not a Gmail config.`));
|
|
1649
|
+
return "back";
|
|
1650
|
+
}
|
|
1651
|
+
if (config && !authConfigAllowsGmailSend(config)) {
|
|
1652
|
+
console.log(chalk.yellow(`Auth config ${id} does not explicitly list ${GMAIL_SEND_TOOL}; AO will create the connect link anyway.`));
|
|
1653
|
+
}
|
|
1654
|
+
return id;
|
|
1655
|
+
}
|
|
1656
|
+
async function listGmailAuthConfigs(client) {
|
|
1657
|
+
if (!client.authConfigs?.list) {
|
|
1658
|
+
throw new ComposioSetupError("Composio SDK client does not expose authConfigs.list(); enter a Gmail authConfigId manually.");
|
|
1659
|
+
}
|
|
1660
|
+
return authConfigsFromListResult(await client.authConfigs.list({ toolkit: GMAIL_TOOLKIT })).filter((config) => !config.toolkit?.slug || config.toolkit.slug.toLowerCase() === GMAIL_TOOLKIT);
|
|
1661
|
+
}
|
|
1662
|
+
async function promptChooseGmailAuthConfig(clack, configs) {
|
|
1663
|
+
if (configs.length === 0) {
|
|
1664
|
+
console.log(chalk.yellow("No Composio Gmail auth configs were found. Create one in Composio or enter authConfigId manually."));
|
|
1665
|
+
return "back";
|
|
1666
|
+
}
|
|
1667
|
+
const sendConfigs = configs.filter(authConfigAllowsGmailSend);
|
|
1668
|
+
const candidates = sendConfigs.length > 0 ? sendConfigs : configs;
|
|
1669
|
+
if (sendConfigs.length === 0) {
|
|
1670
|
+
console.log(chalk.yellow(`No Gmail auth config explicitly lists ${GMAIL_SEND_TOOL}; showing existing Gmail auth configs anyway.`));
|
|
1671
|
+
}
|
|
1672
|
+
const selected = await clack.select({
|
|
1673
|
+
message: "Select the Gmail auth config for the Composio connect link:",
|
|
1674
|
+
options: [
|
|
1675
|
+
...candidates.map((config) => ({
|
|
1676
|
+
value: config.id,
|
|
1677
|
+
label: config.id,
|
|
1678
|
+
hint: authConfigAllowsGmailSend(config) ? GMAIL_SEND_TOOL : "Gmail auth config",
|
|
1679
|
+
})),
|
|
1680
|
+
{ value: "back", label: "Back", hint: "Return to Gmail account options" },
|
|
1681
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1682
|
+
],
|
|
1683
|
+
});
|
|
1684
|
+
if (clack.isCancel(selected) || selected === "cancel") {
|
|
1685
|
+
cancelInteractiveComposioSetup(clack);
|
|
1686
|
+
}
|
|
1687
|
+
if (selected === "back")
|
|
1688
|
+
return "back";
|
|
1689
|
+
return candidates.find((config) => config.id === selected) ?? "back";
|
|
1690
|
+
}
|
|
1691
|
+
async function promptInteractiveGmailAuthConfig(clack, client, existingAuthConfigId) {
|
|
1692
|
+
printComposioGmailConnectInfo();
|
|
1693
|
+
while (true) {
|
|
1694
|
+
const choice = await clack.select({
|
|
1695
|
+
message: "How should AO choose the Gmail auth config for the connect link?",
|
|
1696
|
+
options: [
|
|
1697
|
+
...(existingAuthConfigId
|
|
1698
|
+
? [
|
|
1699
|
+
{
|
|
1700
|
+
value: "use-existing",
|
|
1701
|
+
label: "Use existing authConfigId",
|
|
1702
|
+
hint: existingAuthConfigId,
|
|
1703
|
+
},
|
|
1704
|
+
]
|
|
1705
|
+
: []),
|
|
1706
|
+
{
|
|
1707
|
+
value: "choose-existing",
|
|
1708
|
+
label: "Choose existing Gmail auth config",
|
|
1709
|
+
hint: "List Gmail auth configs in Composio",
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
value: "enter-id",
|
|
1713
|
+
label: "Enter authConfigId",
|
|
1714
|
+
hint: "Use an existing ac_... value",
|
|
1715
|
+
},
|
|
1716
|
+
{ value: "back", label: "Back", hint: "Return to Gmail account options" },
|
|
1717
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1718
|
+
],
|
|
1719
|
+
});
|
|
1720
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1721
|
+
cancelInteractiveComposioSetup(clack);
|
|
1722
|
+
}
|
|
1723
|
+
if (choice === "back")
|
|
1724
|
+
return "back";
|
|
1725
|
+
if (choice === "use-existing" && existingAuthConfigId)
|
|
1726
|
+
return existingAuthConfigId;
|
|
1727
|
+
if (choice === "enter-id") {
|
|
1728
|
+
const id = await promptManualGmailAuthConfigId(clack, client, existingAuthConfigId);
|
|
1729
|
+
if (id !== "back")
|
|
1730
|
+
return id;
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (choice === "choose-existing") {
|
|
1734
|
+
try {
|
|
1735
|
+
const config = await promptChooseGmailAuthConfig(clack, await listGmailAuthConfigs(client));
|
|
1736
|
+
if (config !== "back")
|
|
1737
|
+
return config.id;
|
|
1738
|
+
}
|
|
1739
|
+
catch (error) {
|
|
1740
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function promptAfterGmailConnectLink(clack, client, userId, existingConnectedAccountId) {
|
|
1746
|
+
console.log(chalk.yellow("Gmail connection did not complete yet. Open the connect URL above and finish the Composio flow."));
|
|
1747
|
+
while (true) {
|
|
1748
|
+
const next = await clack.select({
|
|
1749
|
+
message: "After opening the Composio Gmail connect link, what do you want to do?",
|
|
1750
|
+
options: [
|
|
1751
|
+
{
|
|
1752
|
+
value: "check-active",
|
|
1753
|
+
label: "I completed the connection",
|
|
1754
|
+
hint: "Check Composio for usable Gmail accounts",
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
value: "retry-link",
|
|
1758
|
+
label: "Generate link again",
|
|
1759
|
+
hint: "Use the same Gmail auth config",
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
value: "change-auth-config",
|
|
1763
|
+
label: "Change authConfigId",
|
|
1764
|
+
hint: "Use a different Gmail auth config",
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
value: "enter-id",
|
|
1768
|
+
label: "Enter connectedAccountId",
|
|
1769
|
+
hint: "Use an existing ca_... value",
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
value: "back",
|
|
1773
|
+
label: "Back",
|
|
1774
|
+
hint: "Return to Gmail account options",
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
value: "cancel",
|
|
1778
|
+
label: "Cancel setup",
|
|
1779
|
+
hint: "Do not change config",
|
|
1780
|
+
},
|
|
1781
|
+
],
|
|
1782
|
+
});
|
|
1783
|
+
if (clack.isCancel(next) || next === "cancel") {
|
|
1784
|
+
cancelInteractiveComposioSetup(clack);
|
|
1785
|
+
}
|
|
1786
|
+
if (next === "back")
|
|
1787
|
+
return "back";
|
|
1788
|
+
if (next === "retry-link")
|
|
1789
|
+
return "retry-link";
|
|
1790
|
+
if (next === "change-auth-config")
|
|
1791
|
+
return "change-auth-config";
|
|
1792
|
+
if (next === "enter-id") {
|
|
1793
|
+
const accountId = await promptManualGmailConnectedAccountId(clack, client, userId, existingConnectedAccountId);
|
|
1794
|
+
if (accountId !== "back")
|
|
1795
|
+
return accountId;
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
if (next === "check-active") {
|
|
1799
|
+
const account = await promptChooseGmailConnectedAccount(clack, await listUsableGmailAccounts(client, userId));
|
|
1800
|
+
if (account !== "back")
|
|
1801
|
+
return account.id;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async function promptInteractiveGmailAccount(clack, client, userId, opts, existingConnectedAccountId, existingAuthConfigId) {
|
|
1806
|
+
let authConfigId = existingAuthConfigId;
|
|
1807
|
+
while (true) {
|
|
1808
|
+
const choice = await clack.select({
|
|
1809
|
+
message: "How do you want to choose the Gmail connected account?",
|
|
1810
|
+
options: [
|
|
1811
|
+
...(existingConnectedAccountId
|
|
1812
|
+
? [
|
|
1813
|
+
{
|
|
1814
|
+
value: "use-existing",
|
|
1815
|
+
label: "Use existing connected account",
|
|
1816
|
+
hint: existingConnectedAccountId,
|
|
1817
|
+
},
|
|
1818
|
+
]
|
|
1819
|
+
: []),
|
|
1820
|
+
{
|
|
1821
|
+
value: "choose-active",
|
|
1822
|
+
label: "Choose active Gmail account",
|
|
1823
|
+
hint: "List accounts already connected in Composio",
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
value: "create-link",
|
|
1827
|
+
label: "Generate Gmail connect link",
|
|
1828
|
+
hint: "Use an existing Composio Gmail auth config",
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
value: "enter-id",
|
|
1832
|
+
label: "Enter connectedAccountId",
|
|
1833
|
+
hint: "Use an existing ca_... value",
|
|
1834
|
+
},
|
|
1835
|
+
{ value: "back", label: "Back", hint: "Return to recipient email" },
|
|
1836
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1837
|
+
],
|
|
1838
|
+
});
|
|
1839
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1840
|
+
cancelInteractiveComposioSetup(clack);
|
|
1841
|
+
}
|
|
1842
|
+
if (choice === "back")
|
|
1843
|
+
return "back";
|
|
1844
|
+
if (choice === "use-existing" && existingConnectedAccountId) {
|
|
1845
|
+
try {
|
|
1846
|
+
const account = await verifyUsableGmailConnectedAccount(client, userId, existingConnectedAccountId);
|
|
1847
|
+
return account.id;
|
|
1848
|
+
}
|
|
1849
|
+
catch (error) {
|
|
1850
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1851
|
+
}
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (choice === "choose-active") {
|
|
1855
|
+
const account = await promptChooseGmailConnectedAccount(clack, await listUsableGmailAccounts(client, userId));
|
|
1856
|
+
if (account !== "back")
|
|
1857
|
+
return account.id;
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (choice === "enter-id") {
|
|
1861
|
+
const accountId = await promptManualGmailConnectedAccountId(clack, client, userId, existingConnectedAccountId);
|
|
1862
|
+
if (accountId !== "back")
|
|
1863
|
+
return accountId;
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
if (choice === "create-link") {
|
|
1867
|
+
while (true) {
|
|
1868
|
+
if (!authConfigId) {
|
|
1869
|
+
const selectedAuthConfigId = await promptInteractiveGmailAuthConfig(clack, client, authConfigId);
|
|
1870
|
+
if (selectedAuthConfigId === "back")
|
|
1871
|
+
break;
|
|
1872
|
+
authConfigId = selectedAuthConfigId;
|
|
1873
|
+
}
|
|
1874
|
+
const connection = await createManagedOAuthConnectionRequest(client, userId, GMAIL_TOOLKIT, "Gmail", "Gmail Auth Config", parseWaitMs(opts.waitMs), { authConfigId });
|
|
1875
|
+
if (connection.account) {
|
|
1876
|
+
try {
|
|
1877
|
+
const account = await withConnectedAccountDetails(client, connection.account);
|
|
1878
|
+
if (await accountCanSendGmail(client, account))
|
|
1879
|
+
return account.id;
|
|
1880
|
+
console.log(chalk.yellow(`Connected Gmail account ${account.id} is missing Gmail send/profile access.`));
|
|
1881
|
+
}
|
|
1882
|
+
catch (error) {
|
|
1883
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const next = await promptAfterGmailConnectLink(clack, client, userId, existingConnectedAccountId);
|
|
1887
|
+
if (next === "retry-link")
|
|
1888
|
+
continue;
|
|
1889
|
+
if (next === "change-auth-config") {
|
|
1890
|
+
authConfigId = undefined;
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
if (next !== "back")
|
|
1894
|
+
return next;
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
async function promptInteractiveComposioGmailReview(clack, resolved, apiKeySource) {
|
|
1901
|
+
printComposioGmailReview(resolved, apiKeySource);
|
|
1902
|
+
const choice = await clack.select({
|
|
1903
|
+
message: "Write this Composio Gmail config?",
|
|
1904
|
+
options: [
|
|
1905
|
+
{ value: "write", label: "Write config", hint: "Update agent-orchestrator.yaml" },
|
|
1906
|
+
{ value: "email", label: "Change recipient email", hint: "Return to email step" },
|
|
1907
|
+
{ value: "account", label: "Change Gmail account", hint: "Return to account step" },
|
|
1908
|
+
{ value: "routing", label: "Change routing", hint: "Choose notification priorities" },
|
|
1909
|
+
{ value: "app", label: "Back to app choices", hint: "Choose another Composio app" },
|
|
1910
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
1911
|
+
],
|
|
1912
|
+
});
|
|
1913
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
1914
|
+
cancelInteractiveComposioSetup(clack);
|
|
1915
|
+
}
|
|
1916
|
+
return choice;
|
|
1917
|
+
}
|
|
1918
|
+
async function confirmComposioSlackConflict(clack, targetName, existingPlugin, force) {
|
|
1919
|
+
if (!existingPlugin || existingPlugin === "composio" || force)
|
|
1920
|
+
return true;
|
|
1921
|
+
const replace = await clack.confirm({
|
|
1922
|
+
message: `notifiers.${targetName} already uses plugin "${existingPlugin}". Replace it with Composio Slack?`,
|
|
1923
|
+
initialValue: false,
|
|
1924
|
+
});
|
|
1925
|
+
if (clack.isCancel(replace)) {
|
|
1926
|
+
cancelInteractiveComposioSetup(clack);
|
|
1927
|
+
}
|
|
1928
|
+
if (!replace) {
|
|
1929
|
+
console.log(chalk.dim(`Keeping existing notifiers.${targetName} config.`));
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
async function confirmComposioDiscordWebhookConflict(clack, targetName, existingPlugin, force, label = "Composio Discord webhook") {
|
|
1935
|
+
if (!existingPlugin || existingPlugin === "composio" || force)
|
|
1936
|
+
return true;
|
|
1937
|
+
const replace = await clack.confirm({
|
|
1938
|
+
message: `notifiers.${targetName} already uses plugin "${existingPlugin}". Replace it with ${label}?`,
|
|
1939
|
+
initialValue: false,
|
|
1940
|
+
});
|
|
1941
|
+
if (clack.isCancel(replace)) {
|
|
1942
|
+
cancelInteractiveComposioSetup(clack);
|
|
1943
|
+
}
|
|
1944
|
+
if (!replace) {
|
|
1945
|
+
console.log(chalk.dim(`Keeping existing notifiers.${targetName} config.`));
|
|
1946
|
+
return false;
|
|
1947
|
+
}
|
|
1948
|
+
return true;
|
|
1949
|
+
}
|
|
1950
|
+
async function runInteractiveComposioSlackSetup(clack, opts, configPath, rawConfig, targetName = COMPOSIO_NOTIFIER) {
|
|
1951
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
1952
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
1953
|
+
const canReplace = await confirmComposioSlackConflict(clack, targetName, existingPlugin, opts.force);
|
|
1954
|
+
if (!canReplace)
|
|
1955
|
+
return "back";
|
|
1956
|
+
const optionRoutingPreset = resolveComposioRoutingPreset(opts.routingPreset);
|
|
1957
|
+
let step = "api-key";
|
|
1958
|
+
let apiKey;
|
|
1959
|
+
let userId;
|
|
1960
|
+
let client;
|
|
1961
|
+
let connectedAccountId;
|
|
1962
|
+
let channel;
|
|
1963
|
+
let routingPreset;
|
|
1964
|
+
while (true) {
|
|
1965
|
+
if (step === "api-key") {
|
|
1966
|
+
const result = await promptInteractiveComposioApiKey(clack, opts, existing);
|
|
1967
|
+
if (result === "back")
|
|
1968
|
+
return "back";
|
|
1969
|
+
apiKey = result;
|
|
1970
|
+
client = await loadComposioClient(apiKey.apiKey);
|
|
1971
|
+
step = "user-id";
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
if (step === "user-id") {
|
|
1975
|
+
const result = await promptInteractiveComposioUserId(clack, opts, existing);
|
|
1976
|
+
if (result === "back") {
|
|
1977
|
+
step = "api-key";
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
userId = result;
|
|
1981
|
+
step = "account";
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (step === "account") {
|
|
1985
|
+
if (!client || !userId) {
|
|
1986
|
+
step = "api-key";
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
const result = await promptInteractiveSlackAccount(clack, client, userId, opts, existing);
|
|
1990
|
+
if (result === "back") {
|
|
1991
|
+
step = "user-id";
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
connectedAccountId = result;
|
|
1995
|
+
step = "channel";
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (step === "channel") {
|
|
1999
|
+
const result = await promptInteractiveSlackChannel(clack, opts, existing);
|
|
2000
|
+
if (result === "back") {
|
|
2001
|
+
step = "account";
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
channel = result;
|
|
2005
|
+
step = "routing";
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (step === "routing") {
|
|
2009
|
+
const selection = optionRoutingPreset ??
|
|
2010
|
+
(await promptNotifierRoutingPreset(clack, rawConfig, targetName, "Composio Slack", () => cancelInteractiveComposioSetup(clack)));
|
|
2011
|
+
if (selection === "back") {
|
|
2012
|
+
step = "channel";
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
routingPreset = selection === "preserve" ? undefined : selection;
|
|
2016
|
+
step = "review";
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
if (!apiKey || !userId || !connectedAccountId) {
|
|
2020
|
+
step = "api-key";
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
const resolved = {
|
|
2024
|
+
apiKey: apiKey.apiKey,
|
|
2025
|
+
shouldWriteApiKey: apiKey.shouldWriteApiKey,
|
|
2026
|
+
userId,
|
|
2027
|
+
targetName,
|
|
2028
|
+
channel,
|
|
2029
|
+
connectedAccountId,
|
|
2030
|
+
routingPreset,
|
|
2031
|
+
};
|
|
2032
|
+
const reviewChoice = await promptInteractiveComposioSlackReview(clack, resolved, apiKey.sourceLabel);
|
|
2033
|
+
if (reviewChoice === "channel") {
|
|
2034
|
+
step = "channel";
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (reviewChoice === "account") {
|
|
2038
|
+
step = "account";
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (reviewChoice === "routing") {
|
|
2042
|
+
step = "routing";
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
if (reviewChoice === "app") {
|
|
2046
|
+
return "back";
|
|
2047
|
+
}
|
|
2048
|
+
writeComposioConfig(configPath, resolved);
|
|
2049
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
2050
|
+
console.log(chalk.green(`✓ Slack connected account: ${connectedAccountId}`));
|
|
2051
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${targetName} --template ci-failing`));
|
|
2052
|
+
clack.outro("Composio Slack setup complete.");
|
|
2053
|
+
return "done";
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
async function runInteractiveComposioDiscordWebhookSetup(clack, opts, configPath, rawConfig, targetName = COMPOSIO_NOTIFIER) {
|
|
2057
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
2058
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
2059
|
+
const existingWebhookUrl = stringValue(opts.webhookUrl) ??
|
|
2060
|
+
stringValue(existing["webhookUrl"]) ??
|
|
2061
|
+
stringValue(process.env.DISCORD_WEBHOOK_URL);
|
|
2062
|
+
const explicitConnectedAccountId = stringValue(opts.connectedAccountId);
|
|
2063
|
+
const existingConnectedAccountId = stringValue(existing["connectedAccountId"]);
|
|
2064
|
+
const canReplace = await confirmComposioDiscordWebhookConflict(clack, targetName, existingPlugin, opts.force);
|
|
2065
|
+
if (!canReplace)
|
|
2066
|
+
return "back";
|
|
2067
|
+
const optionRoutingPreset = resolveComposioRoutingPreset(opts.routingPreset);
|
|
2068
|
+
let step = "api-key";
|
|
2069
|
+
let apiKey;
|
|
2070
|
+
let userId;
|
|
2071
|
+
let webhookUrl;
|
|
2072
|
+
let connectedAccountId;
|
|
2073
|
+
let routingPreset;
|
|
2074
|
+
let setupClient;
|
|
2075
|
+
while (true) {
|
|
2076
|
+
if (step === "api-key") {
|
|
2077
|
+
const result = await promptInteractiveComposioApiKey(clack, opts, existing);
|
|
2078
|
+
if (result === "back")
|
|
2079
|
+
return "back";
|
|
2080
|
+
apiKey = result;
|
|
2081
|
+
step = "user-id";
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (step === "user-id") {
|
|
2085
|
+
const result = await promptInteractiveComposioUserId(clack, opts, existing);
|
|
2086
|
+
if (result === "back") {
|
|
2087
|
+
step = "api-key";
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
userId = result;
|
|
2091
|
+
step = "webhook";
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (step === "webhook") {
|
|
2095
|
+
const result = await promptInteractiveDiscordWebhookUrl(clack, webhookUrl ?? existingWebhookUrl);
|
|
2096
|
+
if (result === "back") {
|
|
2097
|
+
step = "user-id";
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
webhookUrl = result;
|
|
2101
|
+
connectedAccountId = explicitConnectedAccountId;
|
|
2102
|
+
step = "account";
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
if (step === "account") {
|
|
2106
|
+
if (!apiKey || !userId || !webhookUrl) {
|
|
2107
|
+
step = "api-key";
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
setupClient ??= await loadComposioClient(apiKey.apiKey);
|
|
2111
|
+
const result = await promptInteractiveDiscordWebhookAccount(clack, setupClient, userId, webhookUrl, connectedAccountId ?? explicitConnectedAccountId ?? existingConnectedAccountId);
|
|
2112
|
+
if (result === "back") {
|
|
2113
|
+
step = "webhook";
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
connectedAccountId = result;
|
|
2117
|
+
step = "routing";
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (step === "routing") {
|
|
2121
|
+
const selection = optionRoutingPreset ??
|
|
2122
|
+
(await promptNotifierRoutingPreset(clack, rawConfig, targetName, "Composio Discord webhook", () => cancelInteractiveComposioSetup(clack)));
|
|
2123
|
+
if (selection === "back") {
|
|
2124
|
+
step = "webhook";
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
routingPreset = selection === "preserve" ? undefined : selection;
|
|
2128
|
+
step = "review";
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
if (!apiKey || !userId || !webhookUrl) {
|
|
2132
|
+
step = "api-key";
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
if (!connectedAccountId) {
|
|
2136
|
+
step = "account";
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
const resolved = {
|
|
2140
|
+
apiKey: apiKey.apiKey,
|
|
2141
|
+
shouldWriteApiKey: apiKey.shouldWriteApiKey,
|
|
2142
|
+
userId,
|
|
2143
|
+
mode: "webhook",
|
|
2144
|
+
targetName,
|
|
2145
|
+
webhookUrl,
|
|
2146
|
+
connectedAccountId,
|
|
2147
|
+
routingPreset,
|
|
2148
|
+
};
|
|
2149
|
+
const reviewChoice = await promptInteractiveComposioDiscordWebhookReview(clack, resolved, apiKey.sourceLabel);
|
|
2150
|
+
if (reviewChoice === "webhook") {
|
|
2151
|
+
step = "webhook";
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
if (reviewChoice === "account") {
|
|
2155
|
+
step = "account";
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
if (reviewChoice === "routing") {
|
|
2159
|
+
step = "routing";
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
if (reviewChoice === "app") {
|
|
2163
|
+
return "back";
|
|
2164
|
+
}
|
|
2165
|
+
writeComposioDiscordConfig(configPath, resolved);
|
|
2166
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
2167
|
+
console.log(chalk.green("✓ Discord webhook configured through Composio"));
|
|
2168
|
+
console.log(chalk.green(`✓ Discord webhook connected account: ${resolved.connectedAccountId}`));
|
|
2169
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${targetName} --template basic`));
|
|
2170
|
+
clack.outro("Composio Discord webhook setup complete.");
|
|
2171
|
+
return "done";
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
async function runInteractiveComposioDiscordBotSetup(clack, opts, configPath, rawConfig, targetName = COMPOSIO_NOTIFIER) {
|
|
2175
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
2176
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
2177
|
+
const existingIsBot = stringValue(existing["defaultApp"]) === "discord" && stringValue(existing["mode"]) === "bot";
|
|
2178
|
+
const existingChannelId = stringValue(opts.channelId) ?? (existingIsBot ? stringValue(existing["channelId"]) : undefined);
|
|
2179
|
+
const existingConnectedAccountId = existingIsBot
|
|
2180
|
+
? (stringValue(opts.connectedAccountId) ?? stringValue(existing["connectedAccountId"]))
|
|
2181
|
+
: stringValue(opts.connectedAccountId);
|
|
2182
|
+
const optionBotToken = stringValue(opts.botToken);
|
|
2183
|
+
const canReplace = await confirmComposioDiscordWebhookConflict(clack, targetName, existingPlugin, opts.force, "Composio Discord bot");
|
|
2184
|
+
if (!canReplace)
|
|
2185
|
+
return "back";
|
|
2186
|
+
const optionRoutingPreset = resolveComposioRoutingPreset(opts.routingPreset);
|
|
2187
|
+
let step = "api-key";
|
|
2188
|
+
let apiKey;
|
|
2189
|
+
let userId;
|
|
2190
|
+
let client;
|
|
2191
|
+
let channelId;
|
|
2192
|
+
let connectedAccountId;
|
|
2193
|
+
let routingPreset;
|
|
2194
|
+
while (true) {
|
|
2195
|
+
if (step === "api-key") {
|
|
2196
|
+
const result = await promptInteractiveComposioApiKey(clack, opts, existing);
|
|
2197
|
+
if (result === "back")
|
|
2198
|
+
return "back";
|
|
2199
|
+
apiKey = result;
|
|
2200
|
+
client = await loadComposioClient(apiKey.apiKey);
|
|
2201
|
+
step = "user-id";
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
if (step === "user-id") {
|
|
2205
|
+
const result = await promptInteractiveComposioUserId(clack, opts, existing);
|
|
2206
|
+
if (result === "back") {
|
|
2207
|
+
step = "api-key";
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
userId = result;
|
|
2211
|
+
step = "channel";
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (step === "channel") {
|
|
2215
|
+
const result = await promptInteractiveDiscordBotChannel(clack, channelId ?? existingChannelId);
|
|
2216
|
+
if (result === "back") {
|
|
2217
|
+
step = "user-id";
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2220
|
+
if (result !== existingChannelId || (channelId && channelId !== result)) {
|
|
2221
|
+
connectedAccountId = undefined;
|
|
2222
|
+
}
|
|
2223
|
+
channelId = result;
|
|
2224
|
+
step = "account";
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
if (step === "account") {
|
|
2228
|
+
if (!client || !userId || !channelId) {
|
|
2229
|
+
step = "api-key";
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
const result = await promptInteractiveDiscordBotAccount(clack, client, userId, channelId, connectedAccountId ??
|
|
2233
|
+
(channelId === existingChannelId ? existingConnectedAccountId : undefined), optionBotToken);
|
|
2234
|
+
if (result === "back") {
|
|
2235
|
+
step = "channel";
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
connectedAccountId = result;
|
|
2239
|
+
step = "routing";
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
if (step === "routing") {
|
|
2243
|
+
const selection = optionRoutingPreset ??
|
|
2244
|
+
(await promptNotifierRoutingPreset(clack, rawConfig, targetName, "Composio Discord bot", () => cancelInteractiveComposioSetup(clack)));
|
|
2245
|
+
if (selection === "back") {
|
|
2246
|
+
step = "account";
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
routingPreset = selection === "preserve" ? undefined : selection;
|
|
2250
|
+
step = "review";
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
if (!apiKey || !userId || !channelId || !connectedAccountId) {
|
|
2254
|
+
step = "api-key";
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
const resolved = {
|
|
2258
|
+
apiKey: apiKey.apiKey,
|
|
2259
|
+
shouldWriteApiKey: apiKey.shouldWriteApiKey,
|
|
2260
|
+
userId,
|
|
2261
|
+
mode: "bot",
|
|
2262
|
+
targetName,
|
|
2263
|
+
channelId,
|
|
2264
|
+
connectedAccountId,
|
|
2265
|
+
routingPreset,
|
|
2266
|
+
};
|
|
2267
|
+
const reviewChoice = await promptInteractiveComposioDiscordBotReview(clack, resolved, apiKey.sourceLabel);
|
|
2268
|
+
if (reviewChoice === "channel") {
|
|
2269
|
+
step = "channel";
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
if (reviewChoice === "account") {
|
|
2273
|
+
step = "account";
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (reviewChoice === "routing") {
|
|
2277
|
+
step = "routing";
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (reviewChoice === "app") {
|
|
2281
|
+
return "back";
|
|
2282
|
+
}
|
|
2283
|
+
writeComposioDiscordConfig(configPath, resolved);
|
|
2284
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
2285
|
+
console.log(chalk.green(`✓ Discord bot connected account: ${connectedAccountId}`));
|
|
2286
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${targetName} --template basic`));
|
|
2287
|
+
clack.outro("Composio Discord bot setup complete.");
|
|
2288
|
+
return "done";
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
async function runInteractiveComposioGmailSetup(clack, opts, configPath, rawConfig, targetName = COMPOSIO_NOTIFIER) {
|
|
2292
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
2293
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
2294
|
+
const existingIsGmail = stringValue(existing["defaultApp"]) === "gmail";
|
|
2295
|
+
const existingEmailTo = stringValue(opts.emailTo) ?? (existingIsGmail ? stringValue(existing["emailTo"]) : undefined);
|
|
2296
|
+
const existingConnectedAccountId = existingIsGmail
|
|
2297
|
+
? (stringValue(opts.connectedAccountId) ?? stringValue(existing["connectedAccountId"]))
|
|
2298
|
+
: stringValue(opts.connectedAccountId);
|
|
2299
|
+
const existingAuthConfigId = stringValue(opts.authConfigId) ??
|
|
2300
|
+
(existingIsGmail ? stringValue(existing["authConfigId"]) : undefined);
|
|
2301
|
+
const canReplace = await confirmComposioDiscordWebhookConflict(clack, targetName, existingPlugin, opts.force, "Composio Gmail");
|
|
2302
|
+
if (!canReplace)
|
|
2303
|
+
return "back";
|
|
2304
|
+
const optionRoutingPreset = resolveComposioRoutingPreset(opts.routingPreset);
|
|
2305
|
+
let step = "api-key";
|
|
2306
|
+
let apiKey;
|
|
2307
|
+
let userId;
|
|
2308
|
+
let client;
|
|
2309
|
+
let emailTo;
|
|
2310
|
+
let connectedAccountId;
|
|
2311
|
+
let routingPreset;
|
|
2312
|
+
while (true) {
|
|
2313
|
+
if (step === "api-key") {
|
|
2314
|
+
const result = await promptInteractiveComposioApiKey(clack, opts, existing);
|
|
2315
|
+
if (result === "back")
|
|
2316
|
+
return "back";
|
|
2317
|
+
apiKey = result;
|
|
2318
|
+
client = await loadComposioClient(apiKey.apiKey);
|
|
2319
|
+
step = "user-id";
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
if (step === "user-id") {
|
|
2323
|
+
const result = await promptInteractiveComposioUserId(clack, opts, existing);
|
|
2324
|
+
if (result === "back") {
|
|
2325
|
+
step = "api-key";
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
userId = result;
|
|
2329
|
+
step = "email";
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (step === "email") {
|
|
2333
|
+
const result = await promptInteractiveGmailEmail(clack, emailTo ?? existingEmailTo);
|
|
2334
|
+
if (result === "back") {
|
|
2335
|
+
step = "user-id";
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
emailTo = result;
|
|
2339
|
+
step = "account";
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (step === "account") {
|
|
2343
|
+
if (!client || !userId) {
|
|
2344
|
+
step = "api-key";
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
const result = await promptInteractiveGmailAccount(clack, client, userId, opts, connectedAccountId ?? existingConnectedAccountId, existingAuthConfigId);
|
|
2348
|
+
if (result === "back") {
|
|
2349
|
+
step = "email";
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
connectedAccountId = result;
|
|
2353
|
+
step = "routing";
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (step === "routing") {
|
|
2357
|
+
const selection = optionRoutingPreset ??
|
|
2358
|
+
(await promptNotifierRoutingPreset(clack, rawConfig, targetName, "Composio Gmail", () => cancelInteractiveComposioSetup(clack)));
|
|
2359
|
+
if (selection === "back") {
|
|
2360
|
+
step = "account";
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
routingPreset = selection === "preserve" ? undefined : selection;
|
|
2364
|
+
step = "review";
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
if (!apiKey || !userId || !emailTo || !connectedAccountId) {
|
|
2368
|
+
step = "api-key";
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
const resolved = {
|
|
2372
|
+
apiKey: apiKey.apiKey,
|
|
2373
|
+
shouldWriteApiKey: apiKey.shouldWriteApiKey,
|
|
2374
|
+
userId,
|
|
2375
|
+
emailTo,
|
|
2376
|
+
connectedAccountId,
|
|
2377
|
+
targetName,
|
|
2378
|
+
routingPreset,
|
|
2379
|
+
};
|
|
2380
|
+
const reviewChoice = await promptInteractiveComposioGmailReview(clack, resolved, apiKey.sourceLabel);
|
|
2381
|
+
if (reviewChoice === "email") {
|
|
2382
|
+
step = "email";
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (reviewChoice === "account") {
|
|
2386
|
+
step = "account";
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
if (reviewChoice === "routing") {
|
|
2390
|
+
step = "routing";
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
if (reviewChoice === "app") {
|
|
2394
|
+
return "back";
|
|
2395
|
+
}
|
|
2396
|
+
writeComposioMailConfig(configPath, resolved, targetName);
|
|
2397
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
2398
|
+
console.log(chalk.green(`✓ Gmail connected account: ${connectedAccountId}`));
|
|
2399
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${targetName} --template basic`));
|
|
2400
|
+
clack.outro("Composio Gmail setup complete.");
|
|
2401
|
+
return "done";
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
async function showComposioAppPlaceholder(clack, choice) {
|
|
2405
|
+
printComposioAppRequirements(choice);
|
|
2406
|
+
const next = await clack.select({
|
|
2407
|
+
message: "What do you want to do next?",
|
|
2408
|
+
options: [
|
|
2409
|
+
{ value: "back", label: "Back to app choices", hint: "Pick another Composio app" },
|
|
2410
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
2411
|
+
],
|
|
2412
|
+
});
|
|
2413
|
+
if (clack.isCancel(next) || next === "cancel") {
|
|
2414
|
+
cancelInteractiveComposioSetup(clack);
|
|
2415
|
+
}
|
|
2416
|
+
return "back";
|
|
2417
|
+
}
|
|
2418
|
+
async function runInteractiveComposioSetupHub(opts, configPath, rawConfig) {
|
|
2419
|
+
const clack = await import("@clack/prompts");
|
|
2420
|
+
let directChoice = getDirectComposioAppChoice(opts);
|
|
2421
|
+
clack.intro("AO Composio notifier setup");
|
|
2422
|
+
while (true) {
|
|
2423
|
+
const choice = directChoice ??
|
|
2424
|
+
(await clack.select({
|
|
2425
|
+
message: "Which Composio app do you want to configure?",
|
|
2426
|
+
options: [
|
|
2427
|
+
{ value: "slack", label: "Slack" },
|
|
2428
|
+
{
|
|
2429
|
+
value: "discord-webhook",
|
|
2430
|
+
label: "Discord webhook",
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
value: "discord-bot",
|
|
2434
|
+
label: "Discord bot",
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
value: "gmail",
|
|
2438
|
+
label: "Gmail",
|
|
2439
|
+
},
|
|
2440
|
+
{ value: "cancel", label: "Cancel setup", hint: "Do not change config" },
|
|
2441
|
+
],
|
|
2442
|
+
}));
|
|
2443
|
+
directChoice = undefined;
|
|
2444
|
+
if (clack.isCancel(choice) || choice === "cancel") {
|
|
2445
|
+
cancelInteractiveComposioSetup(clack);
|
|
2446
|
+
}
|
|
2447
|
+
if (choice === "slack") {
|
|
2448
|
+
const result = await runInteractiveComposioSlackSetup(clack, opts, configPath, rawConfig);
|
|
2449
|
+
if (result === "done")
|
|
2450
|
+
return;
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
if (choice === "discord-webhook") {
|
|
2454
|
+
const result = await runInteractiveComposioDiscordWebhookSetup(clack, opts, configPath, rawConfig);
|
|
2455
|
+
if (result === "done")
|
|
2456
|
+
return;
|
|
2457
|
+
continue;
|
|
2458
|
+
}
|
|
2459
|
+
if (choice === "discord-bot") {
|
|
2460
|
+
const result = await runInteractiveComposioDiscordBotSetup(clack, opts, configPath, rawConfig);
|
|
2461
|
+
if (result === "done")
|
|
2462
|
+
return;
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
if (choice === "gmail") {
|
|
2466
|
+
const result = await runInteractiveComposioGmailSetup(clack, opts, configPath, rawConfig);
|
|
2467
|
+
if (result === "done")
|
|
2468
|
+
return;
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
await showComposioAppPlaceholder(clack, choice);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
function parseDiscordWebhookUrl(webhookUrl) {
|
|
2475
|
+
let parsed;
|
|
2476
|
+
try {
|
|
2477
|
+
parsed = new URL(webhookUrl);
|
|
2478
|
+
}
|
|
2479
|
+
catch {
|
|
2480
|
+
throw new ComposioSetupError("Invalid Discord webhook URL. Expected https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN.");
|
|
2481
|
+
}
|
|
2482
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
2483
|
+
const webhookIndex = segments.findIndex((segment) => segment === "webhooks");
|
|
2484
|
+
const webhookId = webhookIndex >= 0 ? segments[webhookIndex + 1] : undefined;
|
|
2485
|
+
const webhookToken = webhookIndex >= 0 ? segments[webhookIndex + 2] : undefined;
|
|
2486
|
+
if (!webhookId || !webhookToken) {
|
|
2487
|
+
throw new ComposioSetupError("Invalid Discord webhook URL. Expected https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN.");
|
|
2488
|
+
}
|
|
2489
|
+
return {
|
|
2490
|
+
webhookId: decodeURIComponent(webhookId),
|
|
2491
|
+
webhookToken: decodeURIComponent(webhookToken),
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
async function createDiscordBearerAuthConfig(client, token, name) {
|
|
2495
|
+
if (!client.authConfigs?.create) {
|
|
2496
|
+
throw new ComposioSetupError("Composio SDK client does not expose authConfigs.create(); pass --connected-account-id.");
|
|
2497
|
+
}
|
|
2498
|
+
let authConfig;
|
|
2499
|
+
try {
|
|
2500
|
+
authConfig = await client.authConfigs.create(DISCORD_TOOLKIT, {
|
|
2501
|
+
type: "use_custom_auth",
|
|
2502
|
+
name,
|
|
2503
|
+
authScheme: "BEARER_TOKEN",
|
|
2504
|
+
credentials: { token },
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
catch (error) {
|
|
2508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2509
|
+
throw new ComposioSetupError(`Could not create a Composio Discord auth config: ${message}`);
|
|
2510
|
+
}
|
|
2511
|
+
const authConfigId = isRecord(authConfig) ? stringValue(authConfig["id"]) : undefined;
|
|
2512
|
+
if (!authConfigId) {
|
|
2513
|
+
throw new ComposioSetupError("Could not create a Composio Discord auth config.");
|
|
2514
|
+
}
|
|
2515
|
+
return authConfigId;
|
|
2516
|
+
}
|
|
2517
|
+
async function createDiscordBearerConnectedAccountWithAuthConfig(client, userId, authConfigId, token) {
|
|
2518
|
+
if (!client.connectedAccounts.initiate) {
|
|
2519
|
+
throw new ComposioSetupError("Composio SDK client does not expose connectedAccounts.initiate(); pass --connected-account-id.");
|
|
2520
|
+
}
|
|
2521
|
+
let request;
|
|
2522
|
+
try {
|
|
2523
|
+
request = toConnectionRequest(await client.connectedAccounts.initiate(userId, authConfigId, {
|
|
2524
|
+
allowMultiple: true,
|
|
2525
|
+
config: {
|
|
2526
|
+
authScheme: "BEARER_TOKEN",
|
|
2527
|
+
val: {
|
|
2528
|
+
status: "ACTIVE",
|
|
2529
|
+
token,
|
|
2530
|
+
},
|
|
2531
|
+
},
|
|
2532
|
+
}));
|
|
2533
|
+
}
|
|
2534
|
+
catch (error) {
|
|
2535
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2536
|
+
throw new ComposioSetupError(`Could not create a Composio Discord connected account: ${message}`);
|
|
2537
|
+
}
|
|
2538
|
+
if (!request.id) {
|
|
2539
|
+
throw new ComposioSetupError("Could not create a Composio Discord connected account.");
|
|
2540
|
+
}
|
|
2541
|
+
return request.id;
|
|
2542
|
+
}
|
|
2543
|
+
async function createDiscordBearerConnectedAccount(client, userId, token, name) {
|
|
2544
|
+
const authConfigId = await createDiscordBearerAuthConfig(client, token, name);
|
|
2545
|
+
return createDiscordBearerConnectedAccountWithAuthConfig(client, userId, authConfigId, token);
|
|
2546
|
+
}
|
|
2547
|
+
async function resolveDiscordWebhookConnectedAccountId(client, userId, webhookUrl, connectedAccountId, explicitConnectedAccountId = false) {
|
|
2548
|
+
if (connectedAccountId) {
|
|
2549
|
+
try {
|
|
2550
|
+
const account = await verifyConnectedAccountForToolkit(client, userId, connectedAccountId, DISCORD_TOOLKIT, "Discord webhook");
|
|
2551
|
+
return account.id;
|
|
2552
|
+
}
|
|
2553
|
+
catch (error) {
|
|
2554
|
+
if (explicitConnectedAccountId)
|
|
2555
|
+
throw error;
|
|
2556
|
+
console.log(chalk.yellow(error instanceof Error ? error.message : String(error)));
|
|
2557
|
+
console.log(chalk.dim("Creating a new Discord webhook connected account for this userId."));
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
const { webhookToken } = parseDiscordWebhookUrl(webhookUrl);
|
|
2561
|
+
return createDiscordBearerConnectedAccount(client, userId, webhookToken, "Discord Webhook Auth Config");
|
|
2562
|
+
}
|
|
2563
|
+
async function validateDiscordBotChannelAccess(botToken, channelId) {
|
|
2564
|
+
const res = await fetch(`https://discord.com/api/v10/channels/${encodeURIComponent(channelId)}`, {
|
|
2565
|
+
headers: {
|
|
2566
|
+
Authorization: `Bot ${botToken}`,
|
|
2567
|
+
},
|
|
2568
|
+
});
|
|
2569
|
+
if (res.ok)
|
|
2570
|
+
return;
|
|
2571
|
+
let message = `${res.status} ${res.statusText}`.trim();
|
|
2572
|
+
try {
|
|
2573
|
+
const body = (await res.json());
|
|
2574
|
+
if (isRecord(body) && stringValue(body["message"])) {
|
|
2575
|
+
message = `${res.status} ${stringValue(body["message"])}`;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
catch {
|
|
2579
|
+
// Keep the HTTP status message.
|
|
2580
|
+
}
|
|
2581
|
+
if (res.status === 401) {
|
|
2582
|
+
throw new ComposioSetupError(`Discord bot token is invalid (${message}).`);
|
|
2583
|
+
}
|
|
2584
|
+
if (res.status === 403) {
|
|
2585
|
+
throw new ComposioSetupError(`Discord bot cannot access channel ${channelId} (${message}). Invite the bot to the server and grant View Channel + Send Messages.`);
|
|
2586
|
+
}
|
|
2587
|
+
throw new ComposioSetupError(`Could not validate Discord channel ${channelId}: ${message}.`);
|
|
2588
|
+
}
|
|
2589
|
+
function writeComposioConfig(configPath, resolved) {
|
|
2590
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
2591
|
+
const doc = parseDocument(rawYaml);
|
|
2592
|
+
const rawConfig = doc.toJS() ?? {};
|
|
2593
|
+
const notifiers = isRecord(rawConfig["notifiers"]) ? rawConfig["notifiers"] : {};
|
|
2594
|
+
const targetName = resolved.targetName ?? COMPOSIO_NOTIFIER;
|
|
2595
|
+
const existing = isRecord(notifiers[targetName]) ? notifiers[targetName] : {};
|
|
2596
|
+
const channel = channelConfig(resolved.channel);
|
|
2597
|
+
const composioConfig = {
|
|
2598
|
+
...existing,
|
|
2599
|
+
plugin: "composio",
|
|
2600
|
+
defaultApp: "slack",
|
|
2601
|
+
userId: resolved.userId,
|
|
2602
|
+
...channel,
|
|
2603
|
+
};
|
|
2604
|
+
if ("channelId" in channel)
|
|
2605
|
+
delete composioConfig["channelName"];
|
|
2606
|
+
else if ("channelName" in channel)
|
|
2607
|
+
delete composioConfig["channelId"];
|
|
2608
|
+
else {
|
|
2609
|
+
delete composioConfig["channelId"];
|
|
2610
|
+
delete composioConfig["channelName"];
|
|
2611
|
+
}
|
|
2612
|
+
if (resolved.connectedAccountId) {
|
|
2613
|
+
composioConfig["connectedAccountId"] = resolved.connectedAccountId;
|
|
2614
|
+
}
|
|
2615
|
+
else {
|
|
2616
|
+
delete composioConfig["connectedAccountId"];
|
|
2617
|
+
}
|
|
2618
|
+
if (resolved.shouldWriteApiKey) {
|
|
2619
|
+
composioConfig["composioApiKey"] = resolved.apiKey;
|
|
2620
|
+
}
|
|
2621
|
+
delete composioConfig["mode"];
|
|
2622
|
+
delete composioConfig["webhookUrl"];
|
|
2623
|
+
delete composioConfig["emailTo"];
|
|
2624
|
+
delete composioConfig["entityId"];
|
|
2625
|
+
delete composioConfig["botToken"];
|
|
2626
|
+
delete composioConfig["authConfigId"];
|
|
2627
|
+
delete composioConfig["toolVersion"];
|
|
2628
|
+
notifiers[targetName] = composioConfig;
|
|
2629
|
+
rawConfig["notifiers"] = notifiers;
|
|
2630
|
+
if (resolved.routingPreset) {
|
|
2631
|
+
ensureNotifierDefault(rawConfig, targetName);
|
|
2632
|
+
}
|
|
2633
|
+
applyNotifierRoutingPreset(rawConfig, targetName, resolved.routingPreset);
|
|
2634
|
+
if (!isCanonicalGlobalConfigPath(configPath)) {
|
|
2635
|
+
const currentSchema = doc.get("$schema");
|
|
2636
|
+
if (!(typeof currentSchema === "string" && currentSchema.trim().length > 0)) {
|
|
2637
|
+
doc.set("$schema", CONFIG_SCHEMA_URL);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
doc.setIn(["notifiers"], rawConfig["notifiers"]);
|
|
2641
|
+
doc.setIn(["defaults"], rawConfig["defaults"]);
|
|
2642
|
+
if (rawConfig["notificationRouting"] !== undefined) {
|
|
2643
|
+
doc.setIn(["notificationRouting"], rawConfig["notificationRouting"]);
|
|
2644
|
+
}
|
|
2645
|
+
writeFileSync(configPath, doc.toString({ indent: 2 }));
|
|
2646
|
+
}
|
|
2647
|
+
function writeComposioDiscordConfig(configPath, resolved) {
|
|
2648
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
2649
|
+
const doc = parseDocument(rawYaml);
|
|
2650
|
+
const rawConfig = doc.toJS() ?? {};
|
|
2651
|
+
const notifiers = isRecord(rawConfig["notifiers"]) ? rawConfig["notifiers"] : {};
|
|
2652
|
+
const existingRaw = notifiers[resolved.targetName];
|
|
2653
|
+
const existing = isRecord(existingRaw) ? existingRaw : {};
|
|
2654
|
+
const composioConfig = {
|
|
2655
|
+
...existing,
|
|
2656
|
+
plugin: "composio",
|
|
2657
|
+
defaultApp: "discord",
|
|
2658
|
+
mode: resolved.mode,
|
|
2659
|
+
userId: resolved.userId,
|
|
2660
|
+
toolVersion: DISCORD_TOOL_VERSION,
|
|
2661
|
+
};
|
|
2662
|
+
if (resolved.mode === "webhook") {
|
|
2663
|
+
composioConfig["webhookUrl"] = resolved.webhookUrl;
|
|
2664
|
+
delete composioConfig["channelId"];
|
|
2665
|
+
delete composioConfig["channelName"];
|
|
2666
|
+
delete composioConfig["emailTo"];
|
|
2667
|
+
if (resolved.connectedAccountId) {
|
|
2668
|
+
composioConfig["connectedAccountId"] = resolved.connectedAccountId;
|
|
2669
|
+
}
|
|
2670
|
+
else {
|
|
2671
|
+
delete composioConfig["connectedAccountId"];
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
else {
|
|
2675
|
+
composioConfig["channelId"] = resolved.channelId;
|
|
2676
|
+
delete composioConfig["webhookUrl"];
|
|
2677
|
+
delete composioConfig["channelName"];
|
|
2678
|
+
delete composioConfig["emailTo"];
|
|
2679
|
+
if (resolved.connectedAccountId) {
|
|
2680
|
+
composioConfig["connectedAccountId"] = resolved.connectedAccountId;
|
|
2681
|
+
}
|
|
2682
|
+
else {
|
|
2683
|
+
delete composioConfig["connectedAccountId"];
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if (resolved.shouldWriteApiKey) {
|
|
2687
|
+
composioConfig["composioApiKey"] = resolved.apiKey;
|
|
2688
|
+
}
|
|
2689
|
+
delete composioConfig["entityId"];
|
|
2690
|
+
delete composioConfig["botToken"];
|
|
2691
|
+
delete composioConfig["authConfigId"];
|
|
2692
|
+
notifiers[resolved.targetName] = composioConfig;
|
|
2693
|
+
rawConfig["notifiers"] = notifiers;
|
|
2694
|
+
if (resolved.routingPreset) {
|
|
2695
|
+
ensureNotifierDefault(rawConfig, resolved.targetName);
|
|
2696
|
+
}
|
|
2697
|
+
applyNotifierRoutingPreset(rawConfig, resolved.targetName, resolved.routingPreset);
|
|
2698
|
+
if (!isCanonicalGlobalConfigPath(configPath)) {
|
|
2699
|
+
const currentSchema = doc.get("$schema");
|
|
2700
|
+
if (!(typeof currentSchema === "string" && currentSchema.trim().length > 0)) {
|
|
2701
|
+
doc.set("$schema", CONFIG_SCHEMA_URL);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
doc.setIn(["notifiers"], rawConfig["notifiers"]);
|
|
2705
|
+
doc.setIn(["defaults"], rawConfig["defaults"]);
|
|
2706
|
+
if (rawConfig["notificationRouting"] !== undefined) {
|
|
2707
|
+
doc.setIn(["notificationRouting"], rawConfig["notificationRouting"]);
|
|
2708
|
+
}
|
|
2709
|
+
writeFileSync(configPath, doc.toString({ indent: 2 }));
|
|
2710
|
+
}
|
|
2711
|
+
function writeComposioMailConfig(configPath, resolved, targetName = COMPOSIO_MAIL_NOTIFIER) {
|
|
2712
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
2713
|
+
const doc = parseDocument(rawYaml);
|
|
2714
|
+
const rawConfig = doc.toJS() ?? {};
|
|
2715
|
+
const notifiers = isRecord(rawConfig["notifiers"]) ? rawConfig["notifiers"] : {};
|
|
2716
|
+
const existingRaw = notifiers[targetName];
|
|
2717
|
+
const existing = isRecord(existingRaw) ? existingRaw : {};
|
|
2718
|
+
const composioConfig = {
|
|
2719
|
+
...existing,
|
|
2720
|
+
plugin: "composio",
|
|
2721
|
+
defaultApp: "gmail",
|
|
2722
|
+
userId: resolved.userId,
|
|
2723
|
+
emailTo: resolved.emailTo,
|
|
2724
|
+
toolVersion: GMAIL_TOOL_VERSION,
|
|
2725
|
+
};
|
|
2726
|
+
if (resolved.connectedAccountId) {
|
|
2727
|
+
composioConfig["connectedAccountId"] = resolved.connectedAccountId;
|
|
2728
|
+
}
|
|
2729
|
+
else {
|
|
2730
|
+
delete composioConfig["connectedAccountId"];
|
|
2731
|
+
}
|
|
2732
|
+
if (resolved.shouldWriteApiKey) {
|
|
2733
|
+
composioConfig["composioApiKey"] = resolved.apiKey;
|
|
2734
|
+
}
|
|
2735
|
+
delete composioConfig["entityId"];
|
|
2736
|
+
delete composioConfig["channelId"];
|
|
2737
|
+
delete composioConfig["channelName"];
|
|
2738
|
+
delete composioConfig["webhookUrl"];
|
|
2739
|
+
delete composioConfig["mode"];
|
|
2740
|
+
delete composioConfig["authConfigId"];
|
|
2741
|
+
delete composioConfig["botToken"];
|
|
2742
|
+
notifiers[targetName] = composioConfig;
|
|
2743
|
+
rawConfig["notifiers"] = notifiers;
|
|
2744
|
+
if (resolved.routingPreset) {
|
|
2745
|
+
ensureNotifierDefault(rawConfig, targetName);
|
|
2746
|
+
}
|
|
2747
|
+
applyNotifierRoutingPreset(rawConfig, targetName, resolved.routingPreset);
|
|
2748
|
+
if (!isCanonicalGlobalConfigPath(configPath)) {
|
|
2749
|
+
const currentSchema = doc.get("$schema");
|
|
2750
|
+
if (!(typeof currentSchema === "string" && currentSchema.trim().length > 0)) {
|
|
2751
|
+
doc.set("$schema", CONFIG_SCHEMA_URL);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
doc.setIn(["notifiers"], rawConfig["notifiers"]);
|
|
2755
|
+
doc.setIn(["defaults"], rawConfig["defaults"]);
|
|
2756
|
+
if (rawConfig["notificationRouting"] !== undefined) {
|
|
2757
|
+
doc.setIn(["notificationRouting"], rawConfig["notificationRouting"]);
|
|
2758
|
+
}
|
|
2759
|
+
writeFileSync(configPath, doc.toString({ indent: 2 }));
|
|
2760
|
+
}
|
|
2761
|
+
function printStatus(resolved, accounts, targetName = COMPOSIO_NOTIFIER, rawConfig) {
|
|
2762
|
+
console.log(chalk.bold(`AO Composio notifier (${targetName})`));
|
|
2763
|
+
console.log(" api key: configured");
|
|
2764
|
+
console.log(` userId: ${resolved.userId}`);
|
|
2765
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "not configured"}`);
|
|
2766
|
+
if (rawConfig)
|
|
2767
|
+
console.log(` routing: ${getNotifierRoutingState(rawConfig, targetName).label}`);
|
|
2768
|
+
console.log(` active Slack accounts: ${accounts.length}`);
|
|
2769
|
+
for (const account of accounts) {
|
|
2770
|
+
console.log(` - ${account.id}${account.alias ? ` (${account.alias})` : ""}`);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function printDiscordStatus(resolved, rawConfig) {
|
|
2774
|
+
console.log(chalk.bold(`AO Composio Discord notifier (${resolved.targetName})`));
|
|
2775
|
+
console.log(" api key: configured");
|
|
2776
|
+
console.log(` mode: ${resolved.mode}`);
|
|
2777
|
+
console.log(` userId: ${resolved.userId}`);
|
|
2778
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "not configured"}`);
|
|
2779
|
+
if (rawConfig) {
|
|
2780
|
+
console.log(` routing: ${getNotifierRoutingState(rawConfig, resolved.targetName).label}`);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
function printMailStatus(resolved, accounts, rawConfig, targetName = COMPOSIO_MAIL_NOTIFIER) {
|
|
2784
|
+
console.log(chalk.bold(`AO Composio mail notifier (${targetName})`));
|
|
2785
|
+
console.log(" api key: configured");
|
|
2786
|
+
console.log(` userId: ${resolved.userId}`);
|
|
2787
|
+
console.log(` emailTo: ${resolved.emailTo ?? "not configured"}`);
|
|
2788
|
+
console.log(` connectedAccountId: ${resolved.connectedAccountId ?? "not configured"}`);
|
|
2789
|
+
if (rawConfig)
|
|
2790
|
+
console.log(` routing: ${getNotifierRoutingState(rawConfig, targetName).label}`);
|
|
2791
|
+
console.log(` active Gmail accounts: ${accounts.length}`);
|
|
2792
|
+
for (const account of accounts) {
|
|
2793
|
+
console.log(` - ${account.id}${account.alias ? ` (${account.alias})` : ""}`);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
async function resolveSetup(opts, rawConfig, nonInteractive, targetName = COMPOSIO_NOTIFIER) {
|
|
2797
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
2798
|
+
const { apiKey, shouldWriteApiKey } = resolveApiKey(opts, existing);
|
|
2799
|
+
if (!apiKey) {
|
|
2800
|
+
throw new ComposioSetupError("No Composio API key found. Pass --api-key or set COMPOSIO_API_KEY.");
|
|
2801
|
+
}
|
|
2802
|
+
const userId = resolveUserId(opts, existing);
|
|
2803
|
+
const client = await loadComposioClient(apiKey);
|
|
2804
|
+
const explicitConnectedAccountId = stringValue(opts.connectedAccountId) ?? stringValue(existing["connectedAccountId"]);
|
|
2805
|
+
const routingPreset = resolveComposioRoutingPreset(opts.routingPreset) ?? "all";
|
|
2806
|
+
if (opts.status) {
|
|
2807
|
+
const accounts = await listActiveSlackAccounts(client, userId);
|
|
2808
|
+
printStatus({ apiKey, userId, connectedAccountId: explicitConnectedAccountId }, accounts, targetName, rawConfig);
|
|
2809
|
+
return {
|
|
2810
|
+
apiKey,
|
|
2811
|
+
shouldWriteApiKey,
|
|
2812
|
+
userId,
|
|
2813
|
+
targetName,
|
|
2814
|
+
channel: stringValue(opts.channel),
|
|
2815
|
+
connectedAccountId: explicitConnectedAccountId,
|
|
2816
|
+
routingPreset,
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
if (explicitConnectedAccountId) {
|
|
2820
|
+
const account = await verifyConnectedAccount(client, userId, explicitConnectedAccountId);
|
|
2821
|
+
return {
|
|
2822
|
+
apiKey,
|
|
2823
|
+
shouldWriteApiKey,
|
|
2824
|
+
userId,
|
|
2825
|
+
targetName,
|
|
2826
|
+
channel: stringValue(opts.channel),
|
|
2827
|
+
connectedAccountId: account.id,
|
|
2828
|
+
routingPreset,
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
const accounts = await listActiveSlackAccounts(client, userId);
|
|
2832
|
+
if (accounts.length > 0) {
|
|
2833
|
+
const account = await chooseAccount(accounts, nonInteractive);
|
|
2834
|
+
return {
|
|
2835
|
+
apiKey,
|
|
2836
|
+
shouldWriteApiKey,
|
|
2837
|
+
userId,
|
|
2838
|
+
targetName,
|
|
2839
|
+
channel: stringValue(opts.channel),
|
|
2840
|
+
connectedAccountId: account.id,
|
|
2841
|
+
routingPreset,
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
const connection = await createConnectionRequest(client, userId, parseWaitMs(opts.waitMs));
|
|
2845
|
+
return {
|
|
2846
|
+
apiKey,
|
|
2847
|
+
shouldWriteApiKey,
|
|
2848
|
+
userId,
|
|
2849
|
+
targetName,
|
|
2850
|
+
channel: stringValue(opts.channel),
|
|
2851
|
+
connectedAccountId: connection.account?.id,
|
|
2852
|
+
connectionUrl: connection.url,
|
|
2853
|
+
routingPreset,
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
export async function runComposioSetupAction(opts) {
|
|
2857
|
+
const nonInteractive = opts.nonInteractive || !process.stdin.isTTY;
|
|
2858
|
+
let configPath;
|
|
2859
|
+
try {
|
|
2860
|
+
configPath = findConfigFile() ?? undefined;
|
|
2861
|
+
}
|
|
2862
|
+
catch {
|
|
2863
|
+
configPath = undefined;
|
|
2864
|
+
}
|
|
2865
|
+
if (!configPath) {
|
|
2866
|
+
throw new ComposioSetupError("No agent-orchestrator.yaml found. Run 'athene start' first to create one.");
|
|
2867
|
+
}
|
|
2868
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
2869
|
+
const doc = parseDocument(rawYaml);
|
|
2870
|
+
const rawConfig = doc.toJS() ?? {};
|
|
2871
|
+
const existing = getExistingComposioConfig(rawConfig);
|
|
2872
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
2873
|
+
const directChoice = getDirectComposioAppChoice(opts);
|
|
2874
|
+
if (directChoice && nonInteractive) {
|
|
2875
|
+
throw new ComposioSetupError("Composio app flags require interactive setup. Use the dedicated setup command with --non-interactive for scriptable setup.");
|
|
2876
|
+
}
|
|
2877
|
+
if (shouldUseInteractiveComposioHub(opts, nonInteractive)) {
|
|
2878
|
+
await runInteractiveComposioSetupHub(opts, configPath, rawConfig);
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
if (existingPlugin && existingPlugin !== "composio" && !opts.force) {
|
|
2882
|
+
throw new ComposioSetupError(`notifiers.composio already uses plugin "${existingPlugin}". Re-run with --force to replace it.`);
|
|
2883
|
+
}
|
|
2884
|
+
const resolved = await resolveSetup(opts, rawConfig, nonInteractive);
|
|
2885
|
+
if (opts.status)
|
|
2886
|
+
return;
|
|
2887
|
+
if (resolved.connectionUrl && !resolved.connectedAccountId) {
|
|
2888
|
+
console.log(chalk.yellow("Slack connection did not complete yet. Open the connect URL above, finish the Composio flow, then rerun `athene setup composio`."));
|
|
2889
|
+
console.log(chalk.dim("No config was changed."));
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
writeComposioConfig(configPath, resolved);
|
|
2893
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
2894
|
+
if (resolved.connectedAccountId) {
|
|
2895
|
+
console.log(chalk.green(`✓ Slack connected account: ${resolved.connectedAccountId}`));
|
|
2896
|
+
}
|
|
2897
|
+
console.log(chalk.dim("Test it with: athene notify test --to composio --template ci-failing"));
|
|
2898
|
+
}
|
|
2899
|
+
export async function runComposioSlackSetupAction(opts) {
|
|
2900
|
+
const nonInteractive = opts.nonInteractive || !process.stdin.isTTY;
|
|
2901
|
+
let configPath;
|
|
2902
|
+
try {
|
|
2903
|
+
configPath = findConfigFile() ?? undefined;
|
|
2904
|
+
}
|
|
2905
|
+
catch {
|
|
2906
|
+
configPath = undefined;
|
|
2907
|
+
}
|
|
2908
|
+
if (!configPath) {
|
|
2909
|
+
throw new ComposioSetupError("No agent-orchestrator.yaml found. Run 'athene start' first to create one.");
|
|
2910
|
+
}
|
|
2911
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
2912
|
+
const rawConfig = parseDocument(rawYaml).toJS() ?? {};
|
|
2913
|
+
if (shouldUseInteractiveDedicatedSetup(opts, nonInteractive)) {
|
|
2914
|
+
const clack = await import("@clack/prompts");
|
|
2915
|
+
clack.intro("AO Composio Slack setup");
|
|
2916
|
+
await runInteractiveComposioSlackSetup(clack, opts, configPath, rawConfig, COMPOSIO_SLACK_NOTIFIER);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
const existing = getExistingNotifierConfig(rawConfig, COMPOSIO_SLACK_NOTIFIER);
|
|
2920
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
2921
|
+
if (existingPlugin && existingPlugin !== "composio" && !opts.force) {
|
|
2922
|
+
throw new ComposioSetupError(`notifiers.${COMPOSIO_SLACK_NOTIFIER} already uses plugin "${existingPlugin}". Re-run with --force to replace it.`);
|
|
2923
|
+
}
|
|
2924
|
+
const resolved = await resolveSetup(opts, rawConfig, nonInteractive, COMPOSIO_SLACK_NOTIFIER);
|
|
2925
|
+
if (opts.status)
|
|
2926
|
+
return;
|
|
2927
|
+
if (resolved.connectionUrl && !resolved.connectedAccountId) {
|
|
2928
|
+
console.log(chalk.yellow("Slack connection did not complete yet. Open the connect URL above, finish the Composio flow, then rerun `athene setup composio-slack`."));
|
|
2929
|
+
console.log(chalk.dim("No config was changed."));
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
writeComposioConfig(configPath, resolved);
|
|
2933
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
2934
|
+
if (resolved.connectedAccountId) {
|
|
2935
|
+
console.log(chalk.green(`✓ Slack connected account: ${resolved.connectedAccountId}`));
|
|
2936
|
+
}
|
|
2937
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${COMPOSIO_SLACK_NOTIFIER} --template ci-failing`));
|
|
2938
|
+
}
|
|
2939
|
+
async function resolveDiscordWebhookSetup(opts, rawConfig) {
|
|
2940
|
+
const targetName = COMPOSIO_DISCORD_WEBHOOK_NOTIFIER;
|
|
2941
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
2942
|
+
const { apiKey, shouldWriteApiKey } = resolveApiKey(opts, existing);
|
|
2943
|
+
if (!apiKey) {
|
|
2944
|
+
throw new ComposioSetupError("No Composio API key found. Pass --api-key or set COMPOSIO_API_KEY.");
|
|
2945
|
+
}
|
|
2946
|
+
const userId = resolveUserId(opts, existing);
|
|
2947
|
+
const client = await loadComposioClient(apiKey);
|
|
2948
|
+
const routingPreset = resolveComposioRoutingPreset(opts.routingPreset) ?? "all";
|
|
2949
|
+
const explicitConnectedAccountId = stringValue(opts.connectedAccountId);
|
|
2950
|
+
const existingConnectedAccountId = stringValue(existing["connectedAccountId"]);
|
|
2951
|
+
const connectedAccountId = explicitConnectedAccountId ?? existingConnectedAccountId;
|
|
2952
|
+
const webhookUrl = stringValue(opts.webhookUrl) ??
|
|
2953
|
+
stringValue(process.env.DISCORD_WEBHOOK_URL) ??
|
|
2954
|
+
stringValue(existing["webhookUrl"]);
|
|
2955
|
+
if (opts.status) {
|
|
2956
|
+
printDiscordStatus({ targetName, mode: "webhook", userId, connectedAccountId }, rawConfig);
|
|
2957
|
+
return {
|
|
2958
|
+
apiKey,
|
|
2959
|
+
shouldWriteApiKey,
|
|
2960
|
+
userId,
|
|
2961
|
+
mode: "webhook",
|
|
2962
|
+
targetName,
|
|
2963
|
+
webhookUrl,
|
|
2964
|
+
connectedAccountId,
|
|
2965
|
+
routingPreset,
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
if (!webhookUrl) {
|
|
2969
|
+
throw new ComposioSetupError("No Discord webhook URL found. Pass --webhook-url or set DISCORD_WEBHOOK_URL.");
|
|
2970
|
+
}
|
|
2971
|
+
parseDiscordWebhookUrl(webhookUrl);
|
|
2972
|
+
const resolvedConnectedAccountId = await resolveDiscordWebhookConnectedAccountId(client, userId, webhookUrl, connectedAccountId, Boolean(explicitConnectedAccountId));
|
|
2973
|
+
return {
|
|
2974
|
+
apiKey,
|
|
2975
|
+
shouldWriteApiKey,
|
|
2976
|
+
userId,
|
|
2977
|
+
mode: "webhook",
|
|
2978
|
+
targetName,
|
|
2979
|
+
webhookUrl,
|
|
2980
|
+
connectedAccountId: resolvedConnectedAccountId,
|
|
2981
|
+
routingPreset,
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
async function resolveDiscordBotSetup(opts, rawConfig) {
|
|
2985
|
+
const targetName = COMPOSIO_DISCORD_BOT_NOTIFIER;
|
|
2986
|
+
const existing = getExistingNotifierConfig(rawConfig, targetName);
|
|
2987
|
+
const { apiKey, shouldWriteApiKey } = resolveApiKey(opts, existing);
|
|
2988
|
+
if (!apiKey) {
|
|
2989
|
+
throw new ComposioSetupError("No Composio API key found. Pass --api-key or set COMPOSIO_API_KEY.");
|
|
2990
|
+
}
|
|
2991
|
+
const userId = resolveUserId(opts, existing);
|
|
2992
|
+
const client = await loadComposioClient(apiKey);
|
|
2993
|
+
const routingPreset = resolveComposioRoutingPreset(opts.routingPreset) ?? "all";
|
|
2994
|
+
const connectedAccountId = stringValue(opts.connectedAccountId) ?? stringValue(existing["connectedAccountId"]);
|
|
2995
|
+
const channelId = stringValue(opts.channelId) ?? stringValue(existing["channelId"]);
|
|
2996
|
+
const botToken = stringValue(opts.botToken) ?? stringValue(process.env.DISCORD_BOT_TOKEN);
|
|
2997
|
+
if (opts.status) {
|
|
2998
|
+
printDiscordStatus({ targetName, mode: "bot", userId, connectedAccountId }, rawConfig);
|
|
2999
|
+
return {
|
|
3000
|
+
apiKey,
|
|
3001
|
+
shouldWriteApiKey,
|
|
3002
|
+
userId,
|
|
3003
|
+
mode: "bot",
|
|
3004
|
+
targetName,
|
|
3005
|
+
channelId,
|
|
3006
|
+
connectedAccountId,
|
|
3007
|
+
routingPreset,
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
if (!channelId) {
|
|
3011
|
+
throw new ComposioSetupError("No Discord channel id found. Pass --channel-id.");
|
|
3012
|
+
}
|
|
3013
|
+
if (connectedAccountId) {
|
|
3014
|
+
const account = await verifyConnectedAccountForToolkit(client, userId, connectedAccountId, DISCORD_TOOLKIT, "Discord Bot");
|
|
3015
|
+
return {
|
|
3016
|
+
apiKey,
|
|
3017
|
+
shouldWriteApiKey,
|
|
3018
|
+
userId,
|
|
3019
|
+
mode: "bot",
|
|
3020
|
+
targetName,
|
|
3021
|
+
channelId,
|
|
3022
|
+
connectedAccountId: account.id,
|
|
3023
|
+
routingPreset,
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
if (!botToken) {
|
|
3027
|
+
throw new ComposioSetupError("No Discord bot token found. Pass --bot-token or set DISCORD_BOT_TOKEN.");
|
|
3028
|
+
}
|
|
3029
|
+
await validateDiscordBotChannelAccess(botToken, channelId);
|
|
3030
|
+
return {
|
|
3031
|
+
apiKey,
|
|
3032
|
+
shouldWriteApiKey,
|
|
3033
|
+
userId,
|
|
3034
|
+
mode: "bot",
|
|
3035
|
+
targetName,
|
|
3036
|
+
channelId,
|
|
3037
|
+
connectedAccountId: await createDiscordBearerConnectedAccount(client, userId, botToken, "Discord Bot Auth Config"),
|
|
3038
|
+
routingPreset,
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
async function resolveMailSetup(opts, rawConfig, nonInteractive) {
|
|
3042
|
+
const existing = getExistingNotifierConfig(rawConfig, COMPOSIO_MAIL_NOTIFIER);
|
|
3043
|
+
const { apiKey, shouldWriteApiKey } = resolveApiKey(opts, existing);
|
|
3044
|
+
if (!apiKey) {
|
|
3045
|
+
throw new ComposioSetupError("No Composio API key found. Pass --api-key or set COMPOSIO_API_KEY.");
|
|
3046
|
+
}
|
|
3047
|
+
const userId = resolveUserId(opts, existing);
|
|
3048
|
+
const client = await loadComposioClient(apiKey);
|
|
3049
|
+
const emailTo = stringValue(opts.emailTo) ?? stringValue(existing["emailTo"]);
|
|
3050
|
+
const authConfigId = stringValue(opts.authConfigId) ?? stringValue(existing["authConfigId"]);
|
|
3051
|
+
const optionConnectedAccountId = stringValue(opts.connectedAccountId);
|
|
3052
|
+
const existingConnectedAccountId = stringValue(existing["connectedAccountId"]);
|
|
3053
|
+
const connectedAccountId = optionConnectedAccountId ?? existingConnectedAccountId;
|
|
3054
|
+
const routingPreset = resolveComposioRoutingPreset(opts.routingPreset) ?? "all";
|
|
3055
|
+
if (opts.status) {
|
|
3056
|
+
const accounts = await listActiveGmailAccounts(client, userId);
|
|
3057
|
+
printMailStatus({ userId, emailTo, connectedAccountId }, accounts, rawConfig);
|
|
3058
|
+
return {
|
|
3059
|
+
apiKey,
|
|
3060
|
+
shouldWriteApiKey,
|
|
3061
|
+
userId,
|
|
3062
|
+
emailTo,
|
|
3063
|
+
connectedAccountId,
|
|
3064
|
+
targetName: COMPOSIO_MAIL_NOTIFIER,
|
|
3065
|
+
routingPreset,
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
if (!emailTo) {
|
|
3069
|
+
throw new ComposioSetupError("No recipient email found. Pass --email-to.");
|
|
3070
|
+
}
|
|
3071
|
+
if (connectedAccountId) {
|
|
3072
|
+
let account;
|
|
3073
|
+
try {
|
|
3074
|
+
account = await withConnectedAccountDetails(client, await verifyConnectedAccountForToolkit(client, userId, connectedAccountId, GMAIL_TOOLKIT, "Gmail", () => listActiveGmailAccounts(client, userId)));
|
|
3075
|
+
}
|
|
3076
|
+
catch (err) {
|
|
3077
|
+
if (optionConnectedAccountId)
|
|
3078
|
+
throw err;
|
|
3079
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3080
|
+
console.log(chalk.yellow(`Existing Gmail connected account ${connectedAccountId} could not be used: ${message}. Looking for another Gmail connected account.`));
|
|
3081
|
+
}
|
|
3082
|
+
if (account && (await accountCanSendGmail(client, account))) {
|
|
3083
|
+
return {
|
|
3084
|
+
apiKey,
|
|
3085
|
+
shouldWriteApiKey,
|
|
3086
|
+
userId,
|
|
3087
|
+
emailTo,
|
|
3088
|
+
connectedAccountId: account.id,
|
|
3089
|
+
targetName: COMPOSIO_MAIL_NOTIFIER,
|
|
3090
|
+
routingPreset,
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
if (account && optionConnectedAccountId) {
|
|
3094
|
+
throw new ComposioSetupError(`Connected account ${connectedAccountId} is missing Gmail send/profile access. Connect Gmail in Composio with send access, then rerun \`athene setup composio-mail --email-to ${emailTo} --connected-account-id ${connectedAccountId}\`, or pass a different Gmail connected account.`);
|
|
3095
|
+
}
|
|
3096
|
+
if (account) {
|
|
3097
|
+
console.log(chalk.yellow(`Existing Gmail connected account ${connectedAccountId} is missing Gmail send/profile access. Looking for another Gmail connected account.`));
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
const accounts = await listUsableGmailAccounts(client, userId);
|
|
3101
|
+
if (accounts.length > 0) {
|
|
3102
|
+
const account = await chooseAccount(accounts, nonInteractive, "Gmail");
|
|
3103
|
+
return {
|
|
3104
|
+
apiKey,
|
|
3105
|
+
shouldWriteApiKey,
|
|
3106
|
+
userId,
|
|
3107
|
+
emailTo,
|
|
3108
|
+
connectedAccountId: account.id,
|
|
3109
|
+
targetName: COMPOSIO_MAIL_NOTIFIER,
|
|
3110
|
+
routingPreset,
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
if (opts.connect) {
|
|
3114
|
+
const connection = await createManagedOAuthConnectionRequest(client, userId, GMAIL_TOOLKIT, "Gmail", "Gmail Auth Config", parseWaitMs(opts.waitMs), {
|
|
3115
|
+
authConfigId: await resolveGmailConnectAuthConfigId(client, authConfigId, nonInteractive),
|
|
3116
|
+
});
|
|
3117
|
+
if (connection.account) {
|
|
3118
|
+
const account = await withConnectedAccountDetails(client, connection.account);
|
|
3119
|
+
if (!(await accountCanSendGmail(client, account))) {
|
|
3120
|
+
throw new ComposioSetupError(`Connected Gmail account ${account.id} is missing Gmail send/profile access. Fix the Gmail connection in Composio, then rerun \`athene setup composio-mail\`.`);
|
|
3121
|
+
}
|
|
3122
|
+
return {
|
|
3123
|
+
apiKey,
|
|
3124
|
+
shouldWriteApiKey,
|
|
3125
|
+
userId,
|
|
3126
|
+
emailTo,
|
|
3127
|
+
connectedAccountId: account.id,
|
|
3128
|
+
targetName: COMPOSIO_MAIL_NOTIFIER,
|
|
3129
|
+
routingPreset,
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
return {
|
|
3133
|
+
apiKey,
|
|
3134
|
+
shouldWriteApiKey,
|
|
3135
|
+
userId,
|
|
3136
|
+
emailTo,
|
|
3137
|
+
connectionUrl: connection.url,
|
|
3138
|
+
targetName: COMPOSIO_MAIL_NOTIFIER,
|
|
3139
|
+
routingPreset,
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
throw new ComposioSetupError([
|
|
3143
|
+
`No active Gmail connected account with send access was found for user ${userId}.`,
|
|
3144
|
+
"Connect Gmail in Composio first, then rerun `athene setup composio-mail`, or rerun with `--connect` to print a Composio connect URL.",
|
|
3145
|
+
`You can also pass an existing Gmail account with \`athene setup composio-mail --email-to ${emailTo} --connected-account-id ca_...\`.`,
|
|
3146
|
+
].join(" "));
|
|
3147
|
+
}
|
|
3148
|
+
export async function runComposioDiscordWebhookSetupAction(opts) {
|
|
3149
|
+
let configPath;
|
|
3150
|
+
try {
|
|
3151
|
+
configPath = findConfigFile() ?? undefined;
|
|
3152
|
+
}
|
|
3153
|
+
catch {
|
|
3154
|
+
configPath = undefined;
|
|
3155
|
+
}
|
|
3156
|
+
if (!configPath) {
|
|
3157
|
+
throw new ComposioSetupError("No agent-orchestrator.yaml found. Run 'athene start' first to create one.");
|
|
3158
|
+
}
|
|
3159
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
3160
|
+
const rawConfig = parseDocument(rawYaml).toJS() ?? {};
|
|
3161
|
+
if (shouldUseInteractiveDedicatedSetup(opts, opts.nonInteractive || !process.stdin.isTTY)) {
|
|
3162
|
+
const clack = await import("@clack/prompts");
|
|
3163
|
+
clack.intro("AO Composio Discord webhook setup");
|
|
3164
|
+
await runInteractiveComposioDiscordWebhookSetup(clack, opts, configPath, rawConfig, COMPOSIO_DISCORD_WEBHOOK_NOTIFIER);
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
const existing = getExistingNotifierConfig(rawConfig, COMPOSIO_DISCORD_WEBHOOK_NOTIFIER);
|
|
3168
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
3169
|
+
if (existingPlugin && existingPlugin !== "composio" && !opts.force) {
|
|
3170
|
+
throw new ComposioSetupError(`notifiers.${COMPOSIO_DISCORD_WEBHOOK_NOTIFIER} already uses plugin "${existingPlugin}". Re-run with --force to replace it.`);
|
|
3171
|
+
}
|
|
3172
|
+
const resolved = await resolveDiscordWebhookSetup(opts, rawConfig);
|
|
3173
|
+
if (opts.status)
|
|
3174
|
+
return;
|
|
3175
|
+
writeComposioDiscordConfig(configPath, resolved);
|
|
3176
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
3177
|
+
console.log(chalk.green("✓ Discord webhook configured through Composio"));
|
|
3178
|
+
if (resolved.connectedAccountId) {
|
|
3179
|
+
console.log(chalk.green(`✓ Discord webhook connected account: ${resolved.connectedAccountId}`));
|
|
3180
|
+
}
|
|
3181
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${COMPOSIO_DISCORD_WEBHOOK_NOTIFIER} --template basic`));
|
|
3182
|
+
}
|
|
3183
|
+
export async function runComposioDiscordBotSetupAction(opts) {
|
|
3184
|
+
let configPath;
|
|
3185
|
+
try {
|
|
3186
|
+
configPath = findConfigFile() ?? undefined;
|
|
3187
|
+
}
|
|
3188
|
+
catch {
|
|
3189
|
+
configPath = undefined;
|
|
3190
|
+
}
|
|
3191
|
+
if (!configPath) {
|
|
3192
|
+
throw new ComposioSetupError("No agent-orchestrator.yaml found. Run 'athene start' first to create one.");
|
|
3193
|
+
}
|
|
3194
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
3195
|
+
const rawConfig = parseDocument(rawYaml).toJS() ?? {};
|
|
3196
|
+
if (shouldUseInteractiveDedicatedSetup(opts, opts.nonInteractive || !process.stdin.isTTY)) {
|
|
3197
|
+
const clack = await import("@clack/prompts");
|
|
3198
|
+
clack.intro("AO Composio Discord bot setup");
|
|
3199
|
+
await runInteractiveComposioDiscordBotSetup(clack, opts, configPath, rawConfig, COMPOSIO_DISCORD_BOT_NOTIFIER);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
const existing = getExistingNotifierConfig(rawConfig, COMPOSIO_DISCORD_BOT_NOTIFIER);
|
|
3203
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
3204
|
+
if (existingPlugin && existingPlugin !== "composio" && !opts.force) {
|
|
3205
|
+
throw new ComposioSetupError(`notifiers.${COMPOSIO_DISCORD_BOT_NOTIFIER} already uses plugin "${existingPlugin}". Re-run with --force to replace it.`);
|
|
3206
|
+
}
|
|
3207
|
+
const resolved = await resolveDiscordBotSetup(opts, rawConfig);
|
|
3208
|
+
if (opts.status)
|
|
3209
|
+
return;
|
|
3210
|
+
writeComposioDiscordConfig(configPath, resolved);
|
|
3211
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
3212
|
+
console.log(chalk.green(`✓ Discord bot connected account: ${resolved.connectedAccountId}`));
|
|
3213
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${COMPOSIO_DISCORD_BOT_NOTIFIER} --template basic`));
|
|
3214
|
+
}
|
|
3215
|
+
export async function runComposioMailSetupAction(opts) {
|
|
3216
|
+
const nonInteractive = opts.nonInteractive || !process.stdin.isTTY;
|
|
3217
|
+
let configPath;
|
|
3218
|
+
try {
|
|
3219
|
+
configPath = findConfigFile() ?? undefined;
|
|
3220
|
+
}
|
|
3221
|
+
catch {
|
|
3222
|
+
configPath = undefined;
|
|
3223
|
+
}
|
|
3224
|
+
if (!configPath) {
|
|
3225
|
+
throw new ComposioSetupError("No agent-orchestrator.yaml found. Run 'athene start' first to create one.");
|
|
3226
|
+
}
|
|
3227
|
+
const rawYaml = readFileSync(configPath, "utf-8");
|
|
3228
|
+
const rawConfig = parseDocument(rawYaml).toJS() ?? {};
|
|
3229
|
+
if (shouldUseInteractiveDedicatedSetup(opts, nonInteractive)) {
|
|
3230
|
+
const clack = await import("@clack/prompts");
|
|
3231
|
+
clack.intro("AO Composio Gmail setup");
|
|
3232
|
+
await runInteractiveComposioGmailSetup(clack, opts, configPath, rawConfig, COMPOSIO_MAIL_NOTIFIER);
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
const existing = getExistingNotifierConfig(rawConfig, COMPOSIO_MAIL_NOTIFIER);
|
|
3236
|
+
const existingPlugin = stringValue(existing["plugin"]);
|
|
3237
|
+
if (existingPlugin && existingPlugin !== "composio" && !opts.force) {
|
|
3238
|
+
throw new ComposioSetupError(`notifiers.${COMPOSIO_MAIL_NOTIFIER} already uses plugin "${existingPlugin}". Re-run with --force to replace it.`);
|
|
3239
|
+
}
|
|
3240
|
+
const resolved = await resolveMailSetup(opts, rawConfig, nonInteractive);
|
|
3241
|
+
if (opts.status)
|
|
3242
|
+
return;
|
|
3243
|
+
if (resolved.connectionUrl && !resolved.connectedAccountId) {
|
|
3244
|
+
console.log(chalk.yellow("Gmail connection did not complete yet. Open the connect URL above, finish the Composio flow, then rerun `athene setup composio-mail`."));
|
|
3245
|
+
console.log(chalk.dim("No config was changed."));
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
writeComposioMailConfig(configPath, resolved);
|
|
3249
|
+
console.log(chalk.green(`✓ Config written to ${configPath}`));
|
|
3250
|
+
if (resolved.connectedAccountId) {
|
|
3251
|
+
console.log(chalk.green(`✓ Gmail connected account: ${resolved.connectedAccountId}`));
|
|
3252
|
+
}
|
|
3253
|
+
console.log(chalk.dim(`Test it with: athene notify test --to ${COMPOSIO_MAIL_NOTIFIER} --template basic`));
|
|
3254
|
+
}
|
|
3255
|
+
//# sourceMappingURL=composio-setup.js.map
|