@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.
- package/README.md +18 -222
- package/dist/api/context.d.ts +20 -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 +16 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +281 -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 +3 -9
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +95 -64
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +421 -673
- package/dist/commands/init.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 +0 -9
- 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 +5 -6
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/registry.d.ts +6 -11
- package/dist/commands/skills/registry.d.ts.map +1 -1
- package/dist/commands/skills/registry.js +0 -3
- 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/loader.d.ts.map +1 -1
- package/dist/config/loader.js.map +1 -1
- 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/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -25
- package/dist/index.js.map +1 -1
- 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 -3
- package/dist/api/client.d.ts +0 -11
- package/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js +0 -35
- package/dist/api/client.js.map +0 -1
- package/dist/commands/launch.d.ts +0 -6
- package/dist/commands/launch.d.ts.map +0 -1
- package/dist/commands/launch.js +0 -32
- package/dist/commands/launch.js.map +0 -1
- package/dist/config/transformer.d.ts +0 -11
- package/dist/config/transformer.d.ts.map +0 -1
- package/dist/config/transformer.js +0 -49
- package/dist/config/transformer.js.map +0 -1
- package/dist/mcp-servers.d.ts +0 -11
- package/dist/mcp-servers.d.ts.map +0 -1
- package/dist/mcp-servers.js +0 -27
- package/dist/mcp-servers.js.map +0 -1
- package/dist/mcp-servers.json +0 -216
- package/dist/system-skills.json +0 -561
- package/dist/templates/.env.tmpl +0 -29
- package/dist/templates/.gitignore.tmpl +0 -32
- package/dist/templates/AGENTS.md.tmpl +0 -1
- package/dist/templates/Dockerfile.worker.tmpl +0 -29
- package/dist/templates/README.md.tmpl +0 -95
- package/dist/templates/TESTING.md.tmpl +0 -225
- package/dist/templates/lobu.toml.tmpl +0 -44
- package/dist/types.d.ts +0 -76
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/utils/config.d.ts +0 -2
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -13
- package/dist/utils/config.js.map +0 -1
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 { 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
|
-
//
|
|
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 (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: "
|
|
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
|
+
// Memory
|
|
265
|
+
const { memoryChoice } = await inquirer.prompt([
|
|
448
266
|
{
|
|
449
267
|
type: "list",
|
|
450
|
-
name: "
|
|
451
|
-
message: "
|
|
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: "
|
|
455
|
-
value: "
|
|
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: "
|
|
279
|
+
default: "none",
|
|
467
280
|
},
|
|
468
281
|
]);
|
|
469
|
-
|
|
470
|
-
let
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
".
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
"
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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: "
|
|
514
|
-
name: "
|
|
515
|
-
message: "
|
|
516
|
-
|
|
305
|
+
type: "input",
|
|
306
|
+
name: "customOwlettoUrl",
|
|
307
|
+
message: "Owletto MCP URL:",
|
|
308
|
+
validate: (v) => (v ? true : "URL is required"),
|
|
517
309
|
},
|
|
518
310
|
]);
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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
|
|
572
|
-
|
|
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
|
-
|
|
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,
|
|
369
|
+
DEPLOYMENT_MODE: answers.deploymentMode,
|
|
370
|
+
ADMIN_PASSWORD: adminPassword,
|
|
612
371
|
ENCRYPTION_KEY: answers.encryptionKey,
|
|
613
|
-
|
|
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
|
-
//
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
642
|
-
await
|
|
643
|
-
await writeFile(join(
|
|
644
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
421
|
+
gatewayPort,
|
|
657
422
|
dockerfilePath: "./Dockerfile.worker",
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
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)"));
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
|
|
682
|
-
if (
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
715
|
-
|
|
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.
|
|
718
|
-
console.log(chalk.
|
|
719
|
-
console.log(chalk.
|
|
720
|
-
console.log(chalk.
|
|
721
|
-
console.log(chalk.
|
|
722
|
-
console.log(chalk.
|
|
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
|
-
"#
|
|
737
|
-
"#
|
|
738
|
-
"#
|
|
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
|
-
|
|
743
|
-
`name = "${
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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(
|
|
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
|
-
|
|
786
|
-
lines.push("
|
|
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,
|
|
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
|
|
528
|
+
const workerImage = `ghcr.io/lobu-ai/lobu-worker-base:latest`;
|
|
529
|
+
const dockerSocketMount = deploymentMode === "docker"
|
|
819
530
|
? `
|
|
820
|
-
-
|
|
531
|
+
- /var/run/docker.sock:/var/run/docker.sock`
|
|
821
532
|
: "";
|
|
822
|
-
const
|
|
533
|
+
const workerImageEnv = deploymentMode === "docker"
|
|
823
534
|
? `
|
|
824
|
-
|
|
825
|
-
ENCRYPTION_KEY: \${ENCRYPTION_KEY}`
|
|
535
|
+
WORKER_IMAGE: ${workerImage}`
|
|
826
536
|
: "";
|
|
827
|
-
const
|
|
537
|
+
const proxyPort = deploymentMode === "docker"
|
|
828
538
|
? `
|
|
829
|
-
|
|
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
|
|
541
|
+
const owlettoServices = includeOwlettoLocal
|
|
834
542
|
? `
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
#
|
|
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
|
|
603
|
+
command: redis-server --maxmemory 256mb --maxmemory-policy noeviction --save 60 1 --dir /data
|
|
604
|
+
working_dir: /tmp
|
|
848
605
|
volumes:
|
|
849
|
-
-
|
|
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
|
-
- "
|
|
863
|
-
- "8118:8118" # HTTP proxy for workers
|
|
620
|
+
- "127.0.0.1:\${GATEWAY_PORT:-${gatewayPort}}:8080"${proxyPort}
|
|
864
621
|
environment:
|
|
865
|
-
DEPLOYMENT_MODE:
|
|
866
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
632
|
+
MEMORY_URL: \${MEMORY_URL:-}
|
|
633
|
+
LOBU_WORKSPACE_ROOT: /workspace/project
|
|
634
|
+
volumes:${dockerSocketMount}
|
|
635
|
+
- .:/workspace/project:ro
|
|
881
636
|
networks:
|
|
882
|
-
- lobu-public
|
|
883
|
-
- lobu-internal
|
|
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
|