@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.
- package/README.md +8 -8
- package/dist/__tests__/login.test.d.ts +2 -0
- package/dist/__tests__/login.test.d.ts.map +1 -0
- package/dist/__tests__/login.test.js +173 -0
- package/dist/__tests__/login.test.js.map +1 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +3 -5
- package/dist/api/client.js.map +1 -1
- package/dist/api/context.d.ts +22 -0
- package/dist/api/context.d.ts.map +1 -0
- package/dist/api/context.js +113 -0
- package/dist/api/context.js.map +1 -0
- package/dist/api/credentials.d.ts +9 -4
- package/dist/api/credentials.d.ts.map +1 -1
- package/dist/api/credentials.js +127 -15
- package/dist/api/credentials.js.map +1 -1
- package/dist/commands/chat.d.ts +11 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +195 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/context.d.ts +8 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +46 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/dev.d.ts +1 -8
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +236 -57
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +351 -676
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +2 -8
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +283 -14
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +3 -1
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logout.js +5 -3
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/providers/add.d.ts.map +1 -1
- package/dist/commands/providers/add.js +38 -14
- package/dist/commands/providers/add.js.map +1 -1
- package/dist/commands/providers/list.d.ts.map +1 -1
- package/dist/commands/providers/list.js +4 -2
- package/dist/commands/providers/list.js.map +1 -1
- package/dist/commands/skills/add.d.ts.map +1 -1
- package/dist/commands/skills/add.js +25 -7
- package/dist/commands/skills/add.js.map +1 -1
- package/dist/commands/skills/info.d.ts.map +1 -1
- package/dist/commands/skills/info.js.map +1 -1
- package/dist/commands/skills/list.d.ts.map +1 -1
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/registry.d.ts +5 -0
- package/dist/commands/skills/registry.d.ts.map +1 -1
- package/dist/commands/skills/registry.js.map +1 -1
- package/dist/commands/skills/search.d.ts.map +1 -1
- package/dist/commands/skills/search.js +3 -1
- package/dist/commands/skills/search.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +107 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +9 -20
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +17 -3
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config/agents-manifest.d.ts +92 -0
- package/dist/config/agents-manifest.d.ts.map +1 -0
- package/dist/config/agents-manifest.js +7 -0
- package/dist/config/agents-manifest.js.map +1 -0
- package/dist/config/loader.d.ts +18 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +62 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/config/platform-schemas.d.ts +120 -0
- package/dist/config/platform-schemas.d.ts.map +1 -0
- package/dist/config/platform-schemas.js +97 -0
- package/dist/config/platform-schemas.js.map +1 -0
- package/dist/config/schema.d.ts +546 -111
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +26 -19
- package/dist/config/schema.js.map +1 -1
- package/dist/config/transformer.d.ts +3 -0
- package/dist/config/transformer.d.ts.map +1 -1
- package/dist/config/transformer.js +7 -10
- package/dist/config/transformer.js.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -10
- package/dist/index.js.map +1 -1
- package/dist/system-skills.json +89 -29
- package/dist/templates/.env.tmpl +3 -14
- package/dist/templates/.gitignore.tmpl +1 -0
- package/dist/templates/README.md.tmpl +7 -8
- package/dist/utils/markdown.d.ts +3 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/utils/markdown.js +10 -0
- package/dist/utils/markdown.js.map +1 -0
- package/package.json +4 -2
- package/dist/templates/lobu.toml.tmpl +0 -44
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
9
|
-
import {
|
|
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
|
-
//
|
|
141
|
-
const {
|
|
49
|
+
// Deployment mode selection
|
|
50
|
+
const { deploymentMode } = await inquirer.prompt([
|
|
142
51
|
{
|
|
143
|
-
type: "
|
|
144
|
-
name: "
|
|
145
|
-
message: "
|
|
146
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
const {
|
|
64
|
+
],
|
|
65
|
+
default: "embedded",
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
// Gateway port selection
|
|
69
|
+
const { gatewayPort } = await inquirer.prompt([
|
|
242
70
|
{
|
|
243
|
-
type: "
|
|
244
|
-
name: "
|
|
245
|
-
message: "
|
|
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: "
|
|
249
|
-
value: "
|
|
116
|
+
name: "Restricted (recommended) — common registries only (npm, GitHub, PyPI)",
|
|
117
|
+
value: "restricted",
|
|
250
118
|
},
|
|
251
119
|
{
|
|
252
|
-
name: "
|
|
253
|
-
value: "
|
|
120
|
+
name: "Open — workers can access any domain",
|
|
121
|
+
value: "open",
|
|
254
122
|
},
|
|
255
123
|
{
|
|
256
|
-
name: "
|
|
257
|
-
value: "
|
|
124
|
+
name: "Isolated — workers have no internet access",
|
|
125
|
+
value: "isolated",
|
|
258
126
|
},
|
|
259
127
|
],
|
|
260
|
-
|
|
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
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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: "
|
|
350
|
-
name: "
|
|
351
|
-
message:
|
|
352
|
-
|
|
157
|
+
type: "password",
|
|
158
|
+
name: "apiKey",
|
|
159
|
+
message: `${p.displayName} API key:`,
|
|
160
|
+
mask: "*",
|
|
353
161
|
},
|
|
354
162
|
]);
|
|
163
|
+
providerApiKey = apiKey || "";
|
|
355
164
|
}
|
|
356
|
-
|
|
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: "
|
|
359
|
-
name: "
|
|
360
|
-
message: "
|
|
361
|
-
|
|
203
|
+
type: "password",
|
|
204
|
+
name: "botToken",
|
|
205
|
+
message: "Telegram bot token (from @BotFather):",
|
|
206
|
+
mask: "*",
|
|
362
207
|
},
|
|
363
208
|
]);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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: "
|
|
390
|
-
message: "Slack
|
|
391
|
-
|
|
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: "
|
|
401
|
-
message: "Slack
|
|
402
|
-
|
|
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
|
-
|
|
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: "
|
|
435
|
-
message: "
|
|
251
|
+
name: "botToken",
|
|
252
|
+
message: "Discord bot token:",
|
|
253
|
+
mask: "*",
|
|
436
254
|
},
|
|
437
255
|
]);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
//
|
|
447
|
-
const {
|
|
264
|
+
// Settings page OAuth login (optional)
|
|
265
|
+
const { oauthIssuer } = await inquirer.prompt([
|
|
448
266
|
{
|
|
449
|
-
type: "
|
|
450
|
-
name: "
|
|
451
|
-
message: "
|
|
452
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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: "
|
|
486
|
-
name: "
|
|
487
|
-
message: "
|
|
488
|
-
default:
|
|
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: "
|
|
514
|
-
name: "
|
|
515
|
-
message: "
|
|
516
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
307
|
+
// Compute network domains from selected policy
|
|
308
|
+
let allowedDomains;
|
|
309
|
+
let disallowedDomains;
|
|
310
|
+
if (networkPolicy === "open") {
|
|
531
311
|
allowedDomains = "*";
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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
|
|
572
|
-
|
|
573
|
-
await mkdir(
|
|
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
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
642
|
-
await
|
|
643
|
-
await writeFile(join(
|
|
644
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
414
|
+
gatewayPort,
|
|
657
415
|
dockerfilePath: "./Dockerfile.worker",
|
|
658
|
-
|
|
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
|
|
670
|
-
console.log(chalk.dim(
|
|
671
|
-
console.log(chalk.dim(" -
|
|
672
|
-
console.log(chalk.dim(" -
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
"#
|
|
737
|
-
"#
|
|
738
|
-
"#
|
|
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
|
-
|
|
743
|
-
`name = "${
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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(
|
|
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
|
-
|
|
783
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
825
|
-
ENCRYPTION_KEY: \${ENCRYPTION_KEY}`
|
|
512
|
+
- /var/run/docker.sock:/var/run/docker.sock`
|
|
826
513
|
: "";
|
|
827
|
-
const
|
|
514
|
+
const workerImageEnv = deploymentMode === "docker"
|
|
828
515
|
? `
|
|
829
|
-
|
|
830
|
-
TELEGRAM_BOT_TOKEN: \${TELEGRAM_BOT_TOKEN:-}
|
|
831
|
-
TELEGRAM_ALLOW_FROM: \${TELEGRAM_ALLOW_FROM:-}`
|
|
516
|
+
WORKER_IMAGE: ${workerImage}`
|
|
832
517
|
: "";
|
|
833
|
-
const
|
|
518
|
+
const proxyPort = deploymentMode === "docker"
|
|
834
519
|
? `
|
|
835
|
-
|
|
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
|
-
#
|
|
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
|
|
530
|
+
command: redis-server --maxmemory 256mb --maxmemory-policy noeviction --save 60 1 --dir /data
|
|
531
|
+
working_dir: /tmp
|
|
848
532
|
volumes:
|
|
849
|
-
-
|
|
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
|
-
- "
|
|
863
|
-
- "8118:8118" # HTTP proxy for workers
|
|
547
|
+
- "127.0.0.1:\${GATEWAY_PORT:-${gatewayPort}}:8080"${proxyPort}
|
|
864
548
|
environment:
|
|
865
|
-
DEPLOYMENT_MODE:
|
|
866
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
883
|
-
- lobu-internal
|
|
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
|