@lobu/cli 3.0.3 → 3.0.5

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 (113) hide show
  1. package/README.md +18 -222
  2. package/dist/api/context.d.ts +20 -0
  3. package/dist/api/context.d.ts.map +1 -0
  4. package/dist/api/context.js +113 -0
  5. package/dist/api/context.js.map +1 -0
  6. package/dist/api/credentials.d.ts +9 -4
  7. package/dist/api/credentials.d.ts.map +1 -1
  8. package/dist/api/credentials.js +127 -15
  9. package/dist/api/credentials.js.map +1 -1
  10. package/dist/commands/chat.d.ts +16 -0
  11. package/dist/commands/chat.d.ts.map +1 -0
  12. package/dist/commands/chat.js +281 -0
  13. package/dist/commands/chat.js.map +1 -0
  14. package/dist/commands/context.d.ts +8 -0
  15. package/dist/commands/context.d.ts.map +1 -0
  16. package/dist/commands/context.js +46 -0
  17. package/dist/commands/context.js.map +1 -0
  18. package/dist/commands/dev.d.ts +3 -9
  19. package/dist/commands/dev.d.ts.map +1 -1
  20. package/dist/commands/dev.js +95 -64
  21. package/dist/commands/dev.js.map +1 -1
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +421 -673
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/commands/login.d.ts +2 -0
  26. package/dist/commands/login.d.ts.map +1 -1
  27. package/dist/commands/login.js +283 -14
  28. package/dist/commands/login.js.map +1 -1
  29. package/dist/commands/logout.d.ts +3 -1
  30. package/dist/commands/logout.d.ts.map +1 -1
  31. package/dist/commands/logout.js +5 -3
  32. package/dist/commands/logout.js.map +1 -1
  33. package/dist/commands/providers/add.d.ts.map +1 -1
  34. package/dist/commands/providers/add.js +38 -14
  35. package/dist/commands/providers/add.js.map +1 -1
  36. package/dist/commands/providers/list.d.ts.map +1 -1
  37. package/dist/commands/providers/list.js +4 -2
  38. package/dist/commands/providers/list.js.map +1 -1
  39. package/dist/commands/skills/add.d.ts.map +1 -1
  40. package/dist/commands/skills/add.js +25 -7
  41. package/dist/commands/skills/add.js.map +1 -1
  42. package/dist/commands/skills/info.d.ts.map +1 -1
  43. package/dist/commands/skills/info.js +0 -9
  44. package/dist/commands/skills/info.js.map +1 -1
  45. package/dist/commands/skills/list.d.ts.map +1 -1
  46. package/dist/commands/skills/list.js +5 -6
  47. package/dist/commands/skills/list.js.map +1 -1
  48. package/dist/commands/skills/registry.d.ts +6 -11
  49. package/dist/commands/skills/registry.d.ts.map +1 -1
  50. package/dist/commands/skills/registry.js +0 -3
  51. package/dist/commands/skills/registry.js.map +1 -1
  52. package/dist/commands/skills/search.d.ts.map +1 -1
  53. package/dist/commands/skills/search.js +3 -1
  54. package/dist/commands/skills/search.js.map +1 -1
  55. package/dist/commands/status.d.ts +1 -1
  56. package/dist/commands/status.d.ts.map +1 -1
  57. package/dist/commands/status.js +107 -4
  58. package/dist/commands/status.js.map +1 -1
  59. package/dist/commands/validate.d.ts.map +1 -1
  60. package/dist/commands/validate.js +9 -20
  61. package/dist/commands/validate.js.map +1 -1
  62. package/dist/commands/whoami.d.ts +3 -1
  63. package/dist/commands/whoami.d.ts.map +1 -1
  64. package/dist/commands/whoami.js +17 -3
  65. package/dist/commands/whoami.js.map +1 -1
  66. package/dist/config/loader.d.ts.map +1 -1
  67. package/dist/config/loader.js.map +1 -1
  68. package/dist/config/schema.d.ts +546 -111
  69. package/dist/config/schema.d.ts.map +1 -1
  70. package/dist/config/schema.js +26 -19
  71. package/dist/config/schema.js.map +1 -1
  72. package/dist/index.d.ts +0 -4
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +60 -25
  75. package/dist/index.js.map +1 -1
  76. package/dist/utils/markdown.d.ts +3 -0
  77. package/dist/utils/markdown.d.ts.map +1 -0
  78. package/dist/utils/markdown.js +10 -0
  79. package/dist/utils/markdown.js.map +1 -0
  80. package/package.json +4 -3
  81. package/dist/api/client.d.ts +0 -11
  82. package/dist/api/client.d.ts.map +0 -1
  83. package/dist/api/client.js +0 -35
  84. package/dist/api/client.js.map +0 -1
  85. package/dist/commands/launch.d.ts +0 -6
  86. package/dist/commands/launch.d.ts.map +0 -1
  87. package/dist/commands/launch.js +0 -32
  88. package/dist/commands/launch.js.map +0 -1
  89. package/dist/config/transformer.d.ts +0 -11
  90. package/dist/config/transformer.d.ts.map +0 -1
  91. package/dist/config/transformer.js +0 -49
  92. package/dist/config/transformer.js.map +0 -1
  93. package/dist/mcp-servers.d.ts +0 -11
  94. package/dist/mcp-servers.d.ts.map +0 -1
  95. package/dist/mcp-servers.js +0 -27
  96. package/dist/mcp-servers.js.map +0 -1
  97. package/dist/mcp-servers.json +0 -216
  98. package/dist/system-skills.json +0 -561
  99. package/dist/templates/.env.tmpl +0 -29
  100. package/dist/templates/.gitignore.tmpl +0 -32
  101. package/dist/templates/AGENTS.md.tmpl +0 -1
  102. package/dist/templates/Dockerfile.worker.tmpl +0 -29
  103. package/dist/templates/README.md.tmpl +0 -95
  104. package/dist/templates/TESTING.md.tmpl +0 -225
  105. package/dist/templates/lobu.toml.tmpl +0 -44
  106. package/dist/types.d.ts +0 -76
  107. package/dist/types.d.ts.map +0 -1
  108. package/dist/types.js +0 -2
  109. package/dist/types.js.map +0 -1
  110. package/dist/utils/config.d.ts +0 -2
  111. package/dist/utils/config.d.ts.map +0 -1
  112. package/dist/utils/config.js +0 -13
  113. package/dist/utils/config.js.map +0 -1
@@ -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 { secretsSetCommand } from "../commands/secrets.js";
9
+ import { isProviderSkill, loadSkillsRegistry, } from "../commands/skills/registry.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,303 @@ 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 (system skills from registry, excluding provider-only skills)
167
+ const systemSkills = loadSkillsRegistry().filter((s) => !isProviderSkill(s) && !s.hidden);
168
+ const { skillIds } = await inquirer.prompt([
169
+ {
170
+ type: "checkbox",
171
+ name: "skillIds",
172
+ message: "Enable skills?",
173
+ choices: systemSkills.map((s) => ({
174
+ name: `${s.name} — ${s.description}`,
175
+ value: s.id,
176
+ checked: s.id === "github",
177
+ })),
178
+ when: systemSkills.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
+ // Memory
265
+ const { memoryChoice } = await inquirer.prompt([
448
266
  {
449
267
  type: "list",
450
- name: "networkAccessMode",
451
- message: "Configure worker internet access:",
268
+ name: "memoryChoice",
269
+ message: "Memory:",
452
270
  choices: [
271
+ { name: "None (filesystem memory)", value: "none" },
272
+ { name: "Owletto Cloud (owletto.com)", value: "owletto-cloud" },
453
273
  {
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",
274
+ name: "Owletto Local (runs alongside gateway)",
275
+ value: "owletto-local",
464
276
  },
277
+ { name: "Custom Owletto URL", value: "owletto-custom" },
465
278
  ],
466
- default: "filtered",
279
+ default: "none",
467
280
  },
468
281
  ]);
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([
282
+ const envSecrets = [];
283
+ let includeOwlettoLocal = false;
284
+ let owlettoUrl = "";
285
+ if (memoryChoice === "owletto-cloud") {
286
+ owlettoUrl = "https://owletto.com/mcp";
287
+ envSecrets.push({ envVar: "MEMORY_URL", value: owlettoUrl });
288
+ }
289
+ else if (memoryChoice === "owletto-local") {
290
+ includeOwlettoLocal = true;
291
+ owlettoUrl = "http://owletto:8787/mcp";
292
+ envSecrets.push({ envVar: "MEMORY_URL", value: owlettoUrl });
293
+ envSecrets.push({
294
+ envVar: "OWLETTO_AUTH_SECRET",
295
+ value: randomBytes(32).toString("hex"),
296
+ });
297
+ envSecrets.push({
298
+ envVar: "OWLETTO_DB_PASSWORD",
299
+ value: randomBytes(16).toString("hex"),
300
+ });
301
+ }
302
+ else if (memoryChoice === "owletto-custom") {
303
+ const { customOwlettoUrl } = await inquirer.prompt([
512
304
  {
513
- type: "confirm",
514
- name: "addDisallowedDomains",
515
- message: "Add disallowed domains? (Optional - blocks specific domains within allowed patterns)",
516
- default: false,
305
+ type: "input",
306
+ name: "customOwlettoUrl",
307
+ message: "Owletto MCP URL:",
308
+ validate: (v) => (v ? true : "URL is required"),
517
309
  },
518
310
  ]);
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
- }
311
+ owlettoUrl = customOwlettoUrl;
312
+ envSecrets.push({ envVar: "MEMORY_URL", value: owlettoUrl });
529
313
  }
530
- else if (networkAccessMode === "unrestricted") {
314
+ // "none" no env var needed, gateway defaults to filesystem memory
315
+ // Compute network domains from selected policy
316
+ let allowedDomains;
317
+ let disallowedDomains;
318
+ if (networkPolicy === "open") {
531
319
  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
- }
320
+ disallowedDomains = "";
321
+ }
322
+ else if (networkPolicy === "isolated") {
323
+ allowedDomains = "";
324
+ disallowedDomains = "";
325
+ }
326
+ else {
327
+ // restricted (default)
328
+ allowedDomains = [
329
+ "registry.npmjs.org",
330
+ ".npmjs.org",
331
+ "github.com",
332
+ ".github.com",
333
+ ".githubusercontent.com",
334
+ "cdn.jsdelivr.net",
335
+ "unpkg.com",
336
+ "pypi.org",
337
+ "files.pythonhosted.org",
338
+ ].join(",");
339
+ disallowedDomains = "";
550
340
  }
551
- // else isolated mode: leave both empty
552
- // Generate encryption key for credentials
553
341
  const encryptionKey = randomBytes(32).toString("hex");
554
342
  const answers = {
555
343
  ...baseAnswers,
556
- ...credentialAnswers,
557
- anthropicApiKey,
558
- publicUrl,
344
+ deploymentMode: deploymentMode,
559
345
  encryptionKey,
560
- selectedMcpServers,
561
- selectedPlatforms,
562
- telegramBotToken,
563
- telegramAllowFrom,
564
346
  allowedDomains,
565
347
  disallowedDomains,
566
348
  };
@@ -568,95 +350,78 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
568
350
  const composeFilename = "docker-compose.yml";
569
351
  const spinner = ora("Creating Lobu project...").start();
570
352
  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
- }
353
+ // Create data directory in project directory
354
+ await mkdir(join(projectDir, "data"), { recursive: true });
594
355
  // Generate lobu.toml
595
356
  await generateLobuToml(projectDir, {
596
357
  agentName: projectName,
597
- platforms: answers.selectedPlatforms,
598
- mcpServers: answers.selectedMcpServers,
599
358
  allowedDomains: answers.allowedDomains,
359
+ providerId: providerId || undefined,
360
+ providerEnvVar: selectedProvider?.providers?.[0]?.envVarName,
361
+ providerModel: selectedProvider?.providers?.[0]?.defaultModel,
362
+ connectionType: platformType || undefined,
363
+ connectionConfig: Object.keys(connectionConfig).length > 0 ? connectionConfig : undefined,
364
+ skillIds: selectedSkillIds,
600
365
  });
601
366
  const variables = {
602
367
  PROJECT_NAME: projectName,
603
368
  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,
369
+ DEPLOYMENT_MODE: answers.deploymentMode,
370
+ ADMIN_PASSWORD: adminPassword,
612
371
  ENCRYPTION_KEY: answers.encryptionKey,
613
- ANTHROPIC_API_KEY: answers.anthropicApiKey || "",
614
- PUBLIC_GATEWAY_URL: answers.publicUrl || "http://localhost:8080",
615
- GATEWAY_PORT: "8080",
372
+ GATEWAY_PORT: gatewayPort,
616
373
  WORKER_ALLOWED_DOMAINS: answers.allowedDomains,
617
374
  WORKER_DISALLOWED_DOMAINS: answers.disallowedDomains,
618
375
  };
619
376
  // Create .env file
620
377
  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
- }
378
+ // Save public gateway URL if explicitly set
379
+ if (publicGatewayUrl) {
380
+ await secretsSetCommand(projectDir, "PUBLIC_GATEWAY_URL", publicGatewayUrl);
381
+ }
382
+ // Save provider API key to .env
383
+ if (providerApiKey && selectedProvider?.providers?.[0]?.envVarName) {
384
+ await secretsSetCommand(projectDir, selectedProvider.providers[0].envVarName, providerApiKey);
385
+ }
386
+ // Save connection secrets to .env
387
+ for (const secret of connectionSecrets) {
388
+ await secretsSetCommand(projectDir, secret.envVar, secret.value);
389
+ }
390
+ // Save OAuth secrets to .env
391
+ for (const secret of envSecrets) {
392
+ await secretsSetCommand(projectDir, secret.envVar, secret.value);
635
393
  }
636
394
  // Create .gitignore
637
395
  await renderTemplate(".gitignore.tmpl", {}, join(projectDir, ".gitignore"));
638
396
  // Create README
639
397
  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
398
+ // Create agent directory with instruction files
399
+ const agentDir = join(projectDir, "agents", projectName);
400
+ await mkdir(agentDir, { recursive: true });
401
+ await writeFile(join(agentDir, "IDENTITY.md"), `# Identity\n\nYou are ${projectName}, a helpful AI assistant.\n`);
402
+ await writeFile(join(agentDir, "SOUL.md"), `# Instructions\n\nBe concise and helpful. Ask clarifying questions when the request is ambiguous.\n`);
403
+ await writeFile(join(agentDir, "USER.md"), `# User Context\n\n<!-- Add user-specific preferences, timezone, environment details here -->\n`);
404
+ // Create agent-specific skills directory
405
+ await mkdir(join(agentDir, "skills"), { recursive: true });
406
+ await writeFile(join(agentDir, "skills", ".gitkeep"), "");
407
+ // Create shared skills directory
645
408
  await mkdir(join(projectDir, "skills"), { recursive: true });
646
409
  await writeFile(join(projectDir, "skills", ".gitkeep"), "");
647
410
  // Create AGENTS.md
648
411
  await renderTemplate("AGENTS.md.tmpl", variables, join(projectDir, "AGENTS.md"));
649
412
  // Create TESTING.md
650
413
  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)
414
+ if (answers.deploymentMode === "docker") {
415
+ // Create Dockerfile.worker for docker mode
416
+ await renderTemplate("Dockerfile.worker.tmpl", variables, join(projectDir, "Dockerfile.worker"));
417
+ }
418
+ // Always generate docker-compose.yml (Redis is needed for all modes)
654
419
  const composeContent = generateDockerCompose({
655
420
  projectName,
656
- gatewayPort: "8080",
421
+ gatewayPort,
657
422
  dockerfilePath: "./Dockerfile.worker",
658
- hasMcpServers: answers.selectedMcpServers.length > 0,
659
- platforms: answers.selectedPlatforms,
423
+ deploymentMode: answers.deploymentMode,
424
+ includeOwlettoLocal,
660
425
  });
661
426
  await writeFile(join(projectDir, composeFilename), composeContent);
662
427
  spinner.succeed("Project created successfully!");
@@ -666,62 +431,35 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
666
431
  console.log(chalk.cyan(" 1. Navigate to your project:"));
667
432
  console.log(chalk.dim(` cd ${projectName}\n`));
668
433
  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)"));
434
+ console.log(chalk.dim(" - lobu.toml (agents, providers, skills, network)"));
435
+ console.log(chalk.dim(` - agents/${projectName}/ (IDENTITY.md, SOUL.md, USER.md, skills/)`));
436
+ console.log(chalk.dim(" - skills/ (shared skills — all agents)"));
437
+ console.log(chalk.dim(" - .env (secrets)"));
675
438
  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"));
439
+ if (answers.deploymentMode === "docker") {
440
+ console.log(chalk.dim(" - Dockerfile.worker"));
679
441
  }
680
442
  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
- }
443
+ const gatewayUrl = `http://localhost:${gatewayPort}`;
444
+ if (owlettoUrl) {
445
+ const displayUrl = includeOwlettoLocal
446
+ ? "http://localhost:8787"
447
+ : owlettoUrl;
448
+ console.log(chalk.cyan(" Owletto:"));
449
+ console.log(chalk.dim(` ${displayUrl}\n`));
713
450
  }
714
- else {
715
- console.log(chalk.cyan(" 3. Start the services:"));
451
+ console.log(chalk.cyan(" 3. Start the services:"));
452
+ console.log(chalk.dim(" lobu run -d\n"));
453
+ if (includeOwlettoLocal) {
454
+ console.log(chalk.cyan(" 4. Set up Owletto (first run):"));
455
+ console.log(chalk.dim(" Visit http://localhost:8787 to create your account\n"));
716
456
  }
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"));
457
+ console.log(chalk.cyan(` ${includeOwlettoLocal ? "5" : "4"}. Open the API docs:`));
458
+ console.log(chalk.dim(` ${gatewayUrl}/api/docs\n`));
459
+ console.log(chalk.cyan(` ${includeOwlettoLocal ? "6" : "5"}. View logs:`));
460
+ console.log(chalk.dim(" docker compose logs -f\n"));
461
+ console.log(chalk.cyan(` ${includeOwlettoLocal ? "7" : "6"}. Stop the services:`));
462
+ console.log(chalk.dim(" docker compose down\n"));
725
463
  }
726
464
  catch (error) {
727
465
  spinner.fail("Failed to create project");
@@ -729,82 +467,55 @@ export async function initCommand(cwd = process.cwd(), projectNameArg) {
729
467
  }
730
468
  }
731
469
  async function generateLobuToml(projectDir, options) {
470
+ const id = options.agentName;
732
471
  const lines = [
733
472
  "# lobu.toml — Agent configuration",
734
473
  "# Docs: https://lobu.ai/docs/getting-started",
735
474
  "#",
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)",
475
+ "# Each [agents.{id}] defines an agent. The dir field points to a directory",
476
+ "# containing IDENTITY.md, SOUL.md, USER.md, and optionally skills/.",
477
+ "# Shared skills in the root skills/ directory are available to all agents.",
741
478
  "",
742
- "[agent]",
743
- `name = "${options.agentName}"`,
479
+ `[agents.${id}]`,
480
+ `name = "${id}"`,
744
481
  `description = ""`,
482
+ `dir = "./agents/${id}"`,
745
483
  "",
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"]',
484
+ "# LLM providers (order = priority, key = API key or $ENV_VAR)",
754
485
  ];
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
- }
486
+ if (options.providerId && options.providerEnvVar) {
487
+ lines.push(`[[agents.${id}.providers]]`, `id = "${options.providerId}"`, ...(options.providerModel ? [`model = "${options.providerModel}"`] : []), `key = "$${options.providerEnvVar}"`);
488
+ }
489
+ else {
490
+ lines.push("# Add providers via the gateway configuration APIs or uncomment below:", `# [[agents.${id}.providers]]`, '# id = "anthropic"', '# key = "$ANTHROPIC_API_KEY"');
491
+ }
492
+ lines.push("");
493
+ if (options.connectionType && options.connectionConfig) {
494
+ lines.push(`[[agents.${id}.connections]]`, `type = "${options.connectionType}"`);
495
+ lines.push(`[agents.${id}.connections.config]`);
496
+ for (const [key, value] of Object.entries(options.connectionConfig)) {
497
+ lines.push(`${key} = "${value}"`);
764
498
  }
765
499
  }
500
+ else {
501
+ lines.push("# Messaging platform (add via the gateway configuration APIs or uncomment below):", `# [[agents.${id}.connections]]`, '# type = "telegram"', `# [agents.${id}.connections.config]`, '# botToken = "$TELEGRAM_BOT_TOKEN"');
502
+ }
503
+ 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
504
  // Network
505
+ lines.push("", `[agents.${id}.network]`);
767
506
  if (options.allowedDomains) {
768
507
  const domains = options.allowedDomains
769
508
  .split(",")
770
509
  .map((d) => `"${d.trim()}"`)
771
510
  .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");
511
+ lines.push(`allowed = [${domains}]`);
784
512
  }
785
- if (platformLines.length > 0) {
786
- lines.push("", "[platforms]");
787
- lines.push(...platformLines);
513
+ else {
514
+ lines.push("allowed = []");
788
515
  }
789
516
  lines.push(""); // trailing newline
790
517
  await writeFile(join(projectDir, "lobu.toml"), lines.join("\n"));
791
518
  }
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
519
  async function getCliVersion() {
809
520
  const pkgPath = new URL("../../package.json", import.meta.url);
810
521
  const pkgContent = await readFile(pkgPath, "utf-8");
@@ -812,41 +523,87 @@ async function getCliVersion() {
812
523
  return pkg.version || "0.1.0";
813
524
  }
814
525
  function generateDockerCompose(options) {
815
- const { projectName, gatewayPort, hasMcpServers, platforms } = options;
816
- const workerImage = `ghcr.io/lobu-ai/lobu-worker-base:latest`;
526
+ const { projectName, gatewayPort, deploymentMode, includeOwlettoLocal } = options;
817
527
  const gatewayImage = `ghcr.io/lobu-ai/lobu-gateway:latest`;
818
- const mcpConfigMount = hasMcpServers
528
+ const workerImage = `ghcr.io/lobu-ai/lobu-worker-base:latest`;
529
+ const dockerSocketMount = deploymentMode === "docker"
819
530
  ? `
820
- - ./.lobu/mcp.config.json:/app/.lobu/mcp.config.json:ro`
531
+ - /var/run/docker.sock:/var/run/docker.sock`
821
532
  : "";
822
- const mcpEnvVars = hasMcpServers
533
+ const workerImageEnv = deploymentMode === "docker"
823
534
  ? `
824
- MCP_CONFIG_URL: file:///app/.lobu/mcp.config.json
825
- ENCRYPTION_KEY: \${ENCRYPTION_KEY}`
535
+ WORKER_IMAGE: ${workerImage}`
826
536
  : "";
827
- const telegramEnvVars = platforms.includes("telegram")
537
+ const proxyPort = deploymentMode === "docker"
828
538
  ? `
829
- TELEGRAM_ENABLED: \${TELEGRAM_ENABLED:-false}
830
- TELEGRAM_BOT_TOKEN: \${TELEGRAM_BOT_TOKEN:-}
831
- TELEGRAM_ALLOW_FROM: \${TELEGRAM_ALLOW_FROM:-}`
539
+ - "127.0.0.1:8118:8118" # HTTP proxy for workers`
832
540
  : "";
833
- const slackEnvVars = platforms.includes("slack")
541
+ const owlettoServices = includeOwlettoLocal
834
542
  ? `
835
- SLACK_BOT_TOKEN: \${SLACK_BOT_TOKEN}
836
- SLACK_APP_TOKEN: \${SLACK_APP_TOKEN}
837
- SLACK_SIGNING_SECRET: \${SLACK_SIGNING_SECRET}`
543
+ owletto-postgres:
544
+ image: pgvector/pgvector:pg17
545
+ environment:
546
+ POSTGRES_USER: owletto
547
+ POSTGRES_PASSWORD: \${OWLETTO_DB_PASSWORD}
548
+ POSTGRES_DB: owletto
549
+ volumes:
550
+ - owletto-pgdata:/var/lib/postgresql/data
551
+ healthcheck:
552
+ test: ["CMD-SHELL", "pg_isready -U owletto -d owletto"]
553
+ interval: 10s
554
+ timeout: 3s
555
+ retries: 5
556
+ networks:
557
+ - lobu-internal
558
+ restart: unless-stopped
559
+
560
+ owletto:
561
+ image: ghcr.io/lobu-ai/owletto-app:latest
562
+ pull_policy: always
563
+ ports:
564
+ - "127.0.0.1:8787:8787"
565
+ environment:
566
+ DATABASE_URL: postgresql://owletto:\${OWLETTO_DB_PASSWORD}@owletto-postgres:5432/owletto
567
+ BETTER_AUTH_SECRET: \${OWLETTO_AUTH_SECRET}
568
+ PORT: "8787"
569
+ HOST: 0.0.0.0
570
+ networks:
571
+ - lobu-internal
572
+ depends_on:
573
+ owletto-postgres:
574
+ condition: service_healthy
575
+ healthcheck:
576
+ test: ["CMD-SHELL", "curl -f http://127.0.0.1:8787/health || exit 1"]
577
+ interval: 10s
578
+ timeout: 10s
579
+ retries: 10
580
+ start_period: 20s
581
+ restart: unless-stopped
582
+ `
583
+ : "";
584
+ const owlettoDependsOn = includeOwlettoLocal
585
+ ? `
586
+ owletto:
587
+ condition: service_healthy`
588
+ : "";
589
+ const owlettoVolumes = includeOwlettoLocal
590
+ ? `
591
+ volumes:
592
+ owletto-pgdata:
593
+ `
838
594
  : "";
839
595
  return `# Generated by @lobu/cli
840
- # You can modify this file as needed
596
+ # Deployment mode: ${deploymentMode}
841
597
 
842
598
  name: ${projectName}
843
599
 
844
600
  services:
845
601
  redis:
846
602
  image: redis:7-alpine
847
- command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru --save 60 1 --dir /data
603
+ command: redis-server --maxmemory 256mb --maxmemory-policy noeviction --save 60 1 --dir /data
604
+ working_dir: /tmp
848
605
  volumes:
849
- - redis_data:/data
606
+ - ./data:/data
850
607
  healthcheck:
851
608
  test: ["CMD", "redis-cli", "ping"]
852
609
  interval: 10s
@@ -855,51 +612,42 @@ services:
855
612
  networks:
856
613
  - lobu-internal
857
614
  restart: unless-stopped
858
-
615
+ ${owlettoServices}
859
616
  gateway:
860
617
  image: ${gatewayImage}
618
+ pull_policy: always
861
619
  ports:
862
- - "${gatewayPort}:8080"
863
- - "8118:8118" # HTTP proxy for workers
620
+ - "127.0.0.1:\${GATEWAY_PORT:-${gatewayPort}}:8080"${proxyPort}
864
621
  environment:
865
- DEPLOYMENT_MODE: docker
866
- WORKER_IMAGE: ${workerImage}
867
- QUEUE_URL: redis://redis:6379/0${slackEnvVars}${telegramEnvVars}
622
+ DEPLOYMENT_MODE: ${deploymentMode}${workerImageEnv}
623
+ QUEUE_URL: redis://redis:6379/0
868
624
  PUBLIC_GATEWAY_URL: \${PUBLIC_GATEWAY_URL:-}
625
+ GATEWAY_PORT: \${GATEWAY_PORT:-${gatewayPort}}
869
626
  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
627
+ COMPOSE_PROJECT_NAME: ${projectName}
628
+ ADMIN_PASSWORD: \${ADMIN_PASSWORD}
629
+ ENCRYPTION_KEY: \${ENCRYPTION_KEY}
876
630
  WORKER_ALLOWED_DOMAINS: \${WORKER_ALLOWED_DOMAINS:-}
877
631
  WORKER_DISALLOWED_DOMAINS: \${WORKER_DISALLOWED_DOMAINS:-}
878
- volumes:
879
- - /var/run/docker.sock:/var/run/docker.sock${mcpConfigMount}
880
- - env_storage:/app/.lobu/env
632
+ MEMORY_URL: \${MEMORY_URL:-}
633
+ LOBU_WORKSPACE_ROOT: /workspace/project
634
+ volumes:${dockerSocketMount}
635
+ - .:/workspace/project:ro
881
636
  networks:
882
- - lobu-public # Internet access
883
- - lobu-internal # Internal services (redis, workers)
637
+ - lobu-public
638
+ - lobu-internal
884
639
  depends_on:
885
640
  redis:
886
- condition: service_healthy
641
+ condition: service_healthy${owlettoDependsOn}
887
642
  restart: unless-stopped
888
643
 
889
644
  networks:
890
- # Public network with internet access (gateway only)
891
645
  lobu-public:
892
646
  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
647
  lobu-internal:
897
648
  internal: true
898
649
  driver: bridge
899
-
900
- volumes:
901
- redis_data:
902
- env_storage:
650
+ ${owlettoVolumes}
903
651
  `;
904
652
  }
905
653
  //# sourceMappingURL=init.js.map