@lobu/cli 3.0.3 → 3.0.4

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 (106) hide show
  1. package/README.md +8 -8
  2. package/dist/__tests__/login.test.d.ts +2 -0
  3. package/dist/__tests__/login.test.d.ts.map +1 -0
  4. package/dist/__tests__/login.test.js +173 -0
  5. package/dist/__tests__/login.test.js.map +1 -0
  6. package/dist/api/client.d.ts.map +1 -1
  7. package/dist/api/client.js +3 -5
  8. package/dist/api/client.js.map +1 -1
  9. package/dist/api/context.d.ts +22 -0
  10. package/dist/api/context.d.ts.map +1 -0
  11. package/dist/api/context.js +113 -0
  12. package/dist/api/context.js.map +1 -0
  13. package/dist/api/credentials.d.ts +9 -4
  14. package/dist/api/credentials.d.ts.map +1 -1
  15. package/dist/api/credentials.js +127 -15
  16. package/dist/api/credentials.js.map +1 -1
  17. package/dist/commands/chat.d.ts +11 -0
  18. package/dist/commands/chat.d.ts.map +1 -0
  19. package/dist/commands/chat.js +195 -0
  20. package/dist/commands/chat.js.map +1 -0
  21. package/dist/commands/context.d.ts +8 -0
  22. package/dist/commands/context.d.ts.map +1 -0
  23. package/dist/commands/context.js +46 -0
  24. package/dist/commands/context.js.map +1 -0
  25. package/dist/commands/dev.d.ts +1 -8
  26. package/dist/commands/dev.d.ts.map +1 -1
  27. package/dist/commands/dev.js +236 -57
  28. package/dist/commands/dev.js.map +1 -1
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +351 -676
  31. package/dist/commands/init.js.map +1 -1
  32. package/dist/commands/launch.d.ts.map +1 -1
  33. package/dist/commands/launch.js +2 -8
  34. package/dist/commands/launch.js.map +1 -1
  35. package/dist/commands/login.d.ts +2 -0
  36. package/dist/commands/login.d.ts.map +1 -1
  37. package/dist/commands/login.js +283 -14
  38. package/dist/commands/login.js.map +1 -1
  39. package/dist/commands/logout.d.ts +3 -1
  40. package/dist/commands/logout.d.ts.map +1 -1
  41. package/dist/commands/logout.js +5 -3
  42. package/dist/commands/logout.js.map +1 -1
  43. package/dist/commands/providers/add.d.ts.map +1 -1
  44. package/dist/commands/providers/add.js +38 -14
  45. package/dist/commands/providers/add.js.map +1 -1
  46. package/dist/commands/providers/list.d.ts.map +1 -1
  47. package/dist/commands/providers/list.js +4 -2
  48. package/dist/commands/providers/list.js.map +1 -1
  49. package/dist/commands/skills/add.d.ts.map +1 -1
  50. package/dist/commands/skills/add.js +25 -7
  51. package/dist/commands/skills/add.js.map +1 -1
  52. package/dist/commands/skills/info.d.ts.map +1 -1
  53. package/dist/commands/skills/info.js.map +1 -1
  54. package/dist/commands/skills/list.d.ts.map +1 -1
  55. package/dist/commands/skills/list.js.map +1 -1
  56. package/dist/commands/skills/registry.d.ts +5 -0
  57. package/dist/commands/skills/registry.d.ts.map +1 -1
  58. package/dist/commands/skills/registry.js.map +1 -1
  59. package/dist/commands/skills/search.d.ts.map +1 -1
  60. package/dist/commands/skills/search.js +3 -1
  61. package/dist/commands/skills/search.js.map +1 -1
  62. package/dist/commands/status.d.ts +1 -1
  63. package/dist/commands/status.d.ts.map +1 -1
  64. package/dist/commands/status.js +107 -4
  65. package/dist/commands/status.js.map +1 -1
  66. package/dist/commands/validate.d.ts.map +1 -1
  67. package/dist/commands/validate.js +9 -20
  68. package/dist/commands/validate.js.map +1 -1
  69. package/dist/commands/whoami.d.ts +3 -1
  70. package/dist/commands/whoami.d.ts.map +1 -1
  71. package/dist/commands/whoami.js +17 -3
  72. package/dist/commands/whoami.js.map +1 -1
  73. package/dist/config/agents-manifest.d.ts +92 -0
  74. package/dist/config/agents-manifest.d.ts.map +1 -0
  75. package/dist/config/agents-manifest.js +7 -0
  76. package/dist/config/agents-manifest.js.map +1 -0
  77. package/dist/config/loader.d.ts +18 -0
  78. package/dist/config/loader.d.ts.map +1 -1
  79. package/dist/config/loader.js +62 -2
  80. package/dist/config/loader.js.map +1 -1
  81. package/dist/config/platform-schemas.d.ts +120 -0
  82. package/dist/config/platform-schemas.d.ts.map +1 -0
  83. package/dist/config/platform-schemas.js +97 -0
  84. package/dist/config/platform-schemas.js.map +1 -0
  85. package/dist/config/schema.d.ts +546 -111
  86. package/dist/config/schema.d.ts.map +1 -1
  87. package/dist/config/schema.js +26 -19
  88. package/dist/config/schema.js.map +1 -1
  89. package/dist/config/transformer.d.ts +3 -0
  90. package/dist/config/transformer.d.ts.map +1 -1
  91. package/dist/config/transformer.js +7 -10
  92. package/dist/config/transformer.js.map +1 -1
  93. package/dist/index.d.ts +0 -4
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +52 -10
  96. package/dist/index.js.map +1 -1
  97. package/dist/system-skills.json +89 -29
  98. package/dist/templates/.env.tmpl +3 -14
  99. package/dist/templates/.gitignore.tmpl +1 -0
  100. package/dist/templates/README.md.tmpl +7 -8
  101. package/dist/utils/markdown.d.ts +3 -0
  102. package/dist/utils/markdown.d.ts.map +1 -0
  103. package/dist/utils/markdown.js +10 -0
  104. package/dist/utils/markdown.js.map +1 -0
  105. package/package.json +4 -2
  106. package/dist/templates/lobu.toml.tmpl +0 -44
@@ -5,100 +5,9 @@ import { join } from "node:path";
5
5
  import chalk from "chalk";
6
6
  import inquirer from "inquirer";
7
7
  import ora from "ora";
8
- import YAML from "yaml";
9
- import { getRequiredEnvVars, MCP_SERVERS, } from "../mcp-servers.js";
8
+ import { isIntegrationSkill, isProviderSkill, loadSkillsRegistry, } from "../commands/skills/registry.js";
9
+ import { secretsSetCommand } from "../commands/secrets.js";
10
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
11
  export async function initCommand(cwd = process.cwd(), projectNameArg) {
103
12
  console.log(chalk.bold.cyan("\n🤖 Welcome to Lobu!\n"));
104
13
  // Get CLI version
@@ -137,430 +46,295 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
137
46
  await mkdir(projectDir, { recursive: true });
138
47
  console.log(chalk.dim(`\nCreating project in: ${chalk.cyan(projectDir)}\n`));
139
48
  }
140
- // MCP Server selection
141
- const { configureMcp } = await inquirer.prompt([
49
+ // Deployment mode selection
50
+ const { deploymentMode } = await inquirer.prompt([
142
51
  {
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
- },
52
+ type: "list",
53
+ name: "deploymentMode",
54
+ message: "How should workers run?",
55
+ choices: [
186
56
  {
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,
57
+ name: "Embedded — virtual bash & filesystem, no package installs, lower resource usage",
58
+ value: "embedded",
211
59
  },
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
60
  {
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
- },
61
+ name: "Docker — isolated containers per user, full OS access, heavier but more capable",
62
+ value: "docker",
235
63
  },
236
- ]);
237
- publicUrl = publicGatewayUrl;
238
- }
239
- }
240
- // Platform selection
241
- const { selectedPlatforms } = await inquirer.prompt([
64
+ ],
65
+ default: "embedded",
66
+ },
67
+ ]);
68
+ // Gateway port selection
69
+ const { gatewayPort } = await inquirer.prompt([
242
70
  {
243
- type: "checkbox",
244
- name: "selectedPlatforms",
245
- message: "Which messaging platform(s) do you want to configure?",
71
+ type: "input",
72
+ name: "gatewayPort",
73
+ message: "Gateway port?",
74
+ default: "8080",
75
+ validate: (input) => {
76
+ const port = Number(input);
77
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
78
+ return "Please enter a valid port number (1-65535)";
79
+ }
80
+ return true;
81
+ },
82
+ },
83
+ ]);
84
+ // Public gateway URL (optional — only needed for OAuth callbacks and external webhooks)
85
+ const { publicGatewayUrl } = await inquirer.prompt([
86
+ {
87
+ type: "input",
88
+ name: "publicGatewayUrl",
89
+ message: "Public gateway URL? (leave empty for local dev, set for OAuth/webhooks)",
90
+ default: "",
91
+ },
92
+ ]);
93
+ // Admin password
94
+ const { adminPassword } = await inquirer.prompt([
95
+ {
96
+ type: "password",
97
+ name: "adminPassword",
98
+ message: "Admin password?",
99
+ mask: "*",
100
+ validate: (input) => {
101
+ if (!input || input.length < 4) {
102
+ return "Password must be at least 4 characters";
103
+ }
104
+ return true;
105
+ },
106
+ },
107
+ ]);
108
+ // Worker network access policy
109
+ const { networkPolicy } = await inquirer.prompt([
110
+ {
111
+ type: "list",
112
+ name: "networkPolicy",
113
+ message: "Worker network access?",
246
114
  choices: [
247
115
  {
248
- name: "Telegram (quickest1 token from @BotFather)",
249
- value: "telegram",
116
+ name: "Restricted (recommended)common registries only (npm, GitHub, PyPI)",
117
+ value: "restricted",
250
118
  },
251
119
  {
252
- name: "Slack (requires app creation + 3 credentials)",
253
- value: "slack",
120
+ name: "Open workers can access any domain",
121
+ value: "open",
254
122
  },
255
123
  {
256
- name: "API only (no chat platform, use REST endpoints)",
257
- value: "api",
124
+ name: "Isolated workers have no internet access",
125
+ value: "isolated",
258
126
  },
259
127
  ],
260
- validate: (input) => {
261
- if (input.length === 0) {
262
- return "Select at least one platform";
263
- }
264
- return true;
265
- },
128
+ default: "restricted",
266
129
  },
267
130
  ]);
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([
131
+ // Provider selection (from system-skills.json registry)
132
+ const providerSkills = loadSkillsRegistry().filter(isProviderSkill);
133
+ const providerChoices = [
134
+ { name: "Skip — I'll add a provider later", value: "" },
135
+ ...providerSkills.map((s) => ({
136
+ name: `${s.providers[0].displayName}${s.providers[0].defaultModel ? ` (${s.providers[0].defaultModel})` : ""}`,
137
+ value: s.id,
138
+ })),
139
+ ];
140
+ const { providerId } = await inquirer.prompt([
141
+ {
142
+ type: "list",
143
+ name: "providerId",
144
+ message: "AI provider?",
145
+ choices: providerChoices,
146
+ default: "",
147
+ },
148
+ ]);
149
+ let providerApiKey = "";
150
+ let selectedProvider;
151
+ if (providerId) {
152
+ selectedProvider = providerSkills.find((s) => s.id === providerId);
153
+ const p = selectedProvider?.providers?.[0];
154
+ if (p) {
155
+ const { apiKey } = await inquirer.prompt([
348
156
  {
349
- type: "confirm",
350
- name: "slackAppCreated",
351
- message: "Press enter after clicking \u201CCreate\u201D and returning here to continue.",
352
- default: true,
157
+ type: "password",
158
+ name: "apiKey",
159
+ message: `${p.displayName} API key:`,
160
+ mask: "*",
353
161
  },
354
162
  ]);
163
+ providerApiKey = apiKey || "";
355
164
  }
356
- const { slackAppId } = await inquirer.prompt([
165
+ }
166
+ // Skills selection (integration skills from registry, excluding provider-only skills)
167
+ const integrationSkills = loadSkillsRegistry().filter((s) => isIntegrationSkill(s) && !isProviderSkill(s) && !s.hidden);
168
+ const { skillIds } = await inquirer.prompt([
169
+ {
170
+ type: "checkbox",
171
+ name: "skillIds",
172
+ message: "Enable integration skills?",
173
+ choices: integrationSkills.map((s) => ({
174
+ name: `${s.name} — ${s.description}`,
175
+ value: s.id,
176
+ checked: s.id === "github",
177
+ })),
178
+ when: integrationSkills.length > 0,
179
+ },
180
+ ]);
181
+ const selectedSkillIds = skillIds || [];
182
+ // Connection (messaging platform) selection
183
+ const platformChoices = [
184
+ { name: "Skip — I'll connect a platform later", value: "" },
185
+ { name: "Telegram", value: "telegram" },
186
+ { name: "Slack", value: "slack" },
187
+ { name: "Discord", value: "discord" },
188
+ ];
189
+ const { platformType } = await inquirer.prompt([
190
+ {
191
+ type: "list",
192
+ name: "platformType",
193
+ message: "Connect a messaging platform?",
194
+ choices: platformChoices,
195
+ default: "",
196
+ },
197
+ ]);
198
+ const connectionConfig = {};
199
+ const connectionSecrets = [];
200
+ if (platformType === "telegram") {
201
+ const { botToken } = await inquirer.prompt([
357
202
  {
358
- type: "input",
359
- name: "slackAppId",
360
- message: "Slack App ID (optional)?",
361
- default: "",
203
+ type: "password",
204
+ name: "botToken",
205
+ message: "Telegram bot token (from @BotFather):",
206
+ mask: "*",
362
207
  },
363
208
  ]);
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();
209
+ if (botToken) {
210
+ connectionConfig.botToken = "$TELEGRAM_BOT_TOKEN";
211
+ connectionSecrets.push({
212
+ envVar: "TELEGRAM_BOT_TOKEN",
213
+ value: botToken,
214
+ });
374
215
  }
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
- },
216
+ }
217
+ else if (platformType === "slack") {
218
+ const slackAnswers = await inquirer.prompt([
387
219
  {
388
220
  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
- },
221
+ name: "botToken",
222
+ message: "Slack bot token (xoxb-...):",
223
+ mask: "*",
397
224
  },
398
225
  {
399
226
  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
- },
227
+ name: "signingSecret",
228
+ message: "Slack signing secret:",
229
+ mask: "*",
408
230
  },
409
231
  ]);
232
+ if (slackAnswers.botToken) {
233
+ connectionConfig.botToken = "$SLACK_BOT_TOKEN";
234
+ connectionSecrets.push({
235
+ envVar: "SLACK_BOT_TOKEN",
236
+ value: slackAnswers.botToken,
237
+ });
238
+ }
239
+ if (slackAnswers.signingSecret) {
240
+ connectionConfig.signingSecret = "$SLACK_SIGNING_SECRET";
241
+ connectionSecrets.push({
242
+ envVar: "SLACK_SIGNING_SECRET",
243
+ value: slackAnswers.signingSecret,
244
+ });
245
+ }
410
246
  }
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([
247
+ else if (platformType === "discord") {
248
+ const { botToken } = await inquirer.prompt([
432
249
  {
433
250
  type: "password",
434
- name: "sharedAnthropicApiKey",
435
- message: "Shared Anthropic (Claude) API Key (sk-ant-...)?",
251
+ name: "botToken",
252
+ message: "Discord bot token:",
253
+ mask: "*",
436
254
  },
437
255
  ]);
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`));
256
+ if (botToken) {
257
+ connectionConfig.botToken = "$DISCORD_BOT_TOKEN";
258
+ connectionSecrets.push({
259
+ envVar: "DISCORD_BOT_TOKEN",
260
+ value: botToken,
261
+ });
262
+ }
445
263
  }
446
- // Worker network access configuration
447
- const { networkAccessMode } = await inquirer.prompt([
264
+ // Settings page OAuth login (optional)
265
+ const { oauthIssuer } = await inquirer.prompt([
448
266
  {
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",
267
+ type: "input",
268
+ name: "oauthIssuer",
269
+ message: "Settings page OAuth issuer URL (leave empty to skip):",
270
+ default: "",
467
271
  },
468
272
  ]);
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([
273
+ const oauthSecrets = [];
274
+ if (oauthIssuer) {
275
+ const oauthAnswers = await inquirer.prompt([
484
276
  {
485
- type: "confirm",
486
- name: "customizeDomains",
487
- message: "Use default allowed domains? (Claude API, npm, GitHub, PyPI, CDNs)",
488
- default: true,
277
+ type: "input",
278
+ name: "clientId",
279
+ message: "OAuth client ID (leave empty for dynamic registration):",
280
+ default: "",
489
281
  },
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
282
  {
513
- type: "confirm",
514
- name: "addDisallowedDomains",
515
- message: "Add disallowed domains? (Optional - blocks specific domains within allowed patterns)",
516
- default: false,
283
+ type: "password",
284
+ name: "clientSecret",
285
+ message: "OAuth client secret (leave empty if public client):",
286
+ mask: "*",
287
+ default: "",
517
288
  },
518
289
  ]);
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;
290
+ oauthSecrets.push({
291
+ envVar: "SETTINGS_OAUTH_ISSUER_URL",
292
+ value: oauthIssuer,
293
+ });
294
+ if (oauthAnswers.clientId) {
295
+ oauthSecrets.push({
296
+ envVar: "SETTINGS_OAUTH_CLIENT_ID",
297
+ value: oauthAnswers.clientId,
298
+ });
299
+ }
300
+ if (oauthAnswers.clientSecret) {
301
+ oauthSecrets.push({
302
+ envVar: "SETTINGS_OAUTH_CLIENT_SECRET",
303
+ value: oauthAnswers.clientSecret,
304
+ });
528
305
  }
529
306
  }
530
- else if (networkAccessMode === "unrestricted") {
307
+ // Compute network domains from selected policy
308
+ let allowedDomains;
309
+ let disallowedDomains;
310
+ if (networkPolicy === "open") {
531
311
  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
- }
312
+ disallowedDomains = "";
313
+ }
314
+ else if (networkPolicy === "isolated") {
315
+ allowedDomains = "";
316
+ disallowedDomains = "";
317
+ }
318
+ else {
319
+ // restricted (default)
320
+ allowedDomains = [
321
+ "registry.npmjs.org",
322
+ ".npmjs.org",
323
+ "github.com",
324
+ ".github.com",
325
+ ".githubusercontent.com",
326
+ "cdn.jsdelivr.net",
327
+ "unpkg.com",
328
+ "pypi.org",
329
+ "files.pythonhosted.org",
330
+ ].join(",");
331
+ disallowedDomains = "";
550
332
  }
551
- // else isolated mode: leave both empty
552
- // Generate encryption key for credentials
553
333
  const encryptionKey = randomBytes(32).toString("hex");
554
334
  const answers = {
555
335
  ...baseAnswers,
556
- ...credentialAnswers,
557
- anthropicApiKey,
558
- publicUrl,
336
+ deploymentMode: deploymentMode,
559
337
  encryptionKey,
560
- selectedMcpServers,
561
- selectedPlatforms,
562
- telegramBotToken,
563
- telegramAllowFrom,
564
338
  allowedDomains,
565
339
  disallowedDomains,
566
340
  };
@@ -568,95 +342,78 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
568
342
  const composeFilename = "docker-compose.yml";
569
343
  const spinner = ora("Creating Lobu project...").start();
570
344
  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
- }
345
+ // Create .lobu and data directories in project directory
346
+ await mkdir(join(projectDir, ".lobu"), { recursive: true });
347
+ await mkdir(join(projectDir, "data"), { recursive: true });
594
348
  // Generate lobu.toml
595
349
  await generateLobuToml(projectDir, {
596
350
  agentName: projectName,
597
- platforms: answers.selectedPlatforms,
598
- mcpServers: answers.selectedMcpServers,
599
351
  allowedDomains: answers.allowedDomains,
352
+ providerId: providerId || undefined,
353
+ providerEnvVar: selectedProvider?.providers?.[0]?.envVarName,
354
+ providerModel: selectedProvider?.providers?.[0]?.defaultModel,
355
+ connectionType: platformType || undefined,
356
+ connectionConfig: Object.keys(connectionConfig).length > 0 ? connectionConfig : undefined,
357
+ skillIds: selectedSkillIds,
600
358
  });
601
359
  const variables = {
602
360
  PROJECT_NAME: projectName,
603
361
  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,
362
+ DEPLOYMENT_MODE: answers.deploymentMode,
363
+ ADMIN_PASSWORD: adminPassword,
612
364
  ENCRYPTION_KEY: answers.encryptionKey,
613
- ANTHROPIC_API_KEY: answers.anthropicApiKey || "",
614
- PUBLIC_GATEWAY_URL: answers.publicUrl || "http://localhost:8080",
615
- GATEWAY_PORT: "8080",
365
+ GATEWAY_PORT: gatewayPort,
616
366
  WORKER_ALLOWED_DOMAINS: answers.allowedDomains,
617
367
  WORKER_DISALLOWED_DOMAINS: answers.disallowedDomains,
618
368
  };
619
369
  // Create .env file
620
370
  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
- }
371
+ // Save public gateway URL if explicitly set
372
+ if (publicGatewayUrl) {
373
+ await secretsSetCommand(projectDir, "PUBLIC_GATEWAY_URL", publicGatewayUrl);
374
+ }
375
+ // Save provider API key to .env
376
+ if (providerApiKey && selectedProvider?.providers?.[0]?.envVarName) {
377
+ await secretsSetCommand(projectDir, selectedProvider.providers[0].envVarName, providerApiKey);
378
+ }
379
+ // Save connection secrets to .env
380
+ for (const secret of connectionSecrets) {
381
+ await secretsSetCommand(projectDir, secret.envVar, secret.value);
382
+ }
383
+ // Save OAuth secrets to .env
384
+ for (const secret of oauthSecrets) {
385
+ await secretsSetCommand(projectDir, secret.envVar, secret.value);
635
386
  }
636
387
  // Create .gitignore
637
388
  await renderTemplate(".gitignore.tmpl", {}, join(projectDir, ".gitignore"));
638
389
  // Create README
639
390
  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
391
+ // Create agent directory with instruction files
392
+ const agentDir = join(projectDir, "agents", projectName);
393
+ await mkdir(agentDir, { recursive: true });
394
+ await writeFile(join(agentDir, "IDENTITY.md"), `# Identity\n\nYou are ${projectName}, a helpful AI assistant.\n`);
395
+ await writeFile(join(agentDir, "SOUL.md"), `# Instructions\n\nBe concise and helpful. Ask clarifying questions when the request is ambiguous.\n`);
396
+ await writeFile(join(agentDir, "USER.md"), `# User Context\n\n<!-- Add user-specific preferences, timezone, environment details here -->\n`);
397
+ // Create agent-specific skills directory
398
+ await mkdir(join(agentDir, "skills"), { recursive: true });
399
+ await writeFile(join(agentDir, "skills", ".gitkeep"), "");
400
+ // Create shared skills directory
645
401
  await mkdir(join(projectDir, "skills"), { recursive: true });
646
402
  await writeFile(join(projectDir, "skills", ".gitkeep"), "");
647
403
  // Create AGENTS.md
648
404
  await renderTemplate("AGENTS.md.tmpl", variables, join(projectDir, "AGENTS.md"));
649
405
  // Create TESTING.md
650
406
  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)
407
+ if (answers.deploymentMode === "docker") {
408
+ // Create Dockerfile.worker for docker mode
409
+ await renderTemplate("Dockerfile.worker.tmpl", variables, join(projectDir, "Dockerfile.worker"));
410
+ }
411
+ // Always generate docker-compose.yml (Redis is needed for all modes)
654
412
  const composeContent = generateDockerCompose({
655
413
  projectName,
656
- gatewayPort: "8080",
414
+ gatewayPort,
657
415
  dockerfilePath: "./Dockerfile.worker",
658
- hasMcpServers: answers.selectedMcpServers.length > 0,
659
- platforms: answers.selectedPlatforms,
416
+ deploymentMode: answers.deploymentMode,
660
417
  });
661
418
  await writeFile(join(projectDir, composeFilename), composeContent);
662
419
  spinner.succeed("Project created successfully!");
@@ -666,62 +423,24 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
666
423
  console.log(chalk.cyan(" 1. Navigate to your project:"));
667
424
  console.log(chalk.dim(` cd ${projectName}\n`));
668
425
  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)"));
426
+ console.log(chalk.dim(" - lobu.toml (agents, providers, skills, network)"));
427
+ console.log(chalk.dim(` - agents/${projectName}/ (IDENTITY.md, SOUL.md, USER.md, skills/)`));
428
+ console.log(chalk.dim(" - skills/ (shared skills — all agents)"));
429
+ console.log(chalk.dim(" - .env (secrets)"));
675
430
  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"));
431
+ if (answers.deploymentMode === "docker") {
432
+ console.log(chalk.dim(" - Dockerfile.worker"));
679
433
  }
680
434
  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"));
435
+ const gatewayUrl = `http://localhost:${gatewayPort}`;
436
+ console.log(chalk.cyan(" 3. Start the services:"));
437
+ console.log(chalk.dim(" lobu dev -d\n"));
438
+ console.log(chalk.cyan(" 4. Open the admin page:"));
439
+ console.log(chalk.dim(` ${gatewayUrl}/agents\n`));
440
+ console.log(chalk.cyan(" 5. View logs:"));
441
+ console.log(chalk.dim(" docker compose logs -f\n"));
442
+ console.log(chalk.cyan(" 6. Stop the services:"));
443
+ console.log(chalk.dim(" docker compose down\n"));
725
444
  }
726
445
  catch (error) {
727
446
  spinner.fail("Failed to create project");
@@ -729,82 +448,55 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
729
448
  }
730
449
  }
731
450
  async function generateLobuToml(projectDir, options) {
451
+ const id = options.agentName;
732
452
  const lines = [
733
453
  "# lobu.toml — Agent configuration",
734
454
  "# Docs: https://lobu.ai/docs/getting-started",
735
455
  "#",
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)",
456
+ "# Each [agents.{id}] defines an agent. The dir field points to a directory",
457
+ "# containing IDENTITY.md, SOUL.md, USER.md, and optionally skills/.",
458
+ "# Shared skills in the root skills/ directory are available to all agents.",
741
459
  "",
742
- "[agent]",
743
- `name = "${options.agentName}"`,
460
+ `[agents.${id}]`,
461
+ `name = "${id}"`,
744
462
  `description = ""`,
463
+ `dir = "./agents/${id}"`,
745
464
  "",
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"]',
465
+ "# LLM providers (order = priority, key = API key or $ENV_VAR)",
754
466
  ];
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
- }
467
+ if (options.providerId && options.providerEnvVar) {
468
+ lines.push(`[[agents.${id}.providers]]`, `id = "${options.providerId}"`, ...(options.providerModel ? [`model = "${options.providerModel}"`] : []), `key = "$${options.providerEnvVar}"`);
469
+ }
470
+ else {
471
+ lines.push("# Add providers via the admin page or uncomment below:", `# [[agents.${id}.providers]]`, '# id = "anthropic"', '# key = "$ANTHROPIC_API_KEY"');
472
+ }
473
+ lines.push("");
474
+ if (options.connectionType && options.connectionConfig) {
475
+ lines.push(`[[agents.${id}.connections]]`, `type = "${options.connectionType}"`);
476
+ lines.push(`[agents.${id}.connections.config]`);
477
+ for (const [key, value] of Object.entries(options.connectionConfig)) {
478
+ lines.push(`${key} = "${value}"`);
764
479
  }
765
480
  }
481
+ else {
482
+ lines.push("# Messaging platform (add via admin page or uncomment below):", `# [[agents.${id}.connections]]`, '# type = "telegram"', `# [agents.${id}.connections.config]`, '# botToken = "$TELEGRAM_BOT_TOKEN"');
483
+ }
484
+ lines.push("", "# Skills from the registry", `[agents.${id}.skills]`, `enabled = [${(options.skillIds ?? []).map((s) => `"${s}"`).join(", ")}]`, "", "# MCP servers (add custom tool servers with optional OAuth):", `# [agents.${id}.skills.mcp.my-mcp]`, '# url = "https://my-mcp.example.com"', `# [agents.${id}.skills.mcp.my-mcp.oauth]`, '# auth_url = "https://auth.example.com/authorize"', '# token_url = "https://auth.example.com/token"', '# client_id = "$MY_MCP_CLIENT_ID"');
766
485
  // Network
486
+ lines.push("", `[agents.${id}.network]`);
767
487
  if (options.allowedDomains) {
768
488
  const domains = options.allowedDomains
769
489
  .split(",")
770
490
  .map((d) => `"${d.trim()}"`)
771
491
  .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");
492
+ lines.push(`allowed = [${domains}]`);
781
493
  }
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);
494
+ else {
495
+ lines.push("allowed = []");
788
496
  }
789
497
  lines.push(""); // trailing newline
790
498
  await writeFile(join(projectDir, "lobu.toml"), lines.join("\n"));
791
499
  }
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
500
  async function getCliVersion() {
809
501
  const pkgPath = new URL("../../package.json", import.meta.url);
810
502
  const pkgContent = await readFile(pkgPath, "utf-8");
@@ -812,41 +504,33 @@ async function getCliVersion() {
812
504
  return pkg.version || "0.1.0";
813
505
  }
814
506
  function generateDockerCompose(options) {
815
- const { projectName, gatewayPort, hasMcpServers, platforms } = options;
816
- const workerImage = `ghcr.io/lobu-ai/lobu-worker-base:latest`;
507
+ const { projectName, gatewayPort, deploymentMode } = options;
817
508
  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
509
+ const workerImage = `ghcr.io/lobu-ai/lobu-worker-base:latest`;
510
+ const dockerSocketMount = deploymentMode === "docker"
823
511
  ? `
824
- MCP_CONFIG_URL: file:///app/.lobu/mcp.config.json
825
- ENCRYPTION_KEY: \${ENCRYPTION_KEY}`
512
+ - /var/run/docker.sock:/var/run/docker.sock`
826
513
  : "";
827
- const telegramEnvVars = platforms.includes("telegram")
514
+ const workerImageEnv = deploymentMode === "docker"
828
515
  ? `
829
- TELEGRAM_ENABLED: \${TELEGRAM_ENABLED:-false}
830
- TELEGRAM_BOT_TOKEN: \${TELEGRAM_BOT_TOKEN:-}
831
- TELEGRAM_ALLOW_FROM: \${TELEGRAM_ALLOW_FROM:-}`
516
+ WORKER_IMAGE: ${workerImage}`
832
517
  : "";
833
- const slackEnvVars = platforms.includes("slack")
518
+ const proxyPort = deploymentMode === "docker"
834
519
  ? `
835
- SLACK_BOT_TOKEN: \${SLACK_BOT_TOKEN}
836
- SLACK_APP_TOKEN: \${SLACK_APP_TOKEN}
837
- SLACK_SIGNING_SECRET: \${SLACK_SIGNING_SECRET}`
520
+ - "127.0.0.1:8118:8118" # HTTP proxy for workers`
838
521
  : "";
839
522
  return `# Generated by @lobu/cli
840
- # You can modify this file as needed
523
+ # Deployment mode: ${deploymentMode}
841
524
 
842
525
  name: ${projectName}
843
526
 
844
527
  services:
845
528
  redis:
846
529
  image: redis:7-alpine
847
- command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --save 60 1 --dir /data
530
+ command: redis-server --maxmemory 256mb --maxmemory-policy noeviction --save 60 1 --dir /data
531
+ working_dir: /tmp
848
532
  volumes:
849
- - redis_data:/data
533
+ - ./data:/data
850
534
  healthcheck:
851
535
  test: ["CMD", "redis-cli", "ping"]
852
536
  interval: 10s
@@ -858,48 +542,39 @@ services:
858
542
 
859
543
  gateway:
860
544
  image: ${gatewayImage}
545
+ pull_policy: always
861
546
  ports:
862
- - "${gatewayPort}:8080"
863
- - "8118:8118" # HTTP proxy for workers
547
+ - "127.0.0.1:\${GATEWAY_PORT:-${gatewayPort}}:8080"${proxyPort}
864
548
  environment:
865
- DEPLOYMENT_MODE: docker
866
- WORKER_IMAGE: ${workerImage}
867
- QUEUE_URL: redis://redis:6379/0${slackEnvVars}${telegramEnvVars}
549
+ DEPLOYMENT_MODE: ${deploymentMode}${workerImageEnv}
550
+ QUEUE_URL: redis://redis:6379/0
868
551
  PUBLIC_GATEWAY_URL: \${PUBLIC_GATEWAY_URL:-}
869
552
  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
553
+ COMPOSE_PROJECT_NAME: ${projectName}
554
+ ADMIN_PASSWORD: \${ADMIN_PASSWORD}
555
+ ENCRYPTION_KEY: \${ENCRYPTION_KEY}
876
556
  WORKER_ALLOWED_DOMAINS: \${WORKER_ALLOWED_DOMAINS:-}
877
557
  WORKER_DISALLOWED_DOMAINS: \${WORKER_DISALLOWED_DOMAINS:-}
878
- volumes:
879
- - /var/run/docker.sock:/var/run/docker.sock${mcpConfigMount}
880
- - env_storage:/app/.lobu/env
558
+ SETTINGS_OAUTH_ISSUER_URL: \${SETTINGS_OAUTH_ISSUER_URL:-}
559
+ SETTINGS_OAUTH_CLIENT_ID: \${SETTINGS_OAUTH_CLIENT_ID:-}
560
+ SETTINGS_OAUTH_CLIENT_SECRET: \${SETTINGS_OAUTH_CLIENT_SECRET:-}
561
+ volumes:${dockerSocketMount}
562
+ - ./.lobu:/app/.lobu
881
563
  networks:
882
- - lobu-public # Internet access
883
- - lobu-internal # Internal services (redis, workers)
564
+ - lobu-public
565
+ - lobu-internal
884
566
  depends_on:
885
567
  redis:
886
568
  condition: service_healthy
887
569
  restart: unless-stopped
888
570
 
889
571
  networks:
890
- # Public network with internet access (gateway only)
891
572
  lobu-public:
892
573
  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
574
  lobu-internal:
897
575
  internal: true
898
576
  driver: bridge
899
577
 
900
- volumes:
901
- redis_data:
902
- env_storage:
903
578
  `;
904
579
  }
905
580
  //# sourceMappingURL=init.js.map