@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.
Files changed (116) hide show
  1. package/README.md +276 -0
  2. package/bin/create-lobu.js +5 -0
  3. package/bin/lobu.js +5 -0
  4. package/dist/api/client.d.ts +11 -0
  5. package/dist/api/client.d.ts.map +1 -0
  6. package/dist/api/client.js +35 -0
  7. package/dist/api/client.js.map +1 -0
  8. package/dist/api/credentials.d.ts +13 -0
  9. package/dist/api/credentials.d.ts.map +1 -0
  10. package/dist/api/credentials.js +39 -0
  11. package/dist/api/credentials.js.map +1 -0
  12. package/dist/commands/dev.d.ts +13 -0
  13. package/dist/commands/dev.d.ts.map +1 -0
  14. package/dist/commands/dev.js +128 -0
  15. package/dist/commands/dev.js.map +1 -0
  16. package/dist/commands/init.d.ts +2 -0
  17. package/dist/commands/init.d.ts.map +1 -0
  18. package/dist/commands/init.js +905 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/launch.d.ts +6 -0
  21. package/dist/commands/launch.d.ts.map +1 -0
  22. package/dist/commands/launch.js +32 -0
  23. package/dist/commands/launch.js.map +1 -0
  24. package/dist/commands/login.d.ts +4 -0
  25. package/dist/commands/login.d.ts.map +1 -0
  26. package/dist/commands/login.js +29 -0
  27. package/dist/commands/login.js.map +1 -0
  28. package/dist/commands/logout.d.ts +2 -0
  29. package/dist/commands/logout.d.ts.map +1 -0
  30. package/dist/commands/logout.js +7 -0
  31. package/dist/commands/logout.js.map +1 -0
  32. package/dist/commands/providers/add.d.ts +2 -0
  33. package/dist/commands/providers/add.d.ts.map +1 -0
  34. package/dist/commands/providers/add.js +51 -0
  35. package/dist/commands/providers/add.js.map +1 -0
  36. package/dist/commands/providers/list.d.ts +2 -0
  37. package/dist/commands/providers/list.d.ts.map +1 -0
  38. package/dist/commands/providers/list.js +21 -0
  39. package/dist/commands/providers/list.js.map +1 -0
  40. package/dist/commands/secrets.d.ts +8 -0
  41. package/dist/commands/secrets.d.ts.map +1 -0
  42. package/dist/commands/secrets.js +98 -0
  43. package/dist/commands/secrets.js.map +1 -0
  44. package/dist/commands/skills/add.d.ts +2 -0
  45. package/dist/commands/skills/add.d.ts.map +1 -0
  46. package/dist/commands/skills/add.js +47 -0
  47. package/dist/commands/skills/add.js.map +1 -0
  48. package/dist/commands/skills/info.d.ts +2 -0
  49. package/dist/commands/skills/info.d.ts.map +1 -0
  50. package/dist/commands/skills/info.js +45 -0
  51. package/dist/commands/skills/info.js.map +1 -0
  52. package/dist/commands/skills/list.d.ts +2 -0
  53. package/dist/commands/skills/list.d.ts.map +1 -0
  54. package/dist/commands/skills/list.js +30 -0
  55. package/dist/commands/skills/list.js.map +1 -0
  56. package/dist/commands/skills/registry.d.ts +34 -0
  57. package/dist/commands/skills/registry.d.ts.map +1 -0
  58. package/dist/commands/skills/registry.js +38 -0
  59. package/dist/commands/skills/registry.js.map +1 -0
  60. package/dist/commands/skills/search.d.ts +2 -0
  61. package/dist/commands/skills/search.d.ts.map +1 -0
  62. package/dist/commands/skills/search.js +21 -0
  63. package/dist/commands/skills/search.js.map +1 -0
  64. package/dist/commands/status.d.ts +2 -0
  65. package/dist/commands/status.d.ts.map +1 -0
  66. package/dist/commands/status.js +7 -0
  67. package/dist/commands/status.js.map +1 -0
  68. package/dist/commands/validate.d.ts +2 -0
  69. package/dist/commands/validate.d.ts.map +1 -0
  70. package/dist/commands/validate.js +73 -0
  71. package/dist/commands/validate.js.map +1 -0
  72. package/dist/commands/whoami.d.ts +2 -0
  73. package/dist/commands/whoami.d.ts.map +1 -0
  74. package/dist/commands/whoami.js +25 -0
  75. package/dist/commands/whoami.js.map +1 -0
  76. package/dist/config/loader.d.ts +16 -0
  77. package/dist/config/loader.d.ts.map +1 -0
  78. package/dist/config/loader.js +41 -0
  79. package/dist/config/loader.js.map +1 -0
  80. package/dist/config/schema.d.ts +279 -0
  81. package/dist/config/schema.d.ts.map +1 -0
  82. package/dist/config/schema.js +52 -0
  83. package/dist/config/schema.js.map +1 -0
  84. package/dist/config/transformer.d.ts +11 -0
  85. package/dist/config/transformer.d.ts.map +1 -0
  86. package/dist/config/transformer.js +49 -0
  87. package/dist/config/transformer.js.map +1 -0
  88. package/dist/index.d.ts +6 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +183 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/mcp-servers.d.ts +11 -0
  93. package/dist/mcp-servers.d.ts.map +1 -0
  94. package/dist/mcp-servers.js +27 -0
  95. package/dist/mcp-servers.js.map +1 -0
  96. package/dist/mcp-servers.json +216 -0
  97. package/dist/templates/.env.tmpl +29 -0
  98. package/dist/templates/.gitignore.tmpl +32 -0
  99. package/dist/templates/AGENTS.md.tmpl +1 -0
  100. package/dist/templates/Dockerfile.worker.tmpl +29 -0
  101. package/dist/templates/README.md.tmpl +95 -0
  102. package/dist/templates/TESTING.md.tmpl +225 -0
  103. package/dist/templates/lobu.toml.tmpl +44 -0
  104. package/dist/types.d.ts +76 -0
  105. package/dist/types.d.ts.map +1 -0
  106. package/dist/types.js +2 -0
  107. package/dist/types.js.map +1 -0
  108. package/dist/utils/config.d.ts +2 -0
  109. package/dist/utils/config.d.ts.map +1 -0
  110. package/dist/utils/config.js +13 -0
  111. package/dist/utils/config.js.map +1 -0
  112. package/dist/utils/template.d.ts +2 -0
  113. package/dist/utils/template.d.ts.map +1 -0
  114. package/dist/utils/template.js +19 -0
  115. package/dist/utils/template.js.map +1 -0
  116. 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