@lobu/cli 3.0.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/README.md +276 -0
- package/bin/create-lobu.js +5 -0
- package/bin/lobu.js +5 -0
- package/dist/api/client.d.ts +11 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +35 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/credentials.d.ts +13 -0
- package/dist/api/credentials.d.ts.map +1 -0
- package/dist/api/credentials.js +39 -0
- package/dist/api/credentials.js.map +1 -0
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +128 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +905 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/launch.d.ts +6 -0
- package/dist/commands/launch.d.ts.map +1 -0
- package/dist/commands/launch.js +32 -0
- package/dist/commands/launch.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +29 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +7 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/providers/add.d.ts +2 -0
- package/dist/commands/providers/add.d.ts.map +1 -0
- package/dist/commands/providers/add.js +51 -0
- package/dist/commands/providers/add.js.map +1 -0
- package/dist/commands/providers/list.d.ts +2 -0
- package/dist/commands/providers/list.d.ts.map +1 -0
- package/dist/commands/providers/list.js +21 -0
- package/dist/commands/providers/list.js.map +1 -0
- package/dist/commands/secrets.d.ts +8 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +98 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/skills/add.d.ts +2 -0
- package/dist/commands/skills/add.d.ts.map +1 -0
- package/dist/commands/skills/add.js +47 -0
- package/dist/commands/skills/add.js.map +1 -0
- package/dist/commands/skills/info.d.ts +2 -0
- package/dist/commands/skills/info.d.ts.map +1 -0
- package/dist/commands/skills/info.js +45 -0
- package/dist/commands/skills/info.js.map +1 -0
- package/dist/commands/skills/list.d.ts +2 -0
- package/dist/commands/skills/list.d.ts.map +1 -0
- package/dist/commands/skills/list.js +30 -0
- package/dist/commands/skills/list.js.map +1 -0
- package/dist/commands/skills/registry.d.ts +34 -0
- package/dist/commands/skills/registry.d.ts.map +1 -0
- package/dist/commands/skills/registry.js +38 -0
- package/dist/commands/skills/registry.js.map +1 -0
- package/dist/commands/skills/search.d.ts +2 -0
- package/dist/commands/skills/search.d.ts.map +1 -0
- package/dist/commands/skills/search.js +21 -0
- package/dist/commands/skills/search.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +7 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +73 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +25 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config/loader.d.ts +16 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +41 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +279 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +52 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/transformer.d.ts +11 -0
- package/dist/config/transformer.d.ts.map +1 -0
- package/dist/config/transformer.js +49 -0
- package/dist/config/transformer.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-servers.d.ts +11 -0
- package/dist/mcp-servers.d.ts.map +1 -0
- package/dist/mcp-servers.js +27 -0
- package/dist/mcp-servers.js.map +1 -0
- package/dist/mcp-servers.json +216 -0
- package/dist/templates/.env.tmpl +29 -0
- package/dist/templates/.gitignore.tmpl +32 -0
- package/dist/templates/AGENTS.md.tmpl +1 -0
- package/dist/templates/Dockerfile.worker.tmpl +29 -0
- package/dist/templates/README.md.tmpl +95 -0
- package/dist/templates/TESTING.md.tmpl +225 -0
- package/dist/templates/lobu.toml.tmpl +44 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +13 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/template.d.ts +2 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +19 -0
- package/dist/utils/template.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
import { getRequiredEnvVars, MCP_SERVERS, } from "../mcp-servers.js";
|
|
10
|
+
import { renderTemplate } from "../utils/template.js";
|
|
11
|
+
const DEFAULT_SLACK_MANIFEST = {
|
|
12
|
+
display_information: {
|
|
13
|
+
name: "Lobu",
|
|
14
|
+
description: "Hire AI peers to work with you, using your environments",
|
|
15
|
+
background_color: "#4a154b",
|
|
16
|
+
long_description: "This bot integrates Claude Code SDK with Slack to provide AI-powered coding assistance directly in your workspace. You can generate apps/AI peers that will appear as new handles.",
|
|
17
|
+
},
|
|
18
|
+
features: {
|
|
19
|
+
app_home: {
|
|
20
|
+
home_tab_enabled: true,
|
|
21
|
+
messages_tab_enabled: true,
|
|
22
|
+
messages_tab_read_only_enabled: false,
|
|
23
|
+
},
|
|
24
|
+
bot_user: {
|
|
25
|
+
display_name: "Lobu",
|
|
26
|
+
always_online: true,
|
|
27
|
+
},
|
|
28
|
+
slash_commands: [
|
|
29
|
+
{
|
|
30
|
+
command: "/lobu",
|
|
31
|
+
description: "Lobu commands - manage repositories and authentication",
|
|
32
|
+
usage_hint: "connect | login | help",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
assistant_view: {
|
|
36
|
+
assistant_description: "It can generate Claude Code session working on public Github data",
|
|
37
|
+
suggested_prompts: [
|
|
38
|
+
{
|
|
39
|
+
title: "Create a project",
|
|
40
|
+
message: "Create a new project",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Start working on a feature",
|
|
44
|
+
message: "List me projects and let me tell you what I want to develop on which project",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Fix a bug",
|
|
48
|
+
message: "List me projects and let me tell you what I want to develop on which project",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: "Ask a question to the codebase",
|
|
52
|
+
message: "List me projects and let me tell you what I want to develop on which project",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
oauth_config: {
|
|
58
|
+
redirect_urls: [],
|
|
59
|
+
scopes: {
|
|
60
|
+
bot: [
|
|
61
|
+
"app_mentions:read",
|
|
62
|
+
"assistant:write",
|
|
63
|
+
"channels:history",
|
|
64
|
+
"channels:read",
|
|
65
|
+
"chat:write",
|
|
66
|
+
"chat:write.public",
|
|
67
|
+
"groups:history",
|
|
68
|
+
"groups:read",
|
|
69
|
+
"im:history",
|
|
70
|
+
"im:read",
|
|
71
|
+
"im:write",
|
|
72
|
+
"files:read",
|
|
73
|
+
"files:write",
|
|
74
|
+
"mpim:read",
|
|
75
|
+
"reactions:read",
|
|
76
|
+
"reactions:write",
|
|
77
|
+
"users:read",
|
|
78
|
+
"commands",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
settings: {
|
|
83
|
+
event_subscriptions: {
|
|
84
|
+
bot_events: [
|
|
85
|
+
"app_home_opened",
|
|
86
|
+
"app_mention",
|
|
87
|
+
"team_join",
|
|
88
|
+
"member_joined_channel",
|
|
89
|
+
"message.channels",
|
|
90
|
+
"message.groups",
|
|
91
|
+
"message.im",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
interactivity: {
|
|
95
|
+
is_enabled: true,
|
|
96
|
+
},
|
|
97
|
+
org_deploy_enabled: false,
|
|
98
|
+
socket_mode_enabled: true,
|
|
99
|
+
token_rotation_enabled: false,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
export async function initCommand(cwd = process.cwd(), projectNameArg) {
|
|
103
|
+
console.log(chalk.bold.cyan("\n🤖 Welcome to Lobu!\n"));
|
|
104
|
+
// Get CLI version
|
|
105
|
+
const cliVersion = await getCliVersion();
|
|
106
|
+
// Validate project name if provided as argument
|
|
107
|
+
if (projectNameArg && !/^[a-z0-9-]+$/.test(projectNameArg)) {
|
|
108
|
+
console.log(chalk.red("\n✗ Project name must be lowercase alphanumeric with hyphens only\n"));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
// Interactive prompts - basic setup
|
|
112
|
+
const baseAnswers = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "input",
|
|
115
|
+
name: "projectName",
|
|
116
|
+
message: "Project name?",
|
|
117
|
+
default: projectNameArg || "my-lobu",
|
|
118
|
+
validate: (input) => {
|
|
119
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
120
|
+
return "Project name must be lowercase alphanumeric with hyphens only";
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
},
|
|
124
|
+
when: !projectNameArg, // Skip prompt if project name provided as argument
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
// Use project name from argument or prompt
|
|
128
|
+
const projectName = projectNameArg || baseAnswers.projectName;
|
|
129
|
+
const projectDir = join(cwd, projectName);
|
|
130
|
+
try {
|
|
131
|
+
await access(projectDir, constants.F_OK);
|
|
132
|
+
console.log(chalk.red(`\n✗ Directory "${projectName}" already exists. Please choose a different project name or remove the existing directory.\n`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Directory doesn't exist - good to proceed
|
|
137
|
+
await mkdir(projectDir, { recursive: true });
|
|
138
|
+
console.log(chalk.dim(`\nCreating project in: ${chalk.cyan(projectDir)}\n`));
|
|
139
|
+
}
|
|
140
|
+
// MCP Server selection
|
|
141
|
+
const { configureMcp } = await inquirer.prompt([
|
|
142
|
+
{
|
|
143
|
+
type: "confirm",
|
|
144
|
+
name: "configureMcp",
|
|
145
|
+
message: "Would you like to configure MCP servers?",
|
|
146
|
+
default: true,
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
let selectedMcpServers = [];
|
|
150
|
+
let publicUrl = "";
|
|
151
|
+
if (configureMcp) {
|
|
152
|
+
const { mcpServers } = await inquirer.prompt([
|
|
153
|
+
{
|
|
154
|
+
type: "checkbox",
|
|
155
|
+
name: "mcpServers",
|
|
156
|
+
message: "Select MCP servers to configure (you can add more later):",
|
|
157
|
+
choices: [
|
|
158
|
+
...MCP_SERVERS.map((server) => ({
|
|
159
|
+
name: `${server.name} - ${server.description}`,
|
|
160
|
+
value: server.id,
|
|
161
|
+
checked: server.id === "github", // GitHub selected by default
|
|
162
|
+
})),
|
|
163
|
+
{
|
|
164
|
+
name: "Custom MCP server (provide URL)",
|
|
165
|
+
value: "custom",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
pageSize: 15,
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
selectedMcpServers = MCP_SERVERS.filter((s) => mcpServers.includes(s.id));
|
|
172
|
+
// Handle custom MCP server
|
|
173
|
+
if (mcpServers.includes("custom")) {
|
|
174
|
+
const { customMcpUrl, customMcpName } = await inquirer.prompt([
|
|
175
|
+
{
|
|
176
|
+
type: "input",
|
|
177
|
+
name: "customMcpName",
|
|
178
|
+
message: "Custom MCP server name:",
|
|
179
|
+
validate: (input) => {
|
|
180
|
+
if (!input || !/^[a-z0-9-]+$/.test(input)) {
|
|
181
|
+
return "Name must be lowercase alphanumeric with hyphens only";
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
type: "input",
|
|
188
|
+
name: "customMcpUrl",
|
|
189
|
+
message: "Custom MCP server URL:",
|
|
190
|
+
validate: (input) => {
|
|
191
|
+
if (!input) {
|
|
192
|
+
return "Please enter a valid URL";
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
new URL(input);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return "Please enter a valid URL (e.g., https://your-mcp-server.com)";
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
selectedMcpServers.push({
|
|
205
|
+
id: customMcpName,
|
|
206
|
+
name: customMcpName,
|
|
207
|
+
description: "Custom MCP server",
|
|
208
|
+
type: "none",
|
|
209
|
+
config: {
|
|
210
|
+
url: customMcpUrl,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Check if any OAuth servers were selected
|
|
215
|
+
const hasOAuthServers = selectedMcpServers.some((s) => s.type === "oauth");
|
|
216
|
+
if (hasOAuthServers) {
|
|
217
|
+
const { publicGatewayUrl } = await inquirer.prompt([
|
|
218
|
+
{
|
|
219
|
+
type: "input",
|
|
220
|
+
name: "publicGatewayUrl",
|
|
221
|
+
message: "Public Gateway URL (required for OAuth MCP servers):",
|
|
222
|
+
default: "http://localhost:8080",
|
|
223
|
+
validate: (input) => {
|
|
224
|
+
if (!input) {
|
|
225
|
+
return "Public URL is required when using OAuth-based MCP servers";
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
new URL(input);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return "Please enter a valid URL (e.g., https://your-domain.com)";
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
publicUrl = publicGatewayUrl;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Platform selection
|
|
241
|
+
const { selectedPlatforms } = await inquirer.prompt([
|
|
242
|
+
{
|
|
243
|
+
type: "checkbox",
|
|
244
|
+
name: "selectedPlatforms",
|
|
245
|
+
message: "Which messaging platform(s) do you want to configure?",
|
|
246
|
+
choices: [
|
|
247
|
+
{
|
|
248
|
+
name: "Telegram (quickest — 1 token from @BotFather)",
|
|
249
|
+
value: "telegram",
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "Slack (requires app creation + 3 credentials)",
|
|
253
|
+
value: "slack",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "API only (no chat platform, use REST endpoints)",
|
|
257
|
+
value: "api",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
validate: (input) => {
|
|
261
|
+
if (input.length === 0) {
|
|
262
|
+
return "Select at least one platform";
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
// Telegram setup
|
|
269
|
+
let telegramBotToken = "";
|
|
270
|
+
let telegramAllowFrom = "";
|
|
271
|
+
if (selectedPlatforms.includes("telegram")) {
|
|
272
|
+
console.log(chalk.bold("\n📱 Telegram Setup"));
|
|
273
|
+
console.log(chalk.dim(" Step 1: Open Telegram and message @BotFather"));
|
|
274
|
+
console.log(chalk.dim(" Step 2: Send /newbot and follow the prompts"));
|
|
275
|
+
console.log(chalk.dim(" Step 3: Copy the bot token\n"));
|
|
276
|
+
const telegramAnswers = await inquirer.prompt([
|
|
277
|
+
{
|
|
278
|
+
type: "password",
|
|
279
|
+
name: "telegramBotToken",
|
|
280
|
+
message: "Telegram Bot Token?",
|
|
281
|
+
validate: (input) => {
|
|
282
|
+
if (!input) {
|
|
283
|
+
return "Please enter your Telegram bot token";
|
|
284
|
+
}
|
|
285
|
+
if (!/^\d+:[A-Za-z0-9_-]+$/.test(input)) {
|
|
286
|
+
return "Invalid token format. Expected: digits:alphanumeric (e.g., 123456789:ABCdefGHI...)";
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
telegramBotToken = telegramAnswers.telegramBotToken;
|
|
293
|
+
// Validate token via Telegram API
|
|
294
|
+
const spinner = ora("Verifying Telegram bot token...").start();
|
|
295
|
+
try {
|
|
296
|
+
const response = await fetch(`https://api.telegram.org/bot${telegramBotToken}/getMe`);
|
|
297
|
+
const data = (await response.json());
|
|
298
|
+
if (data.ok && data.result?.username) {
|
|
299
|
+
spinner.succeed(`Bot verified: @${data.result.username}`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
spinner.warn("Could not verify bot token — check it after setup if needed");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
spinner.warn("Could not reach Telegram API — token will be saved as-is");
|
|
307
|
+
}
|
|
308
|
+
const { telegramAllowFromInput } = await inquirer.prompt([
|
|
309
|
+
{
|
|
310
|
+
type: "input",
|
|
311
|
+
name: "telegramAllowFromInput",
|
|
312
|
+
message: "Restrict to specific Telegram user IDs? (comma-separated, leave empty to allow all)",
|
|
313
|
+
default: "",
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
telegramAllowFrom = telegramAllowFromInput;
|
|
317
|
+
}
|
|
318
|
+
// Slack setup
|
|
319
|
+
let credentialAnswers = {
|
|
320
|
+
slackSigningSecret: "",
|
|
321
|
+
slackAppToken: "",
|
|
322
|
+
slackBotToken: "",
|
|
323
|
+
};
|
|
324
|
+
if (selectedPlatforms.includes("slack")) {
|
|
325
|
+
const { slackAppOption } = await inquirer.prompt([
|
|
326
|
+
{
|
|
327
|
+
type: "list",
|
|
328
|
+
name: "slackAppOption",
|
|
329
|
+
message: "Slack app setup?",
|
|
330
|
+
choices: [
|
|
331
|
+
{
|
|
332
|
+
name: "Create a new Slack app using the Lobu manifest",
|
|
333
|
+
value: "create",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "Use an existing Slack app",
|
|
337
|
+
value: "existing",
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
default: "create",
|
|
341
|
+
},
|
|
342
|
+
]);
|
|
343
|
+
if (slackAppOption === "create") {
|
|
344
|
+
const manifestUrl = await getSlackManifestUrl();
|
|
345
|
+
console.log(chalk.bold("\n🔗 Create your Slack app"));
|
|
346
|
+
console.log(`Open this link to create the app with the recommended manifest:\n${chalk.cyan(chalk.underline(manifestUrl))}\n`);
|
|
347
|
+
await inquirer.prompt([
|
|
348
|
+
{
|
|
349
|
+
type: "confirm",
|
|
350
|
+
name: "slackAppCreated",
|
|
351
|
+
message: "Press enter after clicking \u201CCreate\u201D and returning here to continue.",
|
|
352
|
+
default: true,
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
const { slackAppId } = await inquirer.prompt([
|
|
357
|
+
{
|
|
358
|
+
type: "input",
|
|
359
|
+
name: "slackAppId",
|
|
360
|
+
message: "Slack App ID (optional)?",
|
|
361
|
+
default: "",
|
|
362
|
+
},
|
|
363
|
+
]);
|
|
364
|
+
const trimmedAppId = slackAppId.trim();
|
|
365
|
+
const appIdForLinks = trimmedAppId !== "" ? trimmedAppId : "<YOUR_APP_ID>";
|
|
366
|
+
const appDashboardUrl = `https://api.slack.com/apps/${appIdForLinks}`;
|
|
367
|
+
const oauthUrl = `${appDashboardUrl}/oauth`;
|
|
368
|
+
console.log(chalk.bold("\n🔐 Collect your Slack credentials"));
|
|
369
|
+
console.log(`Signing Secret & App-Level Tokens: ${chalk.cyan(chalk.underline(appDashboardUrl))}`);
|
|
370
|
+
console.log(`OAuth Tokens (Bot Token): ${chalk.cyan(chalk.underline(oauthUrl))}, you need to install the app first.\n`);
|
|
371
|
+
if (trimmedAppId === "") {
|
|
372
|
+
console.log(chalk.dim("Replace <YOUR_APP_ID> in the links above once you locate your Slack app ID."));
|
|
373
|
+
console.log();
|
|
374
|
+
}
|
|
375
|
+
credentialAnswers = await inquirer.prompt([
|
|
376
|
+
{
|
|
377
|
+
type: "password",
|
|
378
|
+
name: "slackSigningSecret",
|
|
379
|
+
message: "Slack Signing Secret?",
|
|
380
|
+
validate: (input) => {
|
|
381
|
+
if (!input) {
|
|
382
|
+
return "Please enter your Slack signing secret.";
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
type: "password",
|
|
389
|
+
name: "slackAppToken",
|
|
390
|
+
message: "Slack App Token (xapp-...)?",
|
|
391
|
+
validate: (input) => {
|
|
392
|
+
if (!input || !input.startsWith("xapp-")) {
|
|
393
|
+
return "Please enter a valid Slack app token starting with xapp-";
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
type: "password",
|
|
400
|
+
name: "slackBotToken",
|
|
401
|
+
message: "Slack Bot Token (xoxb-...)?",
|
|
402
|
+
validate: (input) => {
|
|
403
|
+
if (!input || !input.startsWith("xoxb-")) {
|
|
404
|
+
return "Please enter a valid Slack bot token starting with xoxb-";
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
const { aiKeyStrategy } = await inquirer.prompt([
|
|
412
|
+
{
|
|
413
|
+
type: "list",
|
|
414
|
+
name: "aiKeyStrategy",
|
|
415
|
+
message: "How should teammates access Claude/OpenAI?",
|
|
416
|
+
choices: [
|
|
417
|
+
{
|
|
418
|
+
name: "Each user brings their subscriptions",
|
|
419
|
+
value: "user-provided",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "Provide shared keys now so the bot works out of the box",
|
|
423
|
+
value: "shared",
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
default: "user-provided",
|
|
427
|
+
},
|
|
428
|
+
]);
|
|
429
|
+
let anthropicApiKey = "";
|
|
430
|
+
if (aiKeyStrategy === "shared") {
|
|
431
|
+
const { sharedAnthropicApiKey } = await inquirer.prompt([
|
|
432
|
+
{
|
|
433
|
+
type: "password",
|
|
434
|
+
name: "sharedAnthropicApiKey",
|
|
435
|
+
message: "Shared Anthropic (Claude) API Key (sk-ant-...)?",
|
|
436
|
+
},
|
|
437
|
+
]);
|
|
438
|
+
anthropicApiKey = sharedAnthropicApiKey;
|
|
439
|
+
}
|
|
440
|
+
if (anthropicApiKey === "") {
|
|
441
|
+
const authHint = selectedPlatforms.includes("slack")
|
|
442
|
+
? "teammates authorize Claude/OpenAI from the Slack Home tab on first use"
|
|
443
|
+
: "users authorize Claude/OpenAI on first use";
|
|
444
|
+
console.log(chalk.dim(`\nℹ With no shared API key, ${authHint}.\n`));
|
|
445
|
+
}
|
|
446
|
+
// Worker network access configuration
|
|
447
|
+
const { networkAccessMode } = await inquirer.prompt([
|
|
448
|
+
{
|
|
449
|
+
type: "list",
|
|
450
|
+
name: "networkAccessMode",
|
|
451
|
+
message: "Configure worker internet access:",
|
|
452
|
+
choices: [
|
|
453
|
+
{
|
|
454
|
+
name: "🔒 Filtered access (recommended) - Allow only specific domains",
|
|
455
|
+
value: "filtered",
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: "🚫 Complete isolation - No internet access",
|
|
459
|
+
value: "isolated",
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "🌐 Unrestricted access - Full internet (not recommended)",
|
|
463
|
+
value: "unrestricted",
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
default: "filtered",
|
|
467
|
+
},
|
|
468
|
+
]);
|
|
469
|
+
let allowedDomains = "";
|
|
470
|
+
let disallowedDomains = "";
|
|
471
|
+
const defaultDomains = [
|
|
472
|
+
"registry.npmjs.org",
|
|
473
|
+
".npmjs.org",
|
|
474
|
+
"github.com",
|
|
475
|
+
".github.com",
|
|
476
|
+
".githubusercontent.com",
|
|
477
|
+
"cdn.jsdelivr.net",
|
|
478
|
+
"unpkg.com",
|
|
479
|
+
"pypi.org",
|
|
480
|
+
"files.pythonhosted.org",
|
|
481
|
+
].join(",");
|
|
482
|
+
if (networkAccessMode === "filtered") {
|
|
483
|
+
const { customizeDomains } = await inquirer.prompt([
|
|
484
|
+
{
|
|
485
|
+
type: "confirm",
|
|
486
|
+
name: "customizeDomains",
|
|
487
|
+
message: "Use default allowed domains? (Claude API, npm, GitHub, PyPI, CDNs)",
|
|
488
|
+
default: true,
|
|
489
|
+
},
|
|
490
|
+
]);
|
|
491
|
+
if (!customizeDomains) {
|
|
492
|
+
const { allowedDomainsInput } = await inquirer.prompt([
|
|
493
|
+
{
|
|
494
|
+
type: "input",
|
|
495
|
+
name: "allowedDomainsInput",
|
|
496
|
+
message: "Enter comma-separated allowed domains:",
|
|
497
|
+
default: defaultDomains,
|
|
498
|
+
validate: (input) => {
|
|
499
|
+
if (!input || input.trim().length === 0) {
|
|
500
|
+
return "At least one domain is required for filtered mode";
|
|
501
|
+
}
|
|
502
|
+
return true;
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
506
|
+
allowedDomains = allowedDomainsInput;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
allowedDomains = defaultDomains;
|
|
510
|
+
}
|
|
511
|
+
const { addDisallowedDomains } = await inquirer.prompt([
|
|
512
|
+
{
|
|
513
|
+
type: "confirm",
|
|
514
|
+
name: "addDisallowedDomains",
|
|
515
|
+
message: "Add disallowed domains? (Optional - blocks specific domains within allowed patterns)",
|
|
516
|
+
default: false,
|
|
517
|
+
},
|
|
518
|
+
]);
|
|
519
|
+
if (addDisallowedDomains) {
|
|
520
|
+
const { disallowedDomainsInput } = await inquirer.prompt([
|
|
521
|
+
{
|
|
522
|
+
type: "input",
|
|
523
|
+
name: "disallowedDomainsInput",
|
|
524
|
+
message: "Enter comma-separated disallowed domains:",
|
|
525
|
+
},
|
|
526
|
+
]);
|
|
527
|
+
disallowedDomains = disallowedDomainsInput;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (networkAccessMode === "unrestricted") {
|
|
531
|
+
allowedDomains = "*";
|
|
532
|
+
const { addDisallowedDomains } = await inquirer.prompt([
|
|
533
|
+
{
|
|
534
|
+
type: "confirm",
|
|
535
|
+
name: "addDisallowedDomains",
|
|
536
|
+
message: "Block any specific domains? (Optional)",
|
|
537
|
+
default: false,
|
|
538
|
+
},
|
|
539
|
+
]);
|
|
540
|
+
if (addDisallowedDomains) {
|
|
541
|
+
const { disallowedDomainsInput } = await inquirer.prompt([
|
|
542
|
+
{
|
|
543
|
+
type: "input",
|
|
544
|
+
name: "disallowedDomainsInput",
|
|
545
|
+
message: "Enter comma-separated domains to block:",
|
|
546
|
+
},
|
|
547
|
+
]);
|
|
548
|
+
disallowedDomains = disallowedDomainsInput;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// else isolated mode: leave both empty
|
|
552
|
+
// Generate encryption key for credentials
|
|
553
|
+
const encryptionKey = randomBytes(32).toString("hex");
|
|
554
|
+
const answers = {
|
|
555
|
+
...baseAnswers,
|
|
556
|
+
...credentialAnswers,
|
|
557
|
+
anthropicApiKey,
|
|
558
|
+
publicUrl,
|
|
559
|
+
encryptionKey,
|
|
560
|
+
selectedMcpServers,
|
|
561
|
+
selectedPlatforms,
|
|
562
|
+
telegramBotToken,
|
|
563
|
+
telegramAllowFrom,
|
|
564
|
+
allowedDomains,
|
|
565
|
+
disallowedDomains,
|
|
566
|
+
};
|
|
567
|
+
// docker-compose.yml will be created in new directory, no need to check
|
|
568
|
+
const composeFilename = "docker-compose.yml";
|
|
569
|
+
const spinner = ora("Creating Lobu project...").start();
|
|
570
|
+
try {
|
|
571
|
+
// Create .lobu directory in project directory
|
|
572
|
+
const lobuDir = join(projectDir, ".lobu");
|
|
573
|
+
await mkdir(lobuDir, { recursive: true });
|
|
574
|
+
// Generate MCP config if servers were selected
|
|
575
|
+
if (answers.selectedMcpServers.length > 0) {
|
|
576
|
+
const mcpConfig = {
|
|
577
|
+
mcpServers: {},
|
|
578
|
+
};
|
|
579
|
+
for (const server of answers.selectedMcpServers) {
|
|
580
|
+
// Clone the config and replace PUBLIC_URL placeholder
|
|
581
|
+
const serverConfig = JSON.parse(JSON.stringify(server.config)
|
|
582
|
+
.replace(/\{PUBLIC_URL\}/g, answers.publicUrl || "http://localhost:8080")
|
|
583
|
+
.replace(/\$\{([A-Z_]+)\}/g, (match, varName) => {
|
|
584
|
+
// Keep env: prefix for secrets, remove for client IDs
|
|
585
|
+
if (match.includes("env:")) {
|
|
586
|
+
return match;
|
|
587
|
+
}
|
|
588
|
+
return `\${${varName}}`; // Will be replaced with instructions
|
|
589
|
+
}));
|
|
590
|
+
mcpConfig.mcpServers[server.id] = serverConfig;
|
|
591
|
+
}
|
|
592
|
+
await writeFile(join(lobuDir, "mcp.config.json"), JSON.stringify(mcpConfig, null, 2));
|
|
593
|
+
}
|
|
594
|
+
// Generate lobu.toml
|
|
595
|
+
await generateLobuToml(projectDir, {
|
|
596
|
+
agentName: projectName,
|
|
597
|
+
platforms: answers.selectedPlatforms,
|
|
598
|
+
mcpServers: answers.selectedMcpServers,
|
|
599
|
+
allowedDomains: answers.allowedDomains,
|
|
600
|
+
});
|
|
601
|
+
const variables = {
|
|
602
|
+
PROJECT_NAME: projectName,
|
|
603
|
+
CLI_VERSION: cliVersion,
|
|
604
|
+
SLACK_SIGNING_SECRET: answers.slackSigningSecret,
|
|
605
|
+
SLACK_BOT_TOKEN: answers.slackBotToken,
|
|
606
|
+
SLACK_APP_TOKEN: answers.slackAppToken,
|
|
607
|
+
TELEGRAM_ENABLED: answers.selectedPlatforms.includes("telegram")
|
|
608
|
+
? "true"
|
|
609
|
+
: "false",
|
|
610
|
+
TELEGRAM_BOT_TOKEN: answers.telegramBotToken,
|
|
611
|
+
TELEGRAM_ALLOW_FROM: answers.telegramAllowFrom,
|
|
612
|
+
ENCRYPTION_KEY: answers.encryptionKey,
|
|
613
|
+
ANTHROPIC_API_KEY: answers.anthropicApiKey || "",
|
|
614
|
+
PUBLIC_GATEWAY_URL: answers.publicUrl || "http://localhost:8080",
|
|
615
|
+
GATEWAY_PORT: "8080",
|
|
616
|
+
WORKER_ALLOWED_DOMAINS: answers.allowedDomains,
|
|
617
|
+
WORKER_DISALLOWED_DOMAINS: answers.disallowedDomains,
|
|
618
|
+
};
|
|
619
|
+
// Create .env file
|
|
620
|
+
await renderTemplate(".env.tmpl", variables, join(projectDir, ".env"));
|
|
621
|
+
// Append MCP environment variables if any were selected
|
|
622
|
+
if (answers.selectedMcpServers.length > 0) {
|
|
623
|
+
const requiredEnvVars = getRequiredEnvVars(answers.selectedMcpServers);
|
|
624
|
+
if (requiredEnvVars.length > 0) {
|
|
625
|
+
let mcpEnvContent = "\n# MCP Server Credentials\n";
|
|
626
|
+
mcpEnvContent +=
|
|
627
|
+
"# Add your OAuth client secrets and API keys below:\n";
|
|
628
|
+
for (const varName of requiredEnvVars) {
|
|
629
|
+
mcpEnvContent += `${varName}=your_${varName.toLowerCase()}_here\n`;
|
|
630
|
+
}
|
|
631
|
+
const envPath = join(projectDir, ".env");
|
|
632
|
+
const currentContent = await readFile(envPath, "utf-8");
|
|
633
|
+
await writeFile(envPath, currentContent + mcpEnvContent);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Create .gitignore
|
|
637
|
+
await renderTemplate(".gitignore.tmpl", {}, join(projectDir, ".gitignore"));
|
|
638
|
+
// Create README
|
|
639
|
+
await renderTemplate("README.md.tmpl", variables, join(projectDir, "README.md"));
|
|
640
|
+
// Create agent instruction files
|
|
641
|
+
await writeFile(join(projectDir, "IDENTITY.md"), `# Identity\n\nYou are ${projectName}, a helpful AI assistant.\n`);
|
|
642
|
+
await writeFile(join(projectDir, "SOUL.md"), `# Instructions\n\nBe concise and helpful. Ask clarifying questions when the request is ambiguous.\n`);
|
|
643
|
+
await writeFile(join(projectDir, "USER.md"), `# User Context\n\n<!-- Add user-specific preferences, timezone, environment details here -->\n`);
|
|
644
|
+
// Create skills directory for custom skills
|
|
645
|
+
await mkdir(join(projectDir, "skills"), { recursive: true });
|
|
646
|
+
await writeFile(join(projectDir, "skills", ".gitkeep"), "");
|
|
647
|
+
// Create AGENTS.md
|
|
648
|
+
await renderTemplate("AGENTS.md.tmpl", variables, join(projectDir, "AGENTS.md"));
|
|
649
|
+
// Create TESTING.md
|
|
650
|
+
await renderTemplate("TESTING.md.tmpl", variables, join(projectDir, "TESTING.md"));
|
|
651
|
+
// Create Dockerfile.worker
|
|
652
|
+
await renderTemplate("Dockerfile.worker.tmpl", variables, join(projectDir, "Dockerfile.worker"));
|
|
653
|
+
// Generate docker-compose.yml (always includes network isolation infrastructure)
|
|
654
|
+
const composeContent = generateDockerCompose({
|
|
655
|
+
projectName,
|
|
656
|
+
gatewayPort: "8080",
|
|
657
|
+
dockerfilePath: "./Dockerfile.worker",
|
|
658
|
+
hasMcpServers: answers.selectedMcpServers.length > 0,
|
|
659
|
+
platforms: answers.selectedPlatforms,
|
|
660
|
+
});
|
|
661
|
+
await writeFile(join(projectDir, composeFilename), composeContent);
|
|
662
|
+
spinner.succeed("Project created successfully!");
|
|
663
|
+
// Print next steps
|
|
664
|
+
console.log(chalk.green("\n✓ Lobu initialized!\n"));
|
|
665
|
+
console.log(chalk.bold("Next steps:\n"));
|
|
666
|
+
console.log(chalk.cyan(" 1. Navigate to your project:"));
|
|
667
|
+
console.log(chalk.dim(` cd ${projectName}\n`));
|
|
668
|
+
console.log(chalk.cyan(" 2. Review your configuration:"));
|
|
669
|
+
console.log(chalk.dim(" - lobu.toml (providers, skills, network)"));
|
|
670
|
+
console.log(chalk.dim(" - IDENTITY.md (who the agent is)"));
|
|
671
|
+
console.log(chalk.dim(" - SOUL.md (behavior rules)"));
|
|
672
|
+
console.log(chalk.dim(" - USER.md (user-specific context)"));
|
|
673
|
+
console.log(chalk.dim(" - skills/ (custom skills — auto-discovered)"));
|
|
674
|
+
console.log(chalk.dim(" - .env (secrets)"));
|
|
675
|
+
console.log(chalk.dim(` - ${composeFilename}`));
|
|
676
|
+
console.log(chalk.dim(" - Dockerfile.worker"));
|
|
677
|
+
if (answers.selectedMcpServers.length > 0) {
|
|
678
|
+
console.log(chalk.dim(" - .lobu/mcp.config.json"));
|
|
679
|
+
}
|
|
680
|
+
console.log();
|
|
681
|
+
// MCP Setup instructions
|
|
682
|
+
if (answers.selectedMcpServers.length > 0) {
|
|
683
|
+
const oauthServers = answers.selectedMcpServers.filter((s) => s.type === "oauth");
|
|
684
|
+
const apiKeyServers = answers.selectedMcpServers.filter((s) => s.type === "api-key");
|
|
685
|
+
if (oauthServers.length > 0 || apiKeyServers.length > 0) {
|
|
686
|
+
console.log(chalk.cyan(" 3. Configure MCP servers:"));
|
|
687
|
+
if (oauthServers.length > 0) {
|
|
688
|
+
console.log(chalk.yellow("\n OAuth-based MCP servers:"));
|
|
689
|
+
for (const server of oauthServers) {
|
|
690
|
+
console.log(chalk.dim(` - ${server.name}:`));
|
|
691
|
+
const instructions = server.setupInstructions
|
|
692
|
+
?.replace(/\{PUBLIC_URL\}/g, answers.publicUrl || "http://localhost:8080")
|
|
693
|
+
.split("\n")
|
|
694
|
+
.filter((line) => line.trim())
|
|
695
|
+
.map((line) => ` ${line}`)
|
|
696
|
+
.join("\n");
|
|
697
|
+
if (instructions) {
|
|
698
|
+
console.log(chalk.dim(instructions));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (apiKeyServers.length > 0) {
|
|
703
|
+
console.log(chalk.yellow("\n API Key-based MCP servers:"));
|
|
704
|
+
for (const server of apiKeyServers) {
|
|
705
|
+
console.log(chalk.dim(` - ${server.name}: Add API key to .env file`));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
console.log(chalk.cyan("\n 4. Start the services:"));
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
console.log(chalk.cyan(" 3. Start the services:"));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
console.log(chalk.cyan(" 3. Start the services:"));
|
|
716
|
+
}
|
|
717
|
+
console.log(chalk.dim(` docker compose -f ${composeFilename} up -d\n`));
|
|
718
|
+
console.log(chalk.cyan(` ${answers.selectedMcpServers.length > 0 ? "5" : "4"}. View logs:`));
|
|
719
|
+
console.log(chalk.dim(` docker compose -f ${composeFilename} logs -f\n`));
|
|
720
|
+
console.log(chalk.cyan(` ${answers.selectedMcpServers.length > 0 ? "6" : "5"}. Stop the services:`));
|
|
721
|
+
console.log(chalk.dim(` docker compose -f ${composeFilename} down\n`));
|
|
722
|
+
console.log(chalk.yellow("ℹ When you modify Dockerfile.worker or context files, rebuild the worker image:\n"));
|
|
723
|
+
console.log(chalk.dim(` docker compose -f ${composeFilename} build worker\n`));
|
|
724
|
+
console.log(chalk.dim(" The gateway will automatically pick up the latest worker image.\n"));
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
spinner.fail("Failed to create project");
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function generateLobuToml(projectDir, options) {
|
|
732
|
+
const lines = [
|
|
733
|
+
"# lobu.toml — Agent configuration",
|
|
734
|
+
"# Docs: https://lobu.ai/docs/getting-started",
|
|
735
|
+
"#",
|
|
736
|
+
"# Agent identity lives in markdown files:",
|
|
737
|
+
"# IDENTITY.md — Who the agent is",
|
|
738
|
+
"# SOUL.md — Behavior rules & instructions",
|
|
739
|
+
"# USER.md — User-specific context",
|
|
740
|
+
"# skills/*.md — Custom capabilities (auto-discovered)",
|
|
741
|
+
"",
|
|
742
|
+
"[agent]",
|
|
743
|
+
`name = "${options.agentName}"`,
|
|
744
|
+
`description = ""`,
|
|
745
|
+
"",
|
|
746
|
+
"# LLM providers (order = priority)",
|
|
747
|
+
"[[providers]]",
|
|
748
|
+
'id = "groq"',
|
|
749
|
+
'model = "llama-3.3-70b-versatile"',
|
|
750
|
+
"",
|
|
751
|
+
"# Skills from the registry",
|
|
752
|
+
"[skills]",
|
|
753
|
+
'enabled = ["github"]',
|
|
754
|
+
];
|
|
755
|
+
// Custom MCP servers
|
|
756
|
+
const customMcps = options.mcpServers.filter((s) => s.type === "none" || s.type === "command");
|
|
757
|
+
if (customMcps.length > 0) {
|
|
758
|
+
lines.push("");
|
|
759
|
+
for (const mcp of customMcps) {
|
|
760
|
+
if (mcp.config?.url) {
|
|
761
|
+
lines.push(`[skills.mcp.${mcp.id}]`);
|
|
762
|
+
lines.push(`url = "${mcp.config.url}"`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// Network
|
|
767
|
+
if (options.allowedDomains) {
|
|
768
|
+
const domains = options.allowedDomains
|
|
769
|
+
.split(",")
|
|
770
|
+
.map((d) => `"${d.trim()}"`)
|
|
771
|
+
.join(", ");
|
|
772
|
+
lines.push("", "[network]", `allowed = [${domains}]`);
|
|
773
|
+
}
|
|
774
|
+
// Platforms
|
|
775
|
+
const platformLines = [];
|
|
776
|
+
if (options.platforms.includes("telegram")) {
|
|
777
|
+
platformLines.push("telegram = true");
|
|
778
|
+
}
|
|
779
|
+
if (options.platforms.includes("slack")) {
|
|
780
|
+
platformLines.push("slack = true");
|
|
781
|
+
}
|
|
782
|
+
if (options.platforms.includes("api")) {
|
|
783
|
+
platformLines.push("api = true");
|
|
784
|
+
}
|
|
785
|
+
if (platformLines.length > 0) {
|
|
786
|
+
lines.push("", "[platforms]");
|
|
787
|
+
lines.push(...platformLines);
|
|
788
|
+
}
|
|
789
|
+
lines.push(""); // trailing newline
|
|
790
|
+
await writeFile(join(projectDir, "lobu.toml"), lines.join("\n"));
|
|
791
|
+
}
|
|
792
|
+
async function getSlackManifestUrl() {
|
|
793
|
+
const manifestYaml = await loadSlackManifestYaml();
|
|
794
|
+
const encodedManifest = encodeURIComponent(manifestYaml);
|
|
795
|
+
return `https://api.slack.com/apps?new_app=1&manifest_yaml=${encodedManifest}`;
|
|
796
|
+
}
|
|
797
|
+
async function loadSlackManifestYaml() {
|
|
798
|
+
try {
|
|
799
|
+
const manifestUrl = new URL("../../../../slack-app-manifest.json", import.meta.url);
|
|
800
|
+
const manifestContent = await readFile(manifestUrl, "utf-8");
|
|
801
|
+
const manifest = JSON.parse(manifestContent);
|
|
802
|
+
return YAML.stringify(manifest).trim();
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
return YAML.stringify(DEFAULT_SLACK_MANIFEST).trim();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
async function getCliVersion() {
|
|
809
|
+
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
810
|
+
const pkgContent = await readFile(pkgPath, "utf-8");
|
|
811
|
+
const pkg = JSON.parse(pkgContent);
|
|
812
|
+
return pkg.version || "0.1.0";
|
|
813
|
+
}
|
|
814
|
+
function generateDockerCompose(options) {
|
|
815
|
+
const { projectName, gatewayPort, hasMcpServers, platforms } = options;
|
|
816
|
+
const workerImage = `ghcr.io/lobu-ai/lobu-worker-base:latest`;
|
|
817
|
+
const gatewayImage = `ghcr.io/lobu-ai/lobu-gateway:latest`;
|
|
818
|
+
const mcpConfigMount = hasMcpServers
|
|
819
|
+
? `
|
|
820
|
+
- ./.lobu/mcp.config.json:/app/.lobu/mcp.config.json:ro`
|
|
821
|
+
: "";
|
|
822
|
+
const mcpEnvVars = hasMcpServers
|
|
823
|
+
? `
|
|
824
|
+
MCP_CONFIG_URL: file:///app/.lobu/mcp.config.json
|
|
825
|
+
ENCRYPTION_KEY: \${ENCRYPTION_KEY}`
|
|
826
|
+
: "";
|
|
827
|
+
const telegramEnvVars = platforms.includes("telegram")
|
|
828
|
+
? `
|
|
829
|
+
TELEGRAM_ENABLED: \${TELEGRAM_ENABLED:-false}
|
|
830
|
+
TELEGRAM_BOT_TOKEN: \${TELEGRAM_BOT_TOKEN:-}
|
|
831
|
+
TELEGRAM_ALLOW_FROM: \${TELEGRAM_ALLOW_FROM:-}`
|
|
832
|
+
: "";
|
|
833
|
+
const slackEnvVars = platforms.includes("slack")
|
|
834
|
+
? `
|
|
835
|
+
SLACK_BOT_TOKEN: \${SLACK_BOT_TOKEN}
|
|
836
|
+
SLACK_APP_TOKEN: \${SLACK_APP_TOKEN}
|
|
837
|
+
SLACK_SIGNING_SECRET: \${SLACK_SIGNING_SECRET}`
|
|
838
|
+
: "";
|
|
839
|
+
return `# Generated by @lobu/cli
|
|
840
|
+
# You can modify this file as needed
|
|
841
|
+
|
|
842
|
+
name: ${projectName}
|
|
843
|
+
|
|
844
|
+
services:
|
|
845
|
+
redis:
|
|
846
|
+
image: redis:7-alpine
|
|
847
|
+
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --save 60 1 --dir /data
|
|
848
|
+
volumes:
|
|
849
|
+
- redis_data:/data
|
|
850
|
+
healthcheck:
|
|
851
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
852
|
+
interval: 10s
|
|
853
|
+
timeout: 3s
|
|
854
|
+
retries: 3
|
|
855
|
+
networks:
|
|
856
|
+
- lobu-internal
|
|
857
|
+
restart: unless-stopped
|
|
858
|
+
|
|
859
|
+
gateway:
|
|
860
|
+
image: ${gatewayImage}
|
|
861
|
+
ports:
|
|
862
|
+
- "${gatewayPort}:8080"
|
|
863
|
+
- "8118:8118" # HTTP proxy for workers
|
|
864
|
+
environment:
|
|
865
|
+
DEPLOYMENT_MODE: docker
|
|
866
|
+
WORKER_IMAGE: ${workerImage}
|
|
867
|
+
QUEUE_URL: redis://redis:6379/0${slackEnvVars}${telegramEnvVars}
|
|
868
|
+
PUBLIC_GATEWAY_URL: \${PUBLIC_GATEWAY_URL:-}
|
|
869
|
+
NODE_ENV: production
|
|
870
|
+
ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
|
|
871
|
+
COMPOSE_PROJECT_NAME: ${projectName}${mcpEnvVars}
|
|
872
|
+
# Worker network access control
|
|
873
|
+
# Empty/unset: Complete isolation (deny all)
|
|
874
|
+
# WORKER_ALLOWED_DOMAINS=*: Unrestricted access
|
|
875
|
+
# WORKER_ALLOWED_DOMAINS=domains: Allowlist mode
|
|
876
|
+
WORKER_ALLOWED_DOMAINS: \${WORKER_ALLOWED_DOMAINS:-}
|
|
877
|
+
WORKER_DISALLOWED_DOMAINS: \${WORKER_DISALLOWED_DOMAINS:-}
|
|
878
|
+
volumes:
|
|
879
|
+
- /var/run/docker.sock:/var/run/docker.sock${mcpConfigMount}
|
|
880
|
+
- env_storage:/app/.lobu/env
|
|
881
|
+
networks:
|
|
882
|
+
- lobu-public # Internet access
|
|
883
|
+
- lobu-internal # Internal services (redis, workers)
|
|
884
|
+
depends_on:
|
|
885
|
+
redis:
|
|
886
|
+
condition: service_healthy
|
|
887
|
+
restart: unless-stopped
|
|
888
|
+
|
|
889
|
+
networks:
|
|
890
|
+
# Public network with internet access (gateway only)
|
|
891
|
+
lobu-public:
|
|
892
|
+
driver: bridge
|
|
893
|
+
|
|
894
|
+
# Internal network - no direct internet access
|
|
895
|
+
# Workers use this network and can only reach internet via gateway's proxy
|
|
896
|
+
lobu-internal:
|
|
897
|
+
internal: true
|
|
898
|
+
driver: bridge
|
|
899
|
+
|
|
900
|
+
volumes:
|
|
901
|
+
redis_data:
|
|
902
|
+
env_storage:
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
//# sourceMappingURL=init.js.map
|