talon-agent 3.33.3 → 3.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/app.ts +16 -8
- package/src/backend/claude-sdk/stream.ts +76 -53
- package/src/backend/remote-server/turn.ts +58 -55
- package/src/bootstrap.ts +120 -79
- package/src/cli/setup.ts +375 -349
- package/src/core/background/cron-spec.ts +273 -0
- package/src/core/background/heartbeat/agent.ts +167 -104
- package/src/core/engine/backend-controller/pool.ts +2 -0
- package/src/core/engine/backend-controller/state.ts +25 -12
- package/src/core/engine/gateway-actions/cron.ts +28 -292
- package/src/core/engine/gateway-routes.ts +239 -0
- package/src/core/engine/gateway.ts +66 -238
- package/src/core/vfs/mounts/files.ts +128 -115
- package/src/core/weaver/shuttle.ts +29 -2
- package/src/core/weaver/weaver.ts +37 -6
- package/src/frontend/discord/callbacks/components/agent-buttons.ts +82 -0
- package/src/frontend/discord/callbacks/components/backend-select.ts +149 -0
- package/src/frontend/discord/callbacks/components/effort.ts +72 -0
- package/src/frontend/discord/callbacks/components/index.ts +120 -0
- package/src/frontend/discord/callbacks/components/metrics.ts +24 -0
- package/src/frontend/discord/callbacks/components/model-nav.ts +93 -0
- package/src/frontend/discord/callbacks/components/model-select.ts +118 -0
- package/src/frontend/discord/callbacks/components/model.ts +33 -0
- package/src/frontend/discord/callbacks/components/pulse.ts +93 -0
- package/src/frontend/discord/callbacks/components/settings.ts +243 -0
- package/src/frontend/discord/callbacks/components/types.ts +34 -0
- package/src/frontend/discord/callbacks/index.ts +4 -4
- package/src/frontend/discord/connection.ts +36 -0
- package/src/frontend/discord/diagnostics.ts +62 -0
- package/src/frontend/discord/guild-policy.ts +89 -0
- package/src/frontend/discord/index.ts +44 -305
- package/src/frontend/discord/outbound.ts +61 -0
- package/src/frontend/discord/ready.ts +73 -0
- package/src/frontend/discord/runtime.ts +55 -0
- package/src/frontend/native/chat-lifecycle.ts +39 -0
- package/src/frontend/native/chat-wire.ts +69 -0
- package/src/frontend/native/context.ts +106 -0
- package/src/frontend/native/control.ts +78 -0
- package/src/frontend/native/emit.ts +167 -0
- package/src/frontend/native/empty-chat-sweep.ts +50 -0
- package/src/frontend/native/handlers.ts +121 -0
- package/src/frontend/native/history.ts +101 -0
- package/src/frontend/native/index.ts +90 -1293
- package/src/frontend/native/media.ts +43 -0
- package/src/frontend/native/models.ts +221 -0
- package/src/frontend/native/queue.ts +47 -0
- package/src/frontend/native/reset.ts +50 -0
- package/src/frontend/native/routes/chats.ts +115 -0
- package/src/frontend/native/routes/daemon.ts +70 -0
- package/src/frontend/native/routes/host.ts +151 -0
- package/src/frontend/native/routes/index.ts +22 -0
- package/src/frontend/native/routes/mesh.ts +72 -0
- package/src/frontend/native/routes/models.ts +54 -0
- package/src/frontend/native/routes/params.ts +29 -0
- package/src/frontend/native/routes/pre-auth.ts +94 -0
- package/src/frontend/native/routes/table.ts +92 -0
- package/src/frontend/native/runtime.ts +109 -0
- package/src/frontend/native/server.ts +30 -555
- package/src/frontend/native/status.ts +26 -0
- package/src/frontend/native/tool-result.ts +48 -0
- package/src/frontend/native/turn.ts +341 -0
- package/src/frontend/whatsapp/access.ts +67 -0
- package/src/frontend/whatsapp/connection.ts +280 -0
- package/src/frontend/whatsapp/inbound.ts +327 -0
- package/src/frontend/whatsapp/index.ts +28 -599
- package/src/frontend/whatsapp/runtime.ts +74 -0
- package/src/plugins/github/provision.ts +1 -1
- package/src/storage/metrics.ts +18 -0
- package/src/storage/session-record.ts +29 -0
- package/src/storage/sessions.ts +24 -0
- package/src/util/boot-timer.ts +31 -0
- package/src/util/concurrency.ts +28 -0
- package/src/frontend/discord/callbacks/components.ts +0 -793
- package/src/frontend/discord/callbacks/shared.ts +0 -22
package/src/cli/setup.ts
CHANGED
|
@@ -7,67 +7,88 @@ import * as p from "@clack/prompts";
|
|
|
7
7
|
import pc from "picocolors";
|
|
8
8
|
import { printBanner, loadConfig, saveConfig, type Config } from "./config.js";
|
|
9
9
|
|
|
10
|
+
/** Strip spaces, "+", brackets and dashes from a phone number. */
|
|
11
|
+
const bareDigits = (raw: string) => raw.replace(/[\s+()-]/g, "");
|
|
12
|
+
|
|
10
13
|
/** "353871234567, +44 7700 900000" → ["353871234567", "447700900000"]. */
|
|
11
14
|
function parseNumberList(raw: string): string[] {
|
|
12
15
|
return raw
|
|
13
16
|
.split(",")
|
|
14
|
-
.map(
|
|
17
|
+
.map(bareDigits)
|
|
15
18
|
.filter((entry) => entry.length > 0);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
p.intro(pc.inverse(" Setup Wizard "));
|
|
21
|
+
/** Empty answers mean "unset"; everything else is trimmed. */
|
|
22
|
+
const trimmedOrUndefined = (raw: string) => raw.trim() || undefined;
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Await a clack prompt; on Esc/Ctrl-C say so and leave the wizard.
|
|
26
|
+
*
|
|
27
|
+
* Every prompt used to be followed by the same four-line `isCancel` guard,
|
|
28
|
+
* and because `@clack/core` narrows `isCancel` to its own unique symbol
|
|
29
|
+
* the unguarded remainder still needed an `as string` cast. clack only
|
|
30
|
+
* ever resolves a symbol to mean "cancelled", so narrowing on `typeof`
|
|
31
|
+
* here removes both.
|
|
32
|
+
*/
|
|
33
|
+
async function askOrExit<T>(prompt: Promise<T | symbol>): Promise<T> {
|
|
34
|
+
const value = await prompt;
|
|
35
|
+
if (typeof value === "symbol") {
|
|
36
|
+
p.cancel("Cancelled.");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Steps: each prompts for one section and returns its slice of the answers ──
|
|
43
|
+
|
|
44
|
+
async function askFrontends(config: Config): Promise<string[]> {
|
|
23
45
|
const existingFrontends = Array.isArray(config.frontend)
|
|
24
46
|
? config.frontend
|
|
25
47
|
: [config.frontend || "telegram"];
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const selectedFrontends = frontendSelection as string[];
|
|
49
|
+
return askOrExit(
|
|
50
|
+
p.multiselect({
|
|
51
|
+
message: "Frontend platforms (space to toggle, enter to confirm)",
|
|
52
|
+
initialValues: existingFrontends,
|
|
53
|
+
options: [
|
|
54
|
+
{
|
|
55
|
+
value: "telegram",
|
|
56
|
+
label: `Telegram ${pc.dim("— bot via @BotFather")}`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
value: "whatsapp",
|
|
60
|
+
label: `WhatsApp ${pc.dim("— a real account via Baileys multi-device")}`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: "discord",
|
|
64
|
+
label: `Discord ${pc.dim("— bot via Developer Portal (discord.js v14)")}`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
value: "teams",
|
|
68
|
+
label: `Teams ${pc.dim("— Microsoft Teams via Power Automate")}`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
value: "terminal",
|
|
72
|
+
label: `Terminal ${pc.dim("— local CLI chat")}`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
value: "native",
|
|
76
|
+
label: `Native ${pc.dim("— HTTP+SSE bridge for the companion app")}`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
required: true,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
type TelegramAnswers = Pick<
|
|
85
|
+
SetupAnswers,
|
|
86
|
+
"botToken" | "adminId" | "apiId" | "apiHash"
|
|
87
|
+
>;
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
async function askTelegram(config: Config): Promise<TelegramAnswers> {
|
|
90
|
+
const botToken = await askOrExit(
|
|
91
|
+
p.text({
|
|
71
92
|
message: "Bot token",
|
|
72
93
|
placeholder: "Paste your token from @BotFather",
|
|
73
94
|
initialValue: config.botToken || undefined,
|
|
@@ -75,76 +96,71 @@ export async function runSetup(): Promise<void> {
|
|
|
75
96
|
if (!v) return "Token is required";
|
|
76
97
|
if (!v.includes(":")) return "Invalid format";
|
|
77
98
|
},
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
p.cancel("Cancelled.");
|
|
81
|
-
process.exit(0);
|
|
82
|
-
}
|
|
83
|
-
botToken = token;
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
84
101
|
|
|
85
|
-
|
|
102
|
+
const adminId = await askOrExit(
|
|
103
|
+
p.text({
|
|
86
104
|
message: "Your Telegram user ID",
|
|
87
105
|
placeholder: "optional — message @userinfobot to find yours",
|
|
88
106
|
initialValue: config.adminUserId ? String(config.adminUserId) : "",
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
p.cancel("Cancelled.");
|
|
92
|
-
process.exit(0);
|
|
93
|
-
}
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
94
109
|
|
|
95
|
-
|
|
110
|
+
const wantUserbot = await askOrExit(
|
|
111
|
+
p.confirm({
|
|
96
112
|
message: "Set up userbot for full history access?",
|
|
97
113
|
initialValue: !!(config.apiId && config.apiHash),
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (id) apiId = parseInt(id, 10);
|
|
130
|
-
if (hash) apiHash = hash as string;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let teamsWebhookUrl: string | undefined;
|
|
135
|
-
let teamsWebhookSecret: string | undefined;
|
|
136
|
-
let teamsWebhookPort: number | undefined;
|
|
137
|
-
let teamsBotDisplayName: string | undefined;
|
|
138
|
-
|
|
139
|
-
if (selectedFrontends.includes("teams")) {
|
|
140
|
-
p.note(
|
|
141
|
-
"Set up two Power Automate workflows in Teams:\n" +
|
|
142
|
-
"1. Send: 'Post to a channel when a webhook request is received' — copy the URL below\n" +
|
|
143
|
-
"2. Receive: 'When a new channel message is added' → HTTP POST to your Talon endpoint",
|
|
144
|
-
"Teams Setup",
|
|
145
|
-
);
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
if (!wantUserbot) return { botToken, adminId };
|
|
117
|
+
|
|
118
|
+
p.note(
|
|
119
|
+
"Get these from https://my.telegram.org → API development tools",
|
|
120
|
+
"Telegram API credentials",
|
|
121
|
+
);
|
|
122
|
+
const id = await askOrExit(
|
|
123
|
+
p.text({
|
|
124
|
+
message: "API ID",
|
|
125
|
+
placeholder: "12345678",
|
|
126
|
+
initialValue: config.apiId ? String(config.apiId) : "",
|
|
127
|
+
validate: (v) => {
|
|
128
|
+
if (v && isNaN(parseInt(v, 10))) return "Must be a number";
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
const hash = await askOrExit(
|
|
133
|
+
p.text({
|
|
134
|
+
message: "API Hash",
|
|
135
|
+
initialValue: config.apiHash || "",
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
botToken,
|
|
140
|
+
adminId,
|
|
141
|
+
...(id ? { apiId: parseInt(id, 10) } : {}),
|
|
142
|
+
...(hash ? { apiHash: hash } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
146
145
|
|
|
147
|
-
|
|
146
|
+
type TeamsAnswers = Pick<
|
|
147
|
+
SetupAnswers,
|
|
148
|
+
| "teamsWebhookUrl"
|
|
149
|
+
| "teamsWebhookSecret"
|
|
150
|
+
| "teamsWebhookPort"
|
|
151
|
+
| "teamsBotDisplayName"
|
|
152
|
+
>;
|
|
153
|
+
|
|
154
|
+
async function askTeams(config: Config): Promise<TeamsAnswers> {
|
|
155
|
+
p.note(
|
|
156
|
+
"Set up two Power Automate workflows in Teams:\n" +
|
|
157
|
+
"1. Send: 'Post to a channel when a webhook request is received' — copy the URL below\n" +
|
|
158
|
+
"2. Receive: 'When a new channel message is added' → HTTP POST to your Talon endpoint",
|
|
159
|
+
"Teams Setup",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const teamsWebhookUrl = await askOrExit(
|
|
163
|
+
p.text({
|
|
148
164
|
message: "Power Automate webhook URL (for sending to Teams)",
|
|
149
165
|
placeholder: "https://prod-XX.westus.logic.azure.com/workflows/...",
|
|
150
166
|
initialValue: config.teamsWebhookUrl || undefined,
|
|
@@ -156,25 +172,19 @@ export async function runSetup(): Promise<void> {
|
|
|
156
172
|
return "Must be a valid URL";
|
|
157
173
|
}
|
|
158
174
|
},
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
p.cancel("Cancelled.");
|
|
162
|
-
process.exit(0);
|
|
163
|
-
}
|
|
164
|
-
teamsWebhookUrl = url;
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
165
177
|
|
|
166
|
-
|
|
178
|
+
const secret = await askOrExit(
|
|
179
|
+
p.text({
|
|
167
180
|
message: "Webhook secret for inbound verification",
|
|
168
181
|
placeholder: "optional — shared secret to verify incoming webhooks",
|
|
169
182
|
initialValue: config.teamsWebhookSecret || "",
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (secret) teamsWebhookSecret = secret;
|
|
176
|
-
|
|
177
|
-
const port = await p.text({
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const port = await askOrExit(
|
|
187
|
+
p.text({
|
|
178
188
|
message: "Webhook receiver port",
|
|
179
189
|
placeholder: "19878",
|
|
180
190
|
initialValue: config.teamsWebhookPort
|
|
@@ -185,99 +195,92 @@ export async function runSetup(): Promise<void> {
|
|
|
185
195
|
const n = parseInt(v, 10);
|
|
186
196
|
if (isNaN(n) || n < 1024 || n > 65535) return "Port must be 1024-65535";
|
|
187
197
|
},
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
p.cancel("Cancelled.");
|
|
191
|
-
process.exit(0);
|
|
192
|
-
}
|
|
193
|
-
teamsWebhookPort = parseInt(port as string, 10);
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
const botName = await askOrExit(
|
|
202
|
+
p.text({
|
|
196
203
|
message: "Bot display name in Teams (for echo loop prevention)",
|
|
197
204
|
placeholder: "optional — e.g. 'Talon Bot'",
|
|
198
205
|
initialValue: config.teamsBotDisplayName || "",
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
p.cancel("Cancelled.");
|
|
202
|
-
process.exit(0);
|
|
203
|
-
}
|
|
204
|
-
if (botName) teamsBotDisplayName = botName;
|
|
205
|
-
}
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
206
208
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
return {
|
|
210
|
+
teamsWebhookUrl,
|
|
211
|
+
teamsWebhookPort: parseInt(port, 10),
|
|
212
|
+
...(secret ? { teamsWebhookSecret: secret } : {}),
|
|
213
|
+
...(botName ? { teamsBotDisplayName: botName } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
type DiscordAnswers = Pick<
|
|
218
|
+
SetupAnswers,
|
|
219
|
+
"discordBotToken" | "discordApplicationId"
|
|
220
|
+
>;
|
|
221
|
+
|
|
222
|
+
async function askDiscord(config: Config): Promise<DiscordAnswers> {
|
|
223
|
+
p.note(
|
|
224
|
+
"Get bot token + application ID from\n" +
|
|
225
|
+
"https://discord.com/developers/applications → your app → Bot",
|
|
226
|
+
"Discord Setup",
|
|
227
|
+
);
|
|
216
228
|
|
|
217
|
-
|
|
229
|
+
const discordBotToken = await askOrExit(
|
|
230
|
+
p.text({
|
|
218
231
|
message: "Discord bot token",
|
|
219
232
|
placeholder: "MTxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
220
233
|
initialValue: config.discord?.botToken || undefined,
|
|
221
234
|
validate: (v) => {
|
|
222
235
|
if (!v) return "Bot token is required";
|
|
223
236
|
},
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
p.cancel("Cancelled.");
|
|
227
|
-
process.exit(0);
|
|
228
|
-
}
|
|
229
|
-
discordBotToken = token as string;
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
230
239
|
|
|
231
|
-
|
|
240
|
+
const discordApplicationId = await askOrExit(
|
|
241
|
+
p.text({
|
|
232
242
|
message: "Discord application ID",
|
|
233
243
|
placeholder: "1234567890123456789",
|
|
234
244
|
initialValue: config.discord?.applicationId || undefined,
|
|
235
245
|
validate: (v) => {
|
|
236
246
|
if (!v) return "Application ID is required";
|
|
237
247
|
},
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
p.cancel("Cancelled.");
|
|
241
|
-
process.exit(0);
|
|
242
|
-
}
|
|
243
|
-
discordApplicationId = appId as string;
|
|
244
|
-
}
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
245
250
|
|
|
246
|
-
|
|
251
|
+
return { discordBotToken, discordApplicationId };
|
|
252
|
+
}
|
|
247
253
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
async function askWhatsApp(config: Config): Promise<Config["whatsapp"]> {
|
|
255
|
+
p.note(
|
|
256
|
+
"Talon drives a real WhatsApp account over Baileys multi-device —\n" +
|
|
257
|
+
"the same mechanism as WhatsApp Web. On first start you'll pair it\n" +
|
|
258
|
+
"under WhatsApp → Linked devices.",
|
|
259
|
+
"WhatsApp Setup",
|
|
260
|
+
);
|
|
255
261
|
|
|
256
|
-
|
|
262
|
+
const pairing = await askOrExit(
|
|
263
|
+
p.text({
|
|
257
264
|
message: "The bot account's own number (E.164 digits, no +)",
|
|
258
265
|
placeholder: "leave blank to pair by QR code instead",
|
|
259
266
|
initialValue: config.whatsapp?.pairingNumber || "",
|
|
260
267
|
validate: (v) => {
|
|
261
|
-
if (v && !/^\d{7,15}$/.test(v
|
|
268
|
+
if (v && !/^\d{7,15}$/.test(bareDigits(v)))
|
|
262
269
|
return "Digits only, country code included — e.g. 353871234567";
|
|
263
270
|
},
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
p.cancel("Cancelled.");
|
|
267
|
-
process.exit(0);
|
|
268
|
-
}
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
269
273
|
|
|
270
|
-
|
|
274
|
+
const allowed = await askOrExit(
|
|
275
|
+
p.text({
|
|
271
276
|
message: "Numbers allowed to DM the bot (comma-separated)",
|
|
272
277
|
placeholder: "353871234567, 447700900000 — blank disables DMs",
|
|
273
278
|
initialValue: (config.whatsapp?.allowedJids ?? []).join(", "),
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
p.cancel("Cancelled.");
|
|
277
|
-
process.exit(0);
|
|
278
|
-
}
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
279
281
|
|
|
280
|
-
|
|
282
|
+
const groupPolicy = await askOrExit(
|
|
283
|
+
p.select<"listed" | "with-allowed-user" | "all">({
|
|
281
284
|
message: "Which groups should the bot serve?",
|
|
282
285
|
initialValue: config.whatsapp?.groupPolicy ?? "listed",
|
|
283
286
|
options: [
|
|
@@ -294,25 +297,22 @@ export async function runSetup(): Promise<void> {
|
|
|
294
297
|
label: `All ${pc.dim("— every group the account belongs to")}`,
|
|
295
298
|
},
|
|
296
299
|
],
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
p.cancel("Cancelled.");
|
|
300
|
-
process.exit(0);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
whatsapp = {
|
|
304
|
-
// Preserve allowedGroups / respondMode / sendReadReceipts — the
|
|
305
|
-
// wizard doesn't prompt for them but must not drop them.
|
|
306
|
-
...config.whatsapp,
|
|
307
|
-
allowedJids: parseNumberList(allowed),
|
|
308
|
-
groupPolicy: groupPolicy as "listed" | "with-allowed-user" | "all",
|
|
309
|
-
...(pairing.replace(/[\s+()-]/g, "")
|
|
310
|
-
? { pairingNumber: pairing.replace(/[\s+()-]/g, "") }
|
|
311
|
-
: {}),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
314
302
|
|
|
315
|
-
|
|
303
|
+
const pairingNumber = bareDigits(pairing);
|
|
304
|
+
return {
|
|
305
|
+
// Preserve allowedGroups / respondMode / sendReadReceipts — the
|
|
306
|
+
// wizard doesn't prompt for them but must not drop them.
|
|
307
|
+
...config.whatsapp,
|
|
308
|
+
allowedJids: parseNumberList(allowed),
|
|
309
|
+
groupPolicy,
|
|
310
|
+
...(pairingNumber ? { pairingNumber } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Discover models from the SDK; fall back to the static list if it isn't installed. */
|
|
315
|
+
async function discoverModels(config: Config) {
|
|
316
316
|
const {
|
|
317
317
|
registerClaudeModels,
|
|
318
318
|
registerClaudeModelsStatic,
|
|
@@ -334,171 +334,197 @@ export async function runSetup(): Promise<void> {
|
|
|
334
334
|
registerClaudeModelsStatic(CLAUDE_MODELS_STATIC);
|
|
335
335
|
}
|
|
336
336
|
const { getModels } = await import("../core/models/catalog.js");
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const model = await p.select({
|
|
340
|
-
message: "Default model",
|
|
341
|
-
initialValue: config.model,
|
|
342
|
-
options: registeredModels.map((m) => ({
|
|
343
|
-
value: m.id,
|
|
344
|
-
label: `${m.displayName.padEnd(12)}${m.description ? pc.dim(`— ${m.description}`) : ""}`,
|
|
345
|
-
})),
|
|
346
|
-
});
|
|
347
|
-
if (p.isCancel(model)) {
|
|
348
|
-
p.cancel("Cancelled.");
|
|
349
|
-
process.exit(0);
|
|
350
|
-
}
|
|
337
|
+
return getModels();
|
|
338
|
+
}
|
|
351
339
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
340
|
+
async function askModel(config: Config): Promise<string> {
|
|
341
|
+
const registeredModels = await discoverModels(config);
|
|
342
|
+
return askOrExit(
|
|
343
|
+
p.select({
|
|
344
|
+
message: "Default model",
|
|
345
|
+
initialValue: config.model,
|
|
346
|
+
options: registeredModels.map((m) => ({
|
|
347
|
+
value: m.id,
|
|
348
|
+
label: `${m.displayName.padEnd(12)}${m.description ? pc.dim(`— ${m.description}`) : ""}`,
|
|
349
|
+
})),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
362
353
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
{
|
|
377
|
-
value: "opencode",
|
|
378
|
-
label: `OpenCode ${pc.dim("— @opencode-ai/sdk")}`,
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
value: "codex",
|
|
382
|
-
label: `Codex ${pc.dim("— OpenAI Codex CLI (@openai/codex)")}`,
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
value: "openai-agents",
|
|
386
|
-
label: `OpenAI Agents ${pc.dim("— @openai/agents (OpenAI or any OpenAI-compatible endpoint)")}`,
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
});
|
|
390
|
-
if (p.isCancel(backendSelection)) {
|
|
391
|
-
p.cancel("Cancelled.");
|
|
392
|
-
process.exit(0);
|
|
393
|
-
}
|
|
394
|
-
const backend = backendSelection as Config["backend"];
|
|
354
|
+
/** Pulse only makes sense with a chat platform — terminal-only installs skip it. */
|
|
355
|
+
async function askPulse(
|
|
356
|
+
config: Config,
|
|
357
|
+
selectedFrontends: string[],
|
|
358
|
+
): Promise<boolean> {
|
|
359
|
+
if (selectedFrontends.every((f) => f === "terminal")) return false;
|
|
360
|
+
return askOrExit(
|
|
361
|
+
p.confirm({
|
|
362
|
+
message: "Enable pulse? (periodic group engagement)",
|
|
363
|
+
initialValue: config.pulse,
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
395
367
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
368
|
+
async function askBackend(config: Config): Promise<Config["backend"]> {
|
|
369
|
+
return askOrExit(
|
|
370
|
+
p.select({
|
|
371
|
+
message: "AI backend",
|
|
372
|
+
initialValue: config.backend ?? "claude",
|
|
373
|
+
options: [
|
|
374
|
+
{
|
|
375
|
+
value: "claude",
|
|
376
|
+
label: `Claude ${pc.dim("— Anthropic Claude Agent SDK")}`,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
value: "kilo",
|
|
380
|
+
label: `Kilo ${pc.dim("— @kilocode/sdk (multi-provider routing)")}`,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
value: "opencode",
|
|
384
|
+
label: `OpenCode ${pc.dim("— @opencode-ai/sdk")}`,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
value: "codex",
|
|
388
|
+
label: `Codex ${pc.dim("— OpenAI Codex CLI (@openai/codex)")}`,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
value: "openai-agents",
|
|
392
|
+
label: `OpenAI Agents ${pc.dim("— @openai/agents (OpenAI or any OpenAI-compatible endpoint)")}`,
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
402
398
|
|
|
403
|
-
|
|
404
|
-
|
|
399
|
+
type BackendAnswers = Pick<
|
|
400
|
+
SetupAnswers,
|
|
401
|
+
| "claudeBinary"
|
|
402
|
+
| "codexApiKey"
|
|
403
|
+
| "openaiApiKey"
|
|
404
|
+
| "openaiBaseUrl"
|
|
405
|
+
| "openaiApiMode"
|
|
406
|
+
>;
|
|
407
|
+
|
|
408
|
+
async function askClaudeBackend(config: Config): Promise<BackendAnswers> {
|
|
409
|
+
const claudeBinary = await askOrExit(
|
|
410
|
+
p.text({
|
|
405
411
|
message: "Claude Code binary path",
|
|
406
412
|
placeholder: "leave empty for default (claude)",
|
|
407
413
|
initialValue: config.claudeBinary || "",
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
return { claudeBinary: trimmedOrUndefined(claudeBinary) };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function askCodexBackend(config: Config): Promise<BackendAnswers> {
|
|
420
|
+
const codexApiKey = await askOrExit(
|
|
421
|
+
p.text({
|
|
416
422
|
message: "Codex OpenAI API key",
|
|
417
423
|
placeholder:
|
|
418
424
|
"leave empty to use CODEX_API_KEY / TALON_CODEX_KEY env or `codex login` auth",
|
|
419
425
|
initialValue: config.codexApiKey || "",
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
{
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
{
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
return { codexApiKey: trimmedOrUndefined(codexApiKey) };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function askOpenAiAgentsBackend(config: Config): Promise<BackendAnswers> {
|
|
432
|
+
const openaiApiKey = trimmedOrUndefined(
|
|
433
|
+
await askOrExit(
|
|
434
|
+
p.text({
|
|
435
|
+
message:
|
|
436
|
+
"API key (OpenAI, OpenRouter, Azure, or whatever your endpoint requires)",
|
|
437
|
+
placeholder: "leave empty to use OPENAI_API_KEY env",
|
|
438
|
+
initialValue: config.openaiApiKey || "",
|
|
439
|
+
}),
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const openaiBaseUrl = trimmedOrUndefined(
|
|
444
|
+
await askOrExit(
|
|
445
|
+
p.text({
|
|
446
|
+
message:
|
|
447
|
+
"Base URL " +
|
|
448
|
+
pc.dim(
|
|
449
|
+
"(leave empty for OpenAI direct; e.g. https://openrouter.ai/api/v1)",
|
|
450
|
+
),
|
|
451
|
+
placeholder: "https://openrouter.ai/api/v1",
|
|
452
|
+
initialValue: config.openaiBaseUrl || "",
|
|
453
|
+
}),
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
if (!openaiBaseUrl) return { openaiApiKey, openaiBaseUrl };
|
|
457
|
+
|
|
458
|
+
const openaiApiMode = await askOrExit(
|
|
459
|
+
p.select<NonNullable<Config["openaiApiMode"]>>({
|
|
460
|
+
message: "OpenAI API surface",
|
|
461
|
+
options: [
|
|
462
|
+
{
|
|
463
|
+
value: "chat_completions",
|
|
464
|
+
label: `Chat Completions ${pc.dim("— most third parties (OpenRouter, Ollama, LiteLLM, most Azure)")}`,
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
value: "responses",
|
|
468
|
+
label: `Responses ${pc.dim("— OpenAI native, requires proxy support")}`,
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
initialValue: config.openaiApiMode ?? "chat_completions",
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
return { openaiApiKey, openaiBaseUrl, openaiApiMode };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* kilo / opencode need no extra prompts — bundled SDK + per-provider creds
|
|
479
|
+
* configured separately (kilo via `kilo login`, opencode via its own auth
|
|
480
|
+
* flow).
|
|
481
|
+
*/
|
|
482
|
+
async function askBackendCredentials(
|
|
483
|
+
config: Config,
|
|
484
|
+
backend: Config["backend"],
|
|
485
|
+
): Promise<BackendAnswers> {
|
|
486
|
+
switch (backend) {
|
|
487
|
+
case "claude":
|
|
488
|
+
return askClaudeBackend(config);
|
|
489
|
+
case "codex":
|
|
490
|
+
return askCodexBackend(config);
|
|
491
|
+
case "openai-agents":
|
|
492
|
+
return askOpenAiAgentsBackend(config);
|
|
493
|
+
default:
|
|
494
|
+
return {};
|
|
475
495
|
}
|
|
496
|
+
}
|
|
476
497
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
498
|
+
// ── The wizard: the steps above, in order ──
|
|
499
|
+
|
|
500
|
+
export async function runSetup(): Promise<void> {
|
|
501
|
+
printBanner();
|
|
502
|
+
p.intro(pc.inverse(" Setup Wizard "));
|
|
503
|
+
|
|
504
|
+
const config = loadConfig();
|
|
505
|
+
const selectedFrontends = await askFrontends(config);
|
|
506
|
+
const on = (id: string) => selectedFrontends.includes(id);
|
|
507
|
+
|
|
508
|
+
const telegram = on("telegram") ? await askTelegram(config) : {};
|
|
509
|
+
const teams = on("teams") ? await askTeams(config) : {};
|
|
510
|
+
const discord = on("discord") ? await askDiscord(config) : {};
|
|
511
|
+
const whatsapp = on("whatsapp") ? await askWhatsApp(config) : undefined;
|
|
512
|
+
|
|
513
|
+
const model = await askModel(config);
|
|
514
|
+
const pulse = await askPulse(config, selectedFrontends);
|
|
515
|
+
const backend = await askBackend(config);
|
|
516
|
+
const credentials = await askBackendCredentials(config, backend);
|
|
480
517
|
|
|
481
518
|
const newConfig = buildSetupConfig(config, {
|
|
482
519
|
selectedFrontends,
|
|
483
520
|
backend,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
codexApiKey,
|
|
487
|
-
openaiApiKey,
|
|
488
|
-
openaiBaseUrl,
|
|
489
|
-
openaiApiMode,
|
|
490
|
-
model: model as string,
|
|
491
|
-
pulse: pulse as boolean,
|
|
492
|
-
adminId,
|
|
493
|
-
apiId,
|
|
494
|
-
apiHash,
|
|
495
|
-
teamsWebhookUrl,
|
|
496
|
-
teamsWebhookSecret,
|
|
497
|
-
teamsWebhookPort,
|
|
498
|
-
teamsBotDisplayName,
|
|
499
|
-
discordBotToken,
|
|
500
|
-
discordApplicationId,
|
|
521
|
+
model,
|
|
522
|
+
pulse,
|
|
501
523
|
whatsapp,
|
|
524
|
+
...telegram,
|
|
525
|
+
...teams,
|
|
526
|
+
...discord,
|
|
527
|
+
...credentials,
|
|
502
528
|
});
|
|
503
529
|
|
|
504
530
|
const s = p.spinner();
|
|
@@ -508,7 +534,7 @@ export async function runSetup(): Promise<void> {
|
|
|
508
534
|
|
|
509
535
|
p.outro(`Run ${pc.cyan(pc.bold("talon start"))} to launch Talon`);
|
|
510
536
|
|
|
511
|
-
if (
|
|
537
|
+
if (on("telegram") && telegram.apiId && telegram.apiHash) {
|
|
512
538
|
console.log(
|
|
513
539
|
` ${pc.yellow("!")} Run ${pc.cyan("npx tsx src/login.ts")} to authenticate the userbot first.\n`,
|
|
514
540
|
);
|