@songsid/agend 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -0
- package/README.zh-TW.md +134 -0
- package/dist/access-path.d.ts +10 -0
- package/dist/access-path.js +32 -0
- package/dist/access-path.js.map +1 -0
- package/dist/adapter-world.d.ts +25 -0
- package/dist/adapter-world.js +41 -0
- package/dist/adapter-world.js.map +1 -0
- package/dist/agent-cli-instructions.md +50 -0
- package/dist/agent-cli.d.ts +2 -0
- package/dist/agent-cli.js +200 -0
- package/dist/agent-cli.js.map +1 -0
- package/dist/agent-endpoint.d.ts +25 -0
- package/dist/agent-endpoint.js +162 -0
- package/dist/agent-endpoint.js.map +1 -0
- package/dist/backend/antigravity.d.ts +17 -0
- package/dist/backend/antigravity.js +98 -0
- package/dist/backend/antigravity.js.map +1 -0
- package/dist/backend/claude-code.d.ts +23 -0
- package/dist/backend/claude-code.js +171 -0
- package/dist/backend/claude-code.js.map +1 -0
- package/dist/backend/codex.d.ts +18 -0
- package/dist/backend/codex.js +160 -0
- package/dist/backend/codex.js.map +1 -0
- package/dist/backend/factory.d.ts +2 -0
- package/dist/backend/factory.js +28 -0
- package/dist/backend/factory.js.map +1 -0
- package/dist/backend/gemini-cli.d.ts +17 -0
- package/dist/backend/gemini-cli.js +163 -0
- package/dist/backend/gemini-cli.js.map +1 -0
- package/dist/backend/index.d.ts +7 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/kiro.d.ts +17 -0
- package/dist/backend/kiro.js +147 -0
- package/dist/backend/kiro.js.map +1 -0
- package/dist/backend/marker-utils.d.ts +13 -0
- package/dist/backend/marker-utils.js +64 -0
- package/dist/backend/marker-utils.js.map +1 -0
- package/dist/backend/mock.d.ts +25 -0
- package/dist/backend/mock.js +85 -0
- package/dist/backend/mock.js.map +1 -0
- package/dist/backend/opencode.d.ts +16 -0
- package/dist/backend/opencode.js +136 -0
- package/dist/backend/opencode.js.map +1 -0
- package/dist/backend/types.d.ts +86 -0
- package/dist/backend/types.js +33 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/channel/access-manager.d.ts +18 -0
- package/dist/channel/access-manager.js +153 -0
- package/dist/channel/access-manager.js.map +1 -0
- package/dist/channel/adapters/telegram.d.ts +63 -0
- package/dist/channel/adapters/telegram.js +646 -0
- package/dist/channel/adapters/telegram.js.map +1 -0
- package/dist/channel/attachment-handler.d.ts +15 -0
- package/dist/channel/attachment-handler.js +88 -0
- package/dist/channel/attachment-handler.js.map +1 -0
- package/dist/channel/factory.d.ts +12 -0
- package/dist/channel/factory.js +67 -0
- package/dist/channel/factory.js.map +1 -0
- package/dist/channel/ipc-bridge.d.ts +26 -0
- package/dist/channel/ipc-bridge.js +220 -0
- package/dist/channel/ipc-bridge.js.map +1 -0
- package/dist/channel/mcp-server.d.ts +10 -0
- package/dist/channel/mcp-server.js +288 -0
- package/dist/channel/mcp-server.js.map +1 -0
- package/dist/channel/mcp-tools.d.ts +17 -0
- package/dist/channel/mcp-tools.js +110 -0
- package/dist/channel/mcp-tools.js.map +1 -0
- package/dist/channel/message-bus.d.ts +17 -0
- package/dist/channel/message-bus.js +86 -0
- package/dist/channel/message-bus.js.map +1 -0
- package/dist/channel/message-queue.d.ts +39 -0
- package/dist/channel/message-queue.js +253 -0
- package/dist/channel/message-queue.js.map +1 -0
- package/dist/channel/tool-router.d.ts +6 -0
- package/dist/channel/tool-router.js +75 -0
- package/dist/channel/tool-router.js.map +1 -0
- package/dist/channel/tool-tracker.d.ts +13 -0
- package/dist/channel/tool-tracker.js +58 -0
- package/dist/channel/tool-tracker.js.map +1 -0
- package/dist/channel/types.d.ts +118 -0
- package/dist/channel/types.js +2 -0
- package/dist/channel/types.js.map +1 -0
- package/dist/chat-export.d.ts +4 -0
- package/dist/chat-export.js +91 -0
- package/dist/chat-export.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +59 -0
- package/dist/classic-channel-manager.js +193 -0
- package/dist/classic-channel-manager.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1833 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +118 -0
- package/dist/config.js.map +1 -0
- package/dist/context-guardian.d.ts +26 -0
- package/dist/context-guardian.js +73 -0
- package/dist/context-guardian.js.map +1 -0
- package/dist/cost-guard.d.ts +36 -0
- package/dist/cost-guard.js +147 -0
- package/dist/cost-guard.js.map +1 -0
- package/dist/daemon-entry.d.ts +1 -0
- package/dist/daemon-entry.js +29 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/daemon.d.ts +152 -0
- package/dist/daemon.js +1714 -0
- package/dist/daemon.js.map +1 -0
- package/dist/daily-summary.d.ts +13 -0
- package/dist/daily-summary.js +55 -0
- package/dist/daily-summary.js.map +1 -0
- package/dist/event-log.d.ts +36 -0
- package/dist/event-log.js +100 -0
- package/dist/event-log.js.map +1 -0
- package/dist/export-import.d.ts +2 -0
- package/dist/export-import.js +162 -0
- package/dist/export-import.js.map +1 -0
- package/dist/fleet-context.d.ts +61 -0
- package/dist/fleet-context.js +4 -0
- package/dist/fleet-context.js.map +1 -0
- package/dist/fleet-dashboard-html.d.ts +6 -0
- package/dist/fleet-dashboard-html.js +443 -0
- package/dist/fleet-dashboard-html.js.map +1 -0
- package/dist/fleet-health-server.d.ts +35 -0
- package/dist/fleet-health-server.js +290 -0
- package/dist/fleet-health-server.js.map +1 -0
- package/dist/fleet-instructions.d.ts +5 -0
- package/dist/fleet-instructions.js +161 -0
- package/dist/fleet-instructions.js.map +1 -0
- package/dist/fleet-manager.d.ts +212 -0
- package/dist/fleet-manager.js +3655 -0
- package/dist/fleet-manager.js.map +1 -0
- package/dist/fleet-rpc-handlers.d.ts +42 -0
- package/dist/fleet-rpc-handlers.js +356 -0
- package/dist/fleet-rpc-handlers.js.map +1 -0
- package/dist/fleet-system-prompt.d.ts +11 -0
- package/dist/fleet-system-prompt.js +61 -0
- package/dist/fleet-system-prompt.js.map +1 -0
- package/dist/general-knowledge/skills.md +177 -0
- package/dist/hang-detector.d.ts +16 -0
- package/dist/hang-detector.js +53 -0
- package/dist/hang-detector.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/instance-lifecycle.d.ts +90 -0
- package/dist/instance-lifecycle.js +592 -0
- package/dist/instance-lifecycle.js.map +1 -0
- package/dist/instructions.d.ts +15 -0
- package/dist/instructions.js +90 -0
- package/dist/instructions.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +84 -0
- package/dist/logger.js.map +1 -0
- package/dist/outbound-handlers.d.ts +51 -0
- package/dist/outbound-handlers.js +739 -0
- package/dist/outbound-handlers.js.map +1 -0
- package/dist/outbound-schemas.d.ts +238 -0
- package/dist/outbound-schemas.js +248 -0
- package/dist/outbound-schemas.js.map +1 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +42 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin/agend/.claude-plugin/plugin.json +5 -0
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +595 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/routing-engine.d.ts +22 -0
- package/dist/routing-engine.js +44 -0
- package/dist/routing-engine.js.map +1 -0
- package/dist/safe-async.d.ts +6 -0
- package/dist/safe-async.js +20 -0
- package/dist/safe-async.js.map +1 -0
- package/dist/scheduler/db.d.ts +37 -0
- package/dist/scheduler/db.js +360 -0
- package/dist/scheduler/db.js.map +1 -0
- package/dist/scheduler/db.test.d.ts +1 -0
- package/dist/scheduler/db.test.js +92 -0
- package/dist/scheduler/db.test.js.map +1 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +4 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +44 -0
- package/dist/scheduler/scheduler.js +197 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/scheduler.test.d.ts +1 -0
- package/dist/scheduler/scheduler.test.js +119 -0
- package/dist/scheduler/scheduler.test.js.map +1 -0
- package/dist/scheduler/types.d.ts +107 -0
- package/dist/scheduler/types.js +7 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service-installer.d.ts +17 -0
- package/dist/service-installer.js +182 -0
- package/dist/service-installer.js.map +1 -0
- package/dist/setup-wizard.d.ts +48 -0
- package/dist/setup-wizard.js +701 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/statusline-watcher.d.ts +34 -0
- package/dist/statusline-watcher.js +73 -0
- package/dist/statusline-watcher.js.map +1 -0
- package/dist/stt.d.ts +10 -0
- package/dist/stt.js +33 -0
- package/dist/stt.js.map +1 -0
- package/dist/tmux-control.d.ts +52 -0
- package/dist/tmux-control.js +207 -0
- package/dist/tmux-control.js.map +1 -0
- package/dist/tmux-manager.d.ts +44 -0
- package/dist/tmux-manager.js +218 -0
- package/dist/tmux-manager.js.map +1 -0
- package/dist/topic-archiver.d.ts +40 -0
- package/dist/topic-archiver.js +103 -0
- package/dist/topic-archiver.js.map +1 -0
- package/dist/topic-commands.d.ts +28 -0
- package/dist/topic-commands.js +359 -0
- package/dist/topic-commands.js.map +1 -0
- package/dist/transcript-monitor.d.ts +23 -0
- package/dist/transcript-monitor.js +164 -0
- package/dist/transcript-monitor.js.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/dashboard.html +719 -0
- package/dist/web-api.d.ts +101 -0
- package/dist/web-api.js +648 -0
- package/dist/web-api.js.map +1 -0
- package/dist/webhook-emitter.d.ts +15 -0
- package/dist/webhook-emitter.js +41 -0
- package/dist/webhook-emitter.js.map +1 -0
- package/dist/workflow-templates/default.md +35 -0
- package/package.json +76 -0
- package/templates/launchd.plist.ejs +31 -0
- package/templates/systemd.service.ejs +16 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync, chmodSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { stdin, stdout } from "node:process";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { getAgendHome } from "./paths.js";
|
|
8
|
+
const DATA_DIR = getAgendHome();
|
|
9
|
+
const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
|
|
10
|
+
const ENV_PATH = join(DATA_DIR, ".env");
|
|
11
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
12
|
+
export function validateBotToken(token) {
|
|
13
|
+
return /^\d+:[A-Za-z0-9_-]{30,}$/.test(token);
|
|
14
|
+
}
|
|
15
|
+
export async function verifyBotToken(token) {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
if (data.ok && data.result?.username) {
|
|
20
|
+
return { valid: true, username: data.result.username };
|
|
21
|
+
}
|
|
22
|
+
return { valid: false, username: null };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { valid: false, username: null };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function verifyDiscordToken(token) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch("https://discord.com/api/v10/users/@me", {
|
|
31
|
+
headers: { Authorization: `Bot ${token}` },
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
return { valid: false, username: null };
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
return { valid: true, username: data.username ?? null };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { valid: false, username: null };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function listDiscordGuilds(token) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch("https://discord.com/api/v10/users/@me/guilds", {
|
|
45
|
+
headers: { Authorization: `Bot ${token}` },
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
return [];
|
|
49
|
+
return (await res.json());
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function bold(s) {
|
|
56
|
+
return `\x1b[1m${s}\x1b[0m`;
|
|
57
|
+
}
|
|
58
|
+
function dim(s) {
|
|
59
|
+
return `\x1b[2m${s}\x1b[0m`;
|
|
60
|
+
}
|
|
61
|
+
function green(s) {
|
|
62
|
+
return `\x1b[32m${s}\x1b[0m`;
|
|
63
|
+
}
|
|
64
|
+
function red(s) {
|
|
65
|
+
return `\x1b[31m${s}\x1b[0m`;
|
|
66
|
+
}
|
|
67
|
+
function yellow(s) {
|
|
68
|
+
return `\x1b[33m${s}\x1b[0m`;
|
|
69
|
+
}
|
|
70
|
+
function cyan(s) {
|
|
71
|
+
return `\x1b[36m${s}\x1b[0m`;
|
|
72
|
+
}
|
|
73
|
+
function step(n, total, label) {
|
|
74
|
+
console.log(`\n${cyan(`[${n}/${total}]`)} ${bold(label)}`);
|
|
75
|
+
}
|
|
76
|
+
/** Ask a question; retry until validator passes. Empty → default. */
|
|
77
|
+
async function ask(rl, prompt, opts = {}) {
|
|
78
|
+
const suffix = opts.default != null ? ` ${dim(`[${opts.default}]`)}` : "";
|
|
79
|
+
for (;;) {
|
|
80
|
+
const raw = await rl.question(` ${prompt}${suffix}: `);
|
|
81
|
+
const value = raw.trim() || opts.default || "";
|
|
82
|
+
if (opts.validate) {
|
|
83
|
+
const err = opts.validate(value);
|
|
84
|
+
if (err) {
|
|
85
|
+
console.log(` ${red(err)}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Ask a yes/no question. */
|
|
93
|
+
async function confirm(rl, prompt, defaultYes = true) {
|
|
94
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
95
|
+
const raw = await rl.question(` ${prompt} (${hint}): `);
|
|
96
|
+
const v = raw.trim().toLowerCase();
|
|
97
|
+
if (v === "")
|
|
98
|
+
return defaultYes;
|
|
99
|
+
return v === "y" || v === "yes";
|
|
100
|
+
}
|
|
101
|
+
/** Ask user to pick from numbered options. Returns 0-based index. */
|
|
102
|
+
async function choose(rl, prompt, options, defaultIndex = 0) {
|
|
103
|
+
console.log(` ${prompt}`);
|
|
104
|
+
for (let i = 0; i < options.length; i++) {
|
|
105
|
+
const marker = i === defaultIndex ? cyan("→") : " ";
|
|
106
|
+
const hint = options[i].hint ? ` ${dim(options[i].hint)}` : "";
|
|
107
|
+
console.log(` ${marker} ${i + 1}. ${options[i].label}${hint}`);
|
|
108
|
+
}
|
|
109
|
+
for (;;) {
|
|
110
|
+
const raw = await rl.question(` Pick ${dim(`[${defaultIndex + 1}]`)}: `);
|
|
111
|
+
const v = raw.trim();
|
|
112
|
+
if (v === "")
|
|
113
|
+
return defaultIndex;
|
|
114
|
+
const n = parseInt(v, 10);
|
|
115
|
+
if (n >= 1 && n <= options.length)
|
|
116
|
+
return n - 1;
|
|
117
|
+
console.log(` ${red(`Enter 1-${options.length}`)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function expandHome(p) {
|
|
121
|
+
if (p.startsWith("~/"))
|
|
122
|
+
return join(homedir(), p.slice(2));
|
|
123
|
+
if (p === "~")
|
|
124
|
+
return homedir();
|
|
125
|
+
return resolve(p);
|
|
126
|
+
}
|
|
127
|
+
export function buildFleetConfig(answers) {
|
|
128
|
+
const fleetData = {};
|
|
129
|
+
if (answers.projectRoots.length > 0) {
|
|
130
|
+
fleetData.project_roots = answers.projectRoots;
|
|
131
|
+
}
|
|
132
|
+
if (answers.channelType === "discord") {
|
|
133
|
+
fleetData.channel = {
|
|
134
|
+
type: "discord",
|
|
135
|
+
mode: answers.channelMode,
|
|
136
|
+
bot_token_env: answers.botTokenEnv,
|
|
137
|
+
...(answers.guildId ? { group_id: answers.guildId } : {}),
|
|
138
|
+
access: {
|
|
139
|
+
mode: answers.accessMode,
|
|
140
|
+
...(answers.allowedUsers.length > 0 ? { allowed_users: answers.allowedUsers } : {}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
fleetData.channel = {
|
|
146
|
+
type: "telegram",
|
|
147
|
+
mode: answers.channelMode,
|
|
148
|
+
bot_token_env: answers.botTokenEnv,
|
|
149
|
+
...(answers.groupId != null ? { group_id: answers.groupId } : {}),
|
|
150
|
+
access: {
|
|
151
|
+
mode: answers.accessMode,
|
|
152
|
+
...(answers.allowedUsers.length > 0 ? { allowed_users: answers.allowedUsers } : {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
fleetData.defaults = {
|
|
157
|
+
...(answers.backend !== "claude-code" ? { backend: answers.backend } : {}),
|
|
158
|
+
restart_policy: {
|
|
159
|
+
max_retries: 10,
|
|
160
|
+
backoff: "exponential",
|
|
161
|
+
reset_after: 300,
|
|
162
|
+
},
|
|
163
|
+
log_level: "info",
|
|
164
|
+
...(answers.costGuard.enabled && answers.costGuard.dailyLimitUsd ? {
|
|
165
|
+
cost_guard: {
|
|
166
|
+
daily_limit_usd: answers.costGuard.dailyLimitUsd,
|
|
167
|
+
warn_at_percentage: 80,
|
|
168
|
+
timezone: answers.costGuard.timezone ?? "UTC",
|
|
169
|
+
},
|
|
170
|
+
} : {}),
|
|
171
|
+
...(answers.dailySummary.enabled ? {
|
|
172
|
+
daily_summary: {
|
|
173
|
+
enabled: true,
|
|
174
|
+
hour: answers.dailySummary.hour ?? 21,
|
|
175
|
+
minute: 0,
|
|
176
|
+
},
|
|
177
|
+
} : {}),
|
|
178
|
+
};
|
|
179
|
+
const instancesObj = {};
|
|
180
|
+
for (const inst of answers.instances) {
|
|
181
|
+
instancesObj[inst.name] = {
|
|
182
|
+
working_directory: inst.workDir,
|
|
183
|
+
...(inst.topicId != null ? { topic_id: inst.topicId } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
fleetData.instances = instancesObj;
|
|
187
|
+
return fleetData;
|
|
188
|
+
}
|
|
189
|
+
// ── Prerequisite checks ──────────────────────────────────
|
|
190
|
+
export const BACKENDS = [
|
|
191
|
+
{ id: "claude-code", binary: "claude", label: "Claude Code",
|
|
192
|
+
installUrl: "https://code.claude.com/docs/en/quickstart",
|
|
193
|
+
install: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
194
|
+
auth: "claude (OAuth) or set ANTHROPIC_API_KEY" },
|
|
195
|
+
{ id: "codex", binary: "codex", label: "OpenAI Codex",
|
|
196
|
+
installUrl: "https://developers.openai.com/codex/quickstart",
|
|
197
|
+
install: "npm i -g @openai/codex",
|
|
198
|
+
auth: "codex (ChatGPT login) or set OPENAI_API_KEY" },
|
|
199
|
+
{ id: "gemini-cli", binary: "gemini", label: "Gemini CLI",
|
|
200
|
+
installUrl: "https://github.com/google-gemini/gemini-cli",
|
|
201
|
+
install: "npm i -g @google/gemini-cli",
|
|
202
|
+
auth: "gemini (Google OAuth)" },
|
|
203
|
+
{ id: "opencode", binary: "opencode", label: "OpenCode",
|
|
204
|
+
installUrl: "https://opencode.ai/download",
|
|
205
|
+
install: "curl -fsSL https://opencode.ai/install | bash",
|
|
206
|
+
auth: "opencode (configure provider)" },
|
|
207
|
+
{ id: "kiro-cli", binary: "kiro-cli", label: "Kiro CLI",
|
|
208
|
+
installUrl: "https://kiro.dev/docs/cli/",
|
|
209
|
+
install: "brew install --cask kiro-cli",
|
|
210
|
+
auth: "kiro-cli login (AWS Builder ID)" },
|
|
211
|
+
];
|
|
212
|
+
export function checkPrerequisites(binary) {
|
|
213
|
+
let backendOk = false;
|
|
214
|
+
let backendVersion = "";
|
|
215
|
+
let tmux = false;
|
|
216
|
+
let tmuxVersion = "";
|
|
217
|
+
try {
|
|
218
|
+
backendVersion = execSync(`${binary} --version`, { stdio: "pipe" }).toString().trim();
|
|
219
|
+
backendOk = true;
|
|
220
|
+
}
|
|
221
|
+
catch { /* not installed */ }
|
|
222
|
+
try {
|
|
223
|
+
tmuxVersion = execSync("tmux -V", { stdio: "pipe" }).toString().trim();
|
|
224
|
+
tmux = true;
|
|
225
|
+
}
|
|
226
|
+
catch { /* not installed */ }
|
|
227
|
+
return { backendOk, backendVersion, tmux, tmuxVersion };
|
|
228
|
+
}
|
|
229
|
+
// ── Main wizard ──────────────────────────────────────────
|
|
230
|
+
export async function runSetupWizard() {
|
|
231
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
232
|
+
console.log(`\n${bold("AgEnD — Setup Wizard")}\n`);
|
|
233
|
+
const TOTAL_STEPS = 9;
|
|
234
|
+
// ── Step 1: Backend + Prerequisites ──
|
|
235
|
+
step(1, TOTAL_STEPS, "Backend & Prerequisites");
|
|
236
|
+
const backendIdx = await choose(rl, "Which AI coding agent?", BACKENDS.map(b => ({ label: b.label, hint: b.binary })), 0);
|
|
237
|
+
const selectedBackend = BACKENDS[backendIdx];
|
|
238
|
+
const prereq = checkPrerequisites(selectedBackend.binary);
|
|
239
|
+
if (prereq.backendOk) {
|
|
240
|
+
console.log(` ${green("✓")} ${selectedBackend.label} ${dim(prereq.backendVersion)}`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
console.log(` ${red("✗")} ${selectedBackend.label} (${selectedBackend.binary}) not found`);
|
|
244
|
+
console.log();
|
|
245
|
+
console.log(` ${bold(`Prerequisites for ${selectedBackend.label}:`)}`);
|
|
246
|
+
console.log(` ${dim("1. Install:")} ${selectedBackend.install}`);
|
|
247
|
+
console.log(` ${dim("2. Auth:")} ${selectedBackend.auth}`);
|
|
248
|
+
console.log(` ${dim("3. Verify:")} ${selectedBackend.binary} --version`);
|
|
249
|
+
console.log();
|
|
250
|
+
console.log(` ${dim(`More info: ${selectedBackend.installUrl}`)}`);
|
|
251
|
+
rl.close();
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
if (prereq.tmux) {
|
|
255
|
+
console.log(` ${green("✓")} tmux ${dim(prereq.tmuxVersion)}`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
console.log(` ${red("✗")} tmux not found`);
|
|
259
|
+
console.log(` macOS: ${dim("brew install tmux")}`);
|
|
260
|
+
console.log(` Linux: ${dim("apt install tmux / dnf install tmux")}`);
|
|
261
|
+
rl.close();
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
// Detect existing config
|
|
265
|
+
if (existsSync(FLEET_CONFIG_PATH)) {
|
|
266
|
+
console.log(`\n ${yellow("⚠")} Existing config found: ${dim(FLEET_CONFIG_PATH)}`);
|
|
267
|
+
const overwrite = await confirm(rl, "Overwrite with new config?", false);
|
|
268
|
+
if (!overwrite) {
|
|
269
|
+
console.log(" Setup cancelled.");
|
|
270
|
+
rl.close();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// ── Step 2: Channel type ──
|
|
275
|
+
step(2, TOTAL_STEPS, "Channel Type");
|
|
276
|
+
const channelTypeIdx = await choose(rl, "Which chat platform?", [
|
|
277
|
+
{ label: "Telegram", hint: "Forum Topics" },
|
|
278
|
+
{ label: "Discord", hint: "Server channels" },
|
|
279
|
+
], 0);
|
|
280
|
+
const channelType = channelTypeIdx === 0 ? "telegram" : "discord";
|
|
281
|
+
let tokenEnvName = "";
|
|
282
|
+
let token = "";
|
|
283
|
+
let botUsername = "";
|
|
284
|
+
let groupId;
|
|
285
|
+
let guildId;
|
|
286
|
+
const allowedUsers = [];
|
|
287
|
+
const accessMode = "locked";
|
|
288
|
+
if (channelType === "telegram") {
|
|
289
|
+
// ── Step 3: Telegram Bot Token ──
|
|
290
|
+
step(3, TOTAL_STEPS, "Telegram Bot Token");
|
|
291
|
+
console.log(` ${dim("Get one from @BotFather on Telegram")}`);
|
|
292
|
+
tokenEnvName = "AGEND_BOT_TOKEN";
|
|
293
|
+
// Check existing .env for token
|
|
294
|
+
if (existsSync(ENV_PATH)) {
|
|
295
|
+
const envContent = readFileSync(ENV_PATH, "utf-8");
|
|
296
|
+
const match = envContent.match(/^([A-Z_]+)=(\d+:[A-Za-z0-9_-]{30,})/m);
|
|
297
|
+
if (match) {
|
|
298
|
+
const maskedToken = match[2].slice(0, 10) + "...";
|
|
299
|
+
console.log(` ${dim(`Found existing token in .env: ${match[1]}=${maskedToken}`)}`);
|
|
300
|
+
const reuse = await confirm(rl, "Use existing token?");
|
|
301
|
+
if (reuse) {
|
|
302
|
+
tokenEnvName = match[1];
|
|
303
|
+
token = match[2];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!token) {
|
|
308
|
+
token = await ask(rl, "Bot Token", {
|
|
309
|
+
validate: (v) => validateBotToken(v) ? null : "Invalid format. Expected: 123456789:ABC...",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
console.log(` Verifying with Telegram API...`);
|
|
313
|
+
const verification = await verifyBotToken(token);
|
|
314
|
+
if (!verification.valid) {
|
|
315
|
+
console.log(` ${red("✗")} Token rejected by Telegram. Check your token.`);
|
|
316
|
+
rl.close();
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
botUsername = verification.username;
|
|
320
|
+
console.log(` ${green("✓")} @${botUsername}`);
|
|
321
|
+
tokenEnvName = await ask(rl, "Env variable name for token", {
|
|
322
|
+
default: tokenEnvName,
|
|
323
|
+
validate: (v) => /^[A-Z_][A-Z0-9_]*$/.test(v) ? null : "Must be uppercase with underscores",
|
|
324
|
+
});
|
|
325
|
+
console.log();
|
|
326
|
+
console.log(` ${yellow("⚠")} Only one service can poll a bot token at a time.`);
|
|
327
|
+
console.log(` ${dim("If this bot is also used by another polling service, stop it first.")}`);
|
|
328
|
+
// ── Step 3b: Telegram Group ID ──
|
|
329
|
+
step(3, TOTAL_STEPS, "Telegram Group");
|
|
330
|
+
console.log();
|
|
331
|
+
console.log(` ${dim("To get the group ID:")}`);
|
|
332
|
+
console.log(` ${dim("1. Add the bot to a Telegram group with Forum Topics enabled")}`);
|
|
333
|
+
console.log(` ${dim("2. Send a message in the group")}`);
|
|
334
|
+
console.log(` ${dim("3. Open https://api.telegram.org/bot<TOKEN>/getUpdates")}`);
|
|
335
|
+
console.log(` ${dim(" Or: add @getidsbot to the group")}`);
|
|
336
|
+
console.log();
|
|
337
|
+
const gidStr = await ask(rl, "Group ID", {
|
|
338
|
+
validate: (v) => {
|
|
339
|
+
const n = parseInt(v, 10);
|
|
340
|
+
if (isNaN(n))
|
|
341
|
+
return "Must be a number";
|
|
342
|
+
if (n >= 0)
|
|
343
|
+
return "Group IDs are negative (e.g., -1001234567890)";
|
|
344
|
+
return null;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
groupId = parseInt(gidStr, 10);
|
|
348
|
+
// ── Step 4: Telegram Access Control ──
|
|
349
|
+
step(4, TOTAL_STEPS, "Access Control");
|
|
350
|
+
console.log(` ${dim("Only whitelisted Telegram user IDs can interact with the bot.")}`);
|
|
351
|
+
console.log(` ${dim("Your Telegram user ID — send /start to @userinfobot or @getidsbot")}`);
|
|
352
|
+
const uidStr = await ask(rl, "Your Telegram user ID", {
|
|
353
|
+
validate: (v) => {
|
|
354
|
+
const n = parseInt(v, 10);
|
|
355
|
+
return isNaN(n) || n <= 0 ? "Must be a positive number" : null;
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
allowedUsers.push(uidStr);
|
|
359
|
+
let addMore = await confirm(rl, "Add another user?", false);
|
|
360
|
+
while (addMore) {
|
|
361
|
+
const uid = await ask(rl, "User ID", {
|
|
362
|
+
validate: (v) => {
|
|
363
|
+
const n = parseInt(v, 10);
|
|
364
|
+
return isNaN(n) || n <= 0 ? "Must be a positive number" : null;
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
allowedUsers.push(uid);
|
|
368
|
+
addMore = await confirm(rl, "Add another user?", false);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// ── Step 3: Discord Bot Token ──
|
|
373
|
+
step(3, TOTAL_STEPS, "Discord Bot Token");
|
|
374
|
+
console.log(` ${dim("Create a bot at https://discord.com/developers/applications")}`);
|
|
375
|
+
tokenEnvName = "AGEND_DISCORD_TOKEN";
|
|
376
|
+
// Check existing .env for Discord token
|
|
377
|
+
if (existsSync(ENV_PATH)) {
|
|
378
|
+
const envContent = readFileSync(ENV_PATH, "utf-8");
|
|
379
|
+
const match = envContent.match(/^(AGEND_DISCORD_TOKEN)=(\S+)/m);
|
|
380
|
+
if (match) {
|
|
381
|
+
const masked = match[2].slice(0, 10) + "...";
|
|
382
|
+
console.log(` ${dim(`Found existing token in .env: ${match[1]}=${masked}`)}`);
|
|
383
|
+
const reuse = await confirm(rl, "Use existing token?");
|
|
384
|
+
if (reuse) {
|
|
385
|
+
tokenEnvName = match[1];
|
|
386
|
+
token = match[2];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!token) {
|
|
391
|
+
token = await ask(rl, "Bot Token", {
|
|
392
|
+
validate: (v) => v.length > 20 ? null : "Token too short",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
console.log(` Verifying with Discord API...`);
|
|
396
|
+
const verification = await verifyDiscordToken(token);
|
|
397
|
+
if (!verification.valid) {
|
|
398
|
+
console.log(` ${red("✗")} Token rejected by Discord. Check your token.`);
|
|
399
|
+
rl.close();
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
botUsername = verification.username;
|
|
403
|
+
console.log(` ${green("✓")} ${botUsername}`);
|
|
404
|
+
tokenEnvName = await ask(rl, "Env variable name for token", {
|
|
405
|
+
default: tokenEnvName,
|
|
406
|
+
validate: (v) => /^[A-Z_][A-Z0-9_]*$/.test(v) ? null : "Must be uppercase with underscores",
|
|
407
|
+
});
|
|
408
|
+
// ── Step 3b: Discord Guild Selection ──
|
|
409
|
+
step(3, TOTAL_STEPS, "Discord Server");
|
|
410
|
+
console.log(` Fetching servers...`);
|
|
411
|
+
const guilds = await listDiscordGuilds(token);
|
|
412
|
+
if (guilds.length === 0) {
|
|
413
|
+
console.log(` ${red("✗")} Bot is not in any server. Invite it first.`);
|
|
414
|
+
rl.close();
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
if (guilds.length === 1) {
|
|
418
|
+
guildId = guilds[0].id;
|
|
419
|
+
console.log(` ${green("✓")} ${guilds[0].name} (${guildId})`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const guildIdx = await choose(rl, "Which server?", guilds.map(g => ({ label: g.name, hint: g.id })), 0);
|
|
423
|
+
guildId = guilds[guildIdx].id;
|
|
424
|
+
}
|
|
425
|
+
// ── Step 4: Discord Access Control ──
|
|
426
|
+
step(4, TOTAL_STEPS, "Access Control");
|
|
427
|
+
console.log(` ${dim("Only whitelisted Discord user IDs can interact with the bot.")}`);
|
|
428
|
+
console.log(` ${dim("Enable Developer Mode in Discord settings to copy user IDs.")}`);
|
|
429
|
+
const uidStr = await ask(rl, "Your Discord user ID", {
|
|
430
|
+
validate: (v) => /^\d{17,20}$/.test(v) ? null : "Must be a Discord snowflake ID (17-20 digits)",
|
|
431
|
+
});
|
|
432
|
+
allowedUsers.push(uidStr);
|
|
433
|
+
let addMore = await confirm(rl, "Add another user?", false);
|
|
434
|
+
while (addMore) {
|
|
435
|
+
const uid = await ask(rl, "User ID", {
|
|
436
|
+
validate: (v) => /^\d{17,20}$/.test(v) ? null : "Must be a Discord snowflake ID",
|
|
437
|
+
});
|
|
438
|
+
allowedUsers.push(uid);
|
|
439
|
+
addMore = await confirm(rl, "Add another user?", false);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const mode = "topic";
|
|
443
|
+
// ── Step 5: Project roots ──
|
|
444
|
+
step(5, TOTAL_STEPS, "Project Roots");
|
|
445
|
+
console.log(` ${dim("Directories containing your projects (for auto-bind browsing)")}`);
|
|
446
|
+
console.log(` ${dim("When a new topic is created, the bot shows projects from these dirs")}`);
|
|
447
|
+
console.log();
|
|
448
|
+
const projectRoots = [];
|
|
449
|
+
let addRoot = true;
|
|
450
|
+
while (addRoot) {
|
|
451
|
+
const root = await ask(rl, projectRoots.length === 0 ? "Project root" : "Another root", {
|
|
452
|
+
default: projectRoots.length === 0 ? "~/Projects" : undefined,
|
|
453
|
+
validate: (v) => {
|
|
454
|
+
const expanded = expandHome(v);
|
|
455
|
+
if (!existsSync(expanded))
|
|
456
|
+
return `Directory not found: ${expanded}`;
|
|
457
|
+
return null;
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
// Store with ~ for readability
|
|
461
|
+
const expanded = expandHome(root);
|
|
462
|
+
const homePrefix = homedir();
|
|
463
|
+
const display = expanded.startsWith(homePrefix)
|
|
464
|
+
? "~" + expanded.slice(homePrefix.length)
|
|
465
|
+
: expanded;
|
|
466
|
+
projectRoots.push(display);
|
|
467
|
+
console.log(` ${green("+")} ${display}`);
|
|
468
|
+
addRoot = await confirm(rl, "Add another root?", false);
|
|
469
|
+
}
|
|
470
|
+
// ── Step 6: Initial instance ──
|
|
471
|
+
step(6, TOTAL_STEPS, "Initial Instances");
|
|
472
|
+
if (mode === "topic") {
|
|
473
|
+
console.log(` ${dim("In topic mode, instances auto-bind to new forum topics.")}`);
|
|
474
|
+
console.log(` ${dim("You can pre-configure instances now, or let the bot handle it.")}`);
|
|
475
|
+
}
|
|
476
|
+
const instances = [];
|
|
477
|
+
const addInstance = await confirm(rl, "Pre-configure an instance now?", false);
|
|
478
|
+
if (addInstance) {
|
|
479
|
+
let more = true;
|
|
480
|
+
while (more) {
|
|
481
|
+
const name = await ask(rl, "Instance name", {
|
|
482
|
+
validate: (v) => {
|
|
483
|
+
if (v.length === 0)
|
|
484
|
+
return "Name required";
|
|
485
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(v) && !/^[\u4e00-\u9fff\u3400-\u4dbf]+$/.test(v)) {
|
|
486
|
+
return "Name must be alphanumeric (a-z, 0-9, ., -, _) or CJK characters";
|
|
487
|
+
}
|
|
488
|
+
if (instances.some((i) => i.name === v))
|
|
489
|
+
return "Name already used";
|
|
490
|
+
return null;
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
const workDir = await ask(rl, "Working directory", {
|
|
494
|
+
validate: (v) => {
|
|
495
|
+
const expanded = expandHome(v);
|
|
496
|
+
if (!existsSync(expanded)) {
|
|
497
|
+
return `Not found: ${expanded} — create it first or auto-create on bind`;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
let topicId;
|
|
503
|
+
if (mode === "topic") {
|
|
504
|
+
const tid = await ask(rl, "Topic ID (leave empty to auto-bind later)", {
|
|
505
|
+
default: "",
|
|
506
|
+
});
|
|
507
|
+
if (tid)
|
|
508
|
+
topicId = tid;
|
|
509
|
+
}
|
|
510
|
+
const expanded = expandHome(workDir);
|
|
511
|
+
instances.push({ name, workDir: expanded, topicId });
|
|
512
|
+
console.log(` ${green("+")} ${name} → ${expanded}${topicId ? ` (topic #${topicId})` : ""}`);
|
|
513
|
+
more = await confirm(rl, "Add another instance?", false);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ── Step 7: Fleet Defaults ──
|
|
517
|
+
step(7, TOTAL_STEPS, "Fleet Defaults");
|
|
518
|
+
let costGuardLimit = 0;
|
|
519
|
+
let costGuardTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
520
|
+
const enableCostGuard = await confirm(rl, "Enable cost guard (daily spending limit)?", false);
|
|
521
|
+
if (enableCostGuard) {
|
|
522
|
+
const limitStr = await ask(rl, "Daily limit (USD)", {
|
|
523
|
+
default: "50",
|
|
524
|
+
validate: (v) => {
|
|
525
|
+
const n = parseFloat(v);
|
|
526
|
+
return isNaN(n) || n <= 0 ? "Must be a positive number" : null;
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
costGuardLimit = parseFloat(limitStr);
|
|
530
|
+
costGuardTimezone = await ask(rl, "Timezone", {
|
|
531
|
+
default: costGuardTimezone,
|
|
532
|
+
validate: (v) => {
|
|
533
|
+
try {
|
|
534
|
+
Intl.DateTimeFormat(undefined, { timeZone: v });
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return `Invalid timezone: "${v}". Use IANA format (e.g. Asia/Taipei, America/New_York)`;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
let dailySummaryHour = 21;
|
|
544
|
+
const enableSummary = await confirm(rl, "Enable daily summary report?", true);
|
|
545
|
+
if (enableSummary) {
|
|
546
|
+
const hourStr = await ask(rl, "Summary hour (0-23, local time)", {
|
|
547
|
+
default: "21",
|
|
548
|
+
validate: (v) => {
|
|
549
|
+
const n = parseInt(v, 10);
|
|
550
|
+
return isNaN(n) || n < 0 || n > 23 ? "Must be 0-23" : null;
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
dailySummaryHour = parseInt(hourStr, 10);
|
|
554
|
+
}
|
|
555
|
+
// ── Step 8: Voice Transcription (Groq) ──
|
|
556
|
+
step(8, TOTAL_STEPS, "Voice Transcription");
|
|
557
|
+
let groqApiKey = "";
|
|
558
|
+
// Check existing .env for GROQ_API_KEY
|
|
559
|
+
if (existsSync(ENV_PATH)) {
|
|
560
|
+
const existingEnv = readFileSync(ENV_PATH, "utf-8");
|
|
561
|
+
const groqMatch = existingEnv.match(/^GROQ_API_KEY=(gsk_\S+)/m);
|
|
562
|
+
if (groqMatch) {
|
|
563
|
+
const masked = groqMatch[1].slice(0, 8) + "..." + groqMatch[1].slice(-4);
|
|
564
|
+
console.log(` ${dim(`Found existing key: ${masked}`)}`);
|
|
565
|
+
const keep = await confirm(rl, "Keep existing Groq API key?");
|
|
566
|
+
if (keep)
|
|
567
|
+
groqApiKey = groqMatch[1];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (!groqApiKey) {
|
|
571
|
+
const enableVoice = await confirm(rl, "Enable voice transcription (Groq Whisper)?", false);
|
|
572
|
+
if (enableVoice) {
|
|
573
|
+
console.log(` ${dim("Get a key from https://console.groq.com/keys")}`);
|
|
574
|
+
groqApiKey = await ask(rl, "Groq API Key", {
|
|
575
|
+
validate: (v) => v.startsWith("gsk_") ? null : "Must start with gsk_",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// ── Step 9: Summary ──
|
|
580
|
+
step(9, TOTAL_STEPS, "Summary");
|
|
581
|
+
console.log();
|
|
582
|
+
console.log(` ${bold("Channel:")} ${channelType}`);
|
|
583
|
+
console.log(` ${bold("Backend:")} ${selectedBackend.label}`);
|
|
584
|
+
console.log(` ${bold("Bot:")} ${channelType === "telegram" ? "@" : ""}${botUsername}`);
|
|
585
|
+
console.log(` ${bold("Token env:")} ${tokenEnvName}`);
|
|
586
|
+
console.log(` ${bold("Mode:")} ${mode}${groupId ? ` (group: ${groupId})` : ""}${guildId ? ` (guild: ${guildId})` : ""}`);
|
|
587
|
+
console.log(` ${bold("Access:")} ${accessMode}${allowedUsers.length > 0 ? ` — users: ${allowedUsers.join(", ")}` : ""}`);
|
|
588
|
+
console.log(` ${bold("Roots:")} ${projectRoots.join(", ") || dim("(none)")}`);
|
|
589
|
+
if (instances.length > 0) {
|
|
590
|
+
console.log(` ${bold("Instances:")}`);
|
|
591
|
+
for (const inst of instances) {
|
|
592
|
+
console.log(` ${inst.name} → ${inst.workDir}${inst.topicId ? ` #${inst.topicId}` : ""}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
console.log(` ${bold("Instances:")} ${dim("(none — will auto-create from topics)")}`);
|
|
597
|
+
}
|
|
598
|
+
if (costGuardLimit > 0) {
|
|
599
|
+
console.log(` ${bold("Cost guard:")} $${costGuardLimit}/day (${costGuardTimezone})`);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
console.log(` ${bold("Cost guard:")} ${dim("disabled")}`);
|
|
603
|
+
}
|
|
604
|
+
console.log(` ${bold("Daily sum.:")} ${enableSummary ? `${dailySummaryHour}:00` : dim("disabled")}`);
|
|
605
|
+
console.log(` ${bold("Voice:")} ${groqApiKey ? green("enabled (Groq Whisper)") : dim("disabled")}`);
|
|
606
|
+
console.log();
|
|
607
|
+
const proceed = await confirm(rl, "Write config?");
|
|
608
|
+
if (!proceed) {
|
|
609
|
+
console.log(" Setup cancelled.");
|
|
610
|
+
rl.close();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// ── Write files ──
|
|
614
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
615
|
+
// .env — merge with existing, don't clobber other vars
|
|
616
|
+
let envContent = "";
|
|
617
|
+
if (existsSync(ENV_PATH)) {
|
|
618
|
+
envContent = readFileSync(ENV_PATH, "utf-8");
|
|
619
|
+
// Remove old token line with same env name
|
|
620
|
+
envContent = envContent
|
|
621
|
+
.split("\n")
|
|
622
|
+
.filter((line) => !line.startsWith(`${tokenEnvName}=`) && !(groqApiKey && line.startsWith("GROQ_API_KEY=")))
|
|
623
|
+
.join("\n");
|
|
624
|
+
if (envContent && !envContent.endsWith("\n"))
|
|
625
|
+
envContent += "\n";
|
|
626
|
+
}
|
|
627
|
+
envContent += `${tokenEnvName}=${token}\n`;
|
|
628
|
+
if (groqApiKey)
|
|
629
|
+
envContent += `GROQ_API_KEY=${groqApiKey}\n`;
|
|
630
|
+
// .env contains the bot token (and possibly third-party API keys) — restrict
|
|
631
|
+
// to owner read/write so other local users / curious processes can't grab it.
|
|
632
|
+
writeFileSync(ENV_PATH, envContent, { mode: 0o600 });
|
|
633
|
+
// writeFileSync's mode is only honoured when the file did not previously
|
|
634
|
+
// exist; chmod the realised file to cover the overwrite case as well.
|
|
635
|
+
try {
|
|
636
|
+
chmodSync(ENV_PATH, 0o600);
|
|
637
|
+
}
|
|
638
|
+
catch { /* best-effort on Windows */ }
|
|
639
|
+
console.log(` ${green("✓")} ${ENV_PATH}`);
|
|
640
|
+
// fleet.yaml
|
|
641
|
+
const yaml = await import("js-yaml");
|
|
642
|
+
const fleetData = buildFleetConfig({
|
|
643
|
+
channelType,
|
|
644
|
+
backend: selectedBackend.id,
|
|
645
|
+
botTokenEnv: tokenEnvName,
|
|
646
|
+
groupId,
|
|
647
|
+
guildId,
|
|
648
|
+
channelMode: mode,
|
|
649
|
+
accessMode,
|
|
650
|
+
allowedUsers,
|
|
651
|
+
projectRoots,
|
|
652
|
+
instances,
|
|
653
|
+
costGuard: { enabled: costGuardLimit > 0, dailyLimitUsd: costGuardLimit || undefined, timezone: costGuardTimezone },
|
|
654
|
+
dailySummary: { enabled: enableSummary, hour: dailySummaryHour },
|
|
655
|
+
});
|
|
656
|
+
writeFileSync(FLEET_CONFIG_PATH, yaml.dump(fleetData, { lineWidth: 120 }));
|
|
657
|
+
console.log(` ${green("✓")} ${FLEET_CONFIG_PATH}`);
|
|
658
|
+
// ── System service (optional) ──
|
|
659
|
+
console.log();
|
|
660
|
+
const installSvc = await confirm(rl, "Install as system service?", false);
|
|
661
|
+
if (installSvc) {
|
|
662
|
+
const { installService, detectPlatform } = await import("./service-installer.js");
|
|
663
|
+
const svcPath = installService({
|
|
664
|
+
label: "com.agend.fleet",
|
|
665
|
+
execPath: process.argv[1],
|
|
666
|
+
path: process.env.PATH,
|
|
667
|
+
workingDirectory: DATA_DIR,
|
|
668
|
+
logPath: join(DATA_DIR, "fleet.log"),
|
|
669
|
+
});
|
|
670
|
+
console.log(` ${green("✓")} ${svcPath}`);
|
|
671
|
+
const plat = detectPlatform();
|
|
672
|
+
if (plat === "macos") {
|
|
673
|
+
console.log(` Run: ${dim(`launchctl load ${svcPath}`)}`);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
console.log(` Run: ${dim("systemctl --user enable --now agend")}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// ── Done ──
|
|
680
|
+
console.log(`\n${green("✓")} ${bold("Setup complete!")}`);
|
|
681
|
+
console.log(` Bot: ${channelType === "telegram" ? "@" : ""}${botUsername}`);
|
|
682
|
+
console.log(` Config: ${FLEET_CONFIG_PATH}`);
|
|
683
|
+
if (channelType === "discord") {
|
|
684
|
+
console.log(` ${dim("Install Discord plugin: npm install -g @suzuke/agend-plugin-discord")}`);
|
|
685
|
+
}
|
|
686
|
+
console.log();
|
|
687
|
+
console.log(` Start the fleet:`);
|
|
688
|
+
console.log(` ${dim("agend fleet start")}`);
|
|
689
|
+
if (channelType === "discord") {
|
|
690
|
+
console.log();
|
|
691
|
+
console.log(` ${dim("Classic Bot Mode: Use /start in any Discord channel to start an agent. Use /chat to talk.")}`);
|
|
692
|
+
}
|
|
693
|
+
if (mode === "topic" && channelType === "telegram") {
|
|
694
|
+
console.log();
|
|
695
|
+
console.log(` ${dim("Create a new topic in the group — the bot will auto-detect it")}`);
|
|
696
|
+
console.log(` ${dim("and let you bind it to a project.")}`);
|
|
697
|
+
}
|
|
698
|
+
console.log();
|
|
699
|
+
rl.close();
|
|
700
|
+
}
|
|
701
|
+
//# sourceMappingURL=setup-wizard.js.map
|